{"id":16487423,"url":"https://github.com/tjenkinson/state-manager","last_synced_at":"2025-03-23T12:33:54.591Z","repository":{"id":37072155,"uuid":"262599892","full_name":"tjenkinson/state-manager","owner":"tjenkinson","description":"StateManager provides a controlled way of managing a state object, and being notified when parts of it have changed in an atomic fashion.","archived":false,"fork":false,"pushed_at":"2024-04-12T21:20:37.000Z","size":1430,"stargazers_count":3,"open_issues_count":4,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-04-14T10:01:02.790Z","etag":null,"topics":["atomic","listener","proxy","state","subscriber","update"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tjenkinson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2020-05-09T15:24:37.000Z","updated_at":"2024-04-22T22:46:01.209Z","dependencies_parsed_at":"2024-01-09T22:47:28.690Z","dependency_job_id":"22bddf45-5b6c-4599-9fed-022bf209268d","html_url":"https://github.com/tjenkinson/state-manager","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjenkinson%2Fstate-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjenkinson%2Fstate-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjenkinson%2Fstate-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjenkinson%2Fstate-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tjenkinson","download_url":"https://codeload.github.com/tjenkinson/state-manager/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221851005,"owners_count":16891699,"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":["atomic","listener","proxy","state","subscriber","update"],"created_at":"2024-10-11T13:34:11.917Z","updated_at":"2024-10-28T15:50:46.094Z","avatar_url":"https://github.com/tjenkinson.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![npm version](https://badge.fury.io/js/%40tjenkinson%2Fstate-manager.svg)](https://badge.fury.io/js/%40tjenkinson%2Fstate-manager)\n\n# State Manager\n\nThis provides a controlled way of managing a state object/array, and being notified when parts of it have changed. It ensures that state updates are atomic, meaning change subscribers are only notified of changes when the state has been updated completely. Subscribers also get the latest state and changes whenver they are invoked.\n\nIt requires [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy) support. If you are running on an environment where this is unavailable I'd recommend [GoogleChrome/proxy-polyfill](https://github.com/GoogleChrome/proxy-polyfill). You can provide the implementation on the `Proxy` config option if you don't want to polyfill. Take note of the differences in behaviour to real `Proxy`'s in the README.\n\nSubscribers receive a `hasChanged` function which when called with a property path will return `true` if something at or below the provided path has changed since the subscriber was last invoked.\n\nSubscribers are also able to update the state. Earlier subscribers see the changes from later subscribers. The last subsciber won't see the intermediary changes.\n\nChanges to values on [plain objects (created by the `Object` constructor)](https://github.com/jonschlinkert/is-plain-object) or arrays, and nested plain objects or arrays in the state are detected (as these are proxied). Values inside other types of objects are not watched.\n\n## Installation\n\n```sh\nnpm install --save @tjenkinson/state-manager\n```\n\nor available on JSDelivr at \"https://cdn.jsdelivr.net/npm/@tjenkinson/state-manager@4\".\n\n## API\n\n### Constructor\n\nProvide the initial state as the first argument. You must not mutate this state directly. You can get a reference to a mutable version using the `getState()` method.\n\nThe second argument is an optional object which can contain the following properties:\n\n- `beforeUpdate`: This is called after the first `update()` call but before the callback. It receives the state as the first argument and you are allowed to update it.\n- `afterUpdate`: This is called after an update occurs after the last subscriber has finished. It receives an object in the first argument with the following:\n  - `state:` A `Proxy` to the current state.\n  - `exceptionOccurred`: This is a boolean which is `true` if an exception occured in one or more of the subscribers.\n  - `retrieveExceptions`: This returns an array of exceptions that occurred in one of more of the subscribers. If you do not call this function the exceptions will be thrown asynchronously when the current stack ends. If you do call this function the exceptions will not be thrown and it's up to you to handle them.\n\nYou can also provide the type of the state as a generic. E.g. `new StateManager\u003cStateType\u003e`.\n\n```ts\nconst initialState = { a: 1, b: 'something' };\nconst stateManager = new StateManager(initialState, {\n  // optional\n  beforeUpdate: (state) =\u003e {},\n  afterUpdate: ({ state, exceptionOccurred, retrieveException }) =\u003e {},\n});\n```\n\n### getState()\n\nReturns a mutable version of the state. This is not a snapshot. The object you get back is a `Proxy` to the original state. Properties to plain objects or arrays are also `Proxy`'s.\n\nIf you are updating multiple properties, you should use [`update`](#updatefn) instead to batch the updates together and only notify subscribers once all updates are done.\n\n```ts\nstateManager.getState();\n```\n\n### hasChanged(...propertyPath)\n\nInforms you if the thing at the given property path or below has changed. This can be useful when you are subscribing if you want to catch up with changes you missed.\n\n```ts\nstateManager.hasChanged(...propertyPath);\n```\n\n### update(fn)\n\nThis is how you update the state.\n\nThe first argument takes a function that will be invoked synchronously and provided with a `Proxy` to the current state as the first argument. To make changes to the state just update the object. It is possible to have multiple nested update calls and the subscribers will only be invoked when all calls have completed.\n\n```ts\nstateManager.update((state) =\u003e {\n  // update `state` here\n});\n```\n\nIt is possible to omit function, which is only useful if `beforeUpdate` makes changes to the state.\n\nThe return value is passed through.\n\n### subscribe(fn)\n\nThis is how you are notified of changes to the state.\n\nThe first argument taked a function which is invoked with 2 arguments:\n\n- `hasChanged`: This is a function which takes a property path. It returns `true` if something at or below the provided path has changed since the subscriber was last invoked. E.g. `hasChanged('a', 'b')` for checking `state.a.b`.\n- `state`: This is a mutable `Proxy` to the current state.\n\nYou are allowed to update the state again from your subscriber.\n\nSubscribers are invoked in the order they were registered. If a subscriber changes the state the first subscriber will be invoked again. This means earlier subscribers see the changes from later subscribers. The last subscriber won't see the intermediary changes.\n\nAn object is returned that contains a `remove` function. Call this to unsubscribe.\n\nIf your subscriber throws an error it will not prevent other subscribers being invoked. The `afterUpdate` function will have an oppurtunity to handle the exception, or it will be rethrown asynchronously.\n\n## Example\n\n```ts\nimport { StateManager } from '@tjenkinson/state-manager';\n\nconst stateManager = new StateManager({ a: 1, b: 2, c: { d: 3, e: 4 } });\n\nstateManager.subscribe((hasChanged, state) =\u003e {\n  if (hasChanged('a')) {\n    console.log(`subscriber1 a=${state.a}`);\n  }\n});\n\nstateManager.subscribe((hasChanged, { b }) =\u003e {\n  if (hasChanged('b')) {\n    console.log(`subscriber2 b=${b}`);\n    if (b === 3) {\n      stateManager.update((state) =\u003e (state.a = 1));\n    }\n  }\n});\n\nstateManager.subscribe((hasChanged, state) =\u003e {\n  if (hasChanged('c', 'e')) {\n    console.log(`subscriber3 c.e=${state.c.e}`);\n  }\n});\n\nstateManager.subscribe((hasChanged, state) =\u003e {\n  console.log(`subscriber4 state=${JSON.stringify(state)}`);\n});\n\nstateManager.update((state) =\u003e {\n  state.a = 100;\n  stateManager.update((state2) =\u003e {\n    state2.a = 2;\n  });\n});\n\n// just before reaching this point there the following would be logged\n// - subscriber1 a=2\n// - subscriber4 state={\"a\":2,\"b\":2\",c:{\"d\":3,\"e\":4}}\n\nstateManager.update((state) =\u003e {\n  state.b = 3;\n  state.c.e = 5;\n});\n\n// just before reaching this point there the following would be logged\n// - subscriber2 b=3\n// - subscriber1 a=1\n// - subscriber3 c.e=5\n// - subscriber4 state={\"a\":1,\"b\":3\",c:{\"d\":3,\"e\":5}}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftjenkinson%2Fstate-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftjenkinson%2Fstate-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftjenkinson%2Fstate-manager/lists"}