{"id":18583178,"url":"https://github.com/gaoding-inc/stateshot","last_synced_at":"2025-04-09T23:19:39.784Z","repository":{"id":44759561,"uuid":"155691497","full_name":"gaoding-inc/stateshot","owner":"gaoding-inc","description":" 💾 Non-aggressive history state management with structure sharing.","archived":false,"fork":false,"pushed_at":"2024-06-15T11:46:02.000Z","size":190,"stargazers_count":195,"open_issues_count":5,"forks_count":11,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-02T21:09:15.100Z","etag":null,"topics":["history-management","history-state","javascript","json","state","state-management","undo-redo"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gaoding-inc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-11-01T09:26:05.000Z","updated_at":"2025-02-19T08:27:32.000Z","dependencies_parsed_at":"2024-06-18T17:02:23.236Z","dependency_job_id":null,"html_url":"https://github.com/gaoding-inc/stateshot","commit_stats":null,"previous_names":["doodlewind/stateshot"],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaoding-inc%2Fstateshot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaoding-inc%2Fstateshot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaoding-inc%2Fstateshot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaoding-inc%2Fstateshot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gaoding-inc","download_url":"https://codeload.github.com/gaoding-inc/stateshot/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248125882,"owners_count":21051820,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["history-management","history-state","javascript","json","state","state-management","undo-redo"],"created_at":"2024-11-07T00:20:52.405Z","updated_at":"2025-04-09T23:19:39.755Z","avatar_url":"https://github.com/gaoding-inc.png","language":"JavaScript","readme":"# StateShot\n💾 Non-aggressive history state management with structure sharing.\n\n\u003cp\u003e\n  \u003ca href=\"https://travis-ci.org/gaoding-inc/stateshot\"\u003e\n    \u003cimg src=\"https://travis-ci.org/gaoding-inc/stateshot.svg?branch=master\"/\u003e\n  \u003c/a\u003e\n  \u003ca href='https://coveralls.io/github/gaoding-inc/stateshot?branch=master'\u003e\n    \u003cimg src='https://coveralls.io/repos/github/gaoding-inc/stateshot/badge.svg?branch=master' alt='Coverage Status'/\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://unpkg.com/stateshot/dist/stateshot.min.js\"\u003e\n    \u003cimg src=\"https://img.shields.io/bundlephobia/minzip/stateshot\"/\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://standardjs.com\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/code_style-standard-brightgreen.svg\"/\u003e\n  \u003c/a\u003e\n  \u003ca href=\"./package.json\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/stateshot.svg?maxAge=300\u0026label=version\u0026colorB=007ec6\u0026maxAge=300\"/\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n![stateshot](./resources/logo.png)\n\n\u003e Just push your states into StateShot and `undo` / `redo` them!\n\n\n## Getting Started\n\nInstall via NPM:\n\n```bash\nnpm i stateshot\n```\n\nBasic usage:\n\n```js\nimport { History } from 'stateshot'\n\nconst state = { a: 1, b: 2 }\n\nconst history = new History()\nhistory.pushSync(state) // the terser `history.push` API is async\n\nstate.a = 2 // mutation!\nhistory.pushSync(state)\n\nhistory.get() // { a: 2, b: 2 }\nhistory.undo().get() // { a: 1, b: 2 }\nhistory.redo().get() // { a: 2, b: 2 }\n```\n\n## Concepts\nFor history state management, the top need is the `undo` / `redo` API. That's what StateShot provides out of the box, which can be simply described in image below:\n\n![stateshot](./resources/concept-1.png)\n\nTrivial, right? While in real world projects, the price on saving full state is high. Immutable data structure is known to be suitable for this, since it can share data structure in different references. However, this requires fully adaptation to immutable libs - can be aggressive indeed.\n\nStateShot supports sharable data structure under its tiny API surface. The core concept is to serialize state node into chunks, computing chunks' hash and share same space if hash meets:\n\n![stateshot](./resources/concept-2.png)\n\nBesides the flexible rule-based transforming StateShot supports, it also provides another low-hanging fruit optimization for SPA apps. Suppose your root state is composed of multi \"pages\", editing on one page does not affect other pages. In this case computing hash on full state is inefficient. As a solution, you can simply specify a `pickIndex` on pushing new state, telling the lib which page to record:\n\n![stateshot](./resources/concept-3.png)\n\nWith this hint, only the affected child's hash will be re-computed. Other children simply remains the same with previous record.\n\n\n## API\n\n### `History`\n`new History(options?: Options)`\n\nMain class for state management, option includes:\n\n* `initialState` - Optional initial state.\n* `rules` - Optional rules array for optimizing data transforming.\n* `delay` - Debounce time for `push` in milliseconds, `50` by default.\n* `maxLength` - Max length saving history states, `100` by default.\n* `useChunks` - Whether serializing state data into chunks. `true` by default.\n* `onChange` - Fired when pushing / pulling states with changed state passed in.\n\n\u003e If you want to use StateShot with immutable data, simply set `useChunks` to `false` and new reference to state will be directly saved as records.\n\n#### `push`\n`(state: State, pickIndex?: number) =\u003e Promise\u003cHistory\u003e`\n\nPush state data into history, using `pushSync` under the hood. `state` doesn't have to be JSON serializable since you can define rules to parse it.\n\nIf `pickIndex` is specified, only this index of state's child will be serialized. Other children will be copied from previous record. This optimization only happens if previous records exists.\n\n#### `pushSync`\n`(state: State, pickIndex?: number) =\u003e History`\n\nPush state into history stack immediately. `pickIndex` also supported.\n\n#### `undo`\n`() =\u003e History`\n\nUndo a record if possible, supports chaining, e.g., `undo().undo().get()`.\n\n#### `redo`\n`() =\u003e History`\n\nRedo a record if possible, also supports chaining,\n\n#### `hasUndo`\n`boolean`\n\nWhether current state has undo records before.\n\n#### `hasRedo`\n`boolean`\n\nWhether current state has redo records after.\n\n#### `length`\n`number`\n\nValid record length of current instance.\n\n#### `get`\n`() =\u003e State`\n\nPull out a history state from records.\n\n#### `reset`\n`() =\u003e History`\n\nClear internal data structure.\n\n\n### `Rule`\n`{ match: function, toRecord: function, fromRecord: function }`\n\nBy defining rules you can specify how to transform between states and internal \"chunks\". Chunks are used for structure sharing.\n\n\u003e Rules are only designed for optimization. You don't have to learn or use them unless you've encountered performance bottleneck.\n\n#### `match`\n`node: StateNode =\u003e boolean`\n\nDefines whether a rule can be matched. For example, if you're saving a vDOM state with different `type` field, just define some rules like `node =\u003e node.type === 'image'` or `node =\u003e node.type === 'text'`.\n\n#### `toRecord`\n`StateNode =\u003e { chunks: Chunks, children: Children }`\n\nFor matched node, `chunks` is the serializable data we transform it into, and `children` picks out its children for further traversing (By default we traverse the `children` field in each state node, you can customize this behavior by providing code like `children: node.elements` or so). Usually one chunk per node is enough, but you can split a node into multi chunks in this manner:\n\n```js\nconst state = {\n  type: 'container',\n  children: [\n    { type: 'image', left: 100, top: 100, image: 'foo' },\n    { type: 'image', left: 200, top: 200, image: 'bar' },\n    { type: 'image', left: 300, top: 300, image: 'baz' }\n  ]\n}\n\n// Suppose `image` is a heavy field, we can split this field as a chunk.\nconst toRecord = node =\u003e ({\n  chunks: [\n    { ...node, image: null },\n    node.image\n  ]\n})\n```\n\n#### `fromRecord`\n`{ chunks: Chunks, children: Children } =\u003e StateNode`\n\nParse the chunks back into the state node. For case before:\n\n```js\n// Recover state node from multi chunks.\nconst fromRecord = ({ chunks, children }) =\u003e ({\n  ...chunks[0],\n  image: chunks[1]\n})\n\nconst rule = {\n  match: ({ type }) =\u003e type === 'image',\n  toRecord,\n  fromRecord\n}\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaoding-inc%2Fstateshot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgaoding-inc%2Fstateshot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaoding-inc%2Fstateshot/lists"}