{"id":21485968,"url":"https://github.com/persevie/statemanjs","last_synced_at":"2025-10-31T08:31:57.388Z","repository":{"id":63297264,"uuid":"566777612","full_name":"persevie/statemanjs","owner":"persevie","description":"Proper state manager for JavaScript","archived":false,"fork":false,"pushed_at":"2024-08-15T20:32:31.000Z","size":2254,"stargazers_count":51,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-16T14:09:24.269Z","etag":null,"topics":["javascript","mutability","mutable-state","state","state-management","storage","typescript"],"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/persevie.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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":"2022-11-16T11:58:53.000Z","updated_at":"2025-02-14T21:13:15.000Z","dependencies_parsed_at":"2024-06-21T13:03:20.238Z","dependency_job_id":"eeb27495-5cb6-4298-b7bd-e1a5849be52d","html_url":"https://github.com/persevie/statemanjs","commit_stats":{"total_commits":84,"total_committers":2,"mean_commits":42.0,"dds":0.04761904761904767,"last_synced_commit":"a790baf1b002dc02b839ed56deefd16c3f5c789d"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/persevie%2Fstatemanjs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/persevie%2Fstatemanjs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/persevie%2Fstatemanjs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/persevie%2Fstatemanjs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/persevie","download_url":"https://codeload.github.com/persevie/statemanjs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239154800,"owners_count":19590769,"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":["javascript","mutability","mutable-state","state","state-management","storage","typescript"],"created_at":"2024-11-23T13:18:17.067Z","updated_at":"2025-10-31T08:31:57.381Z","avatar_url":"https://github.com/persevie.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n\u003cimg height=\"60\" alt=\"Statemanjs logo\" src=\"./assets/stateman-js-logo-full.png\"\u003e\n\u003c/p\u003e\n\n[![codecov](https://codecov.io/gh/persevie/statemanjs/branch/main/graph/badge.svg?token=5NICXEETTY)](https://codecov.io/gh/persevie/statemanjs)\n[![npm version](https://img.shields.io/npm/v/@persevie/statemanjs)](https://www.npmjs.com/package/@persevie/statemanjs)\n[![npm downloads](https://img.shields.io/npm/dm/@persevie/statemanjs)](https://www.npmjs.com/package/@persevie/statemanjs)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@persevie/statemanjs)](https://bundlephobia.com/result?p=@persevie/statemanjs)\n[![license](https://img.shields.io/npm/l/@persevie/statemanjs)](https://github.com/persevie/statemanjs/blob/main/LICENSE)\n[![GitHub stars](https://img.shields.io/github/stars/persevie/statemanjs?style=social)](https://github.com/persevie/statemanjs/stargazers)\n\n```ts\nimport {\n    createState,\n    createComputedState,\n    StatemanjsAPI,\n} from \"@persevie/statemanjs\";\n\ntype Planet = {\n    name: string;\n    system: string;\n    satelites: string[];\n    hasLife: boolean;\n    distance: number;\n    averageTemperature: number;\n};\n\ntype Coordinates = {\n    latitude: number;\n    longitude: number;\n};\n\ntype Rover = {\n    planet: string;\n    name: string;\n    days: number;\n    batteryCharge: number;\n    status: string;\n    weatherOutside: string;\n    coordinates: Coordinates;\n};\n\nconst planetState = createState\u003cPlanet\u003e({\n    name: \"Earth\",\n    system: \"Solar System\",\n    satelites: [],\n    hasLife: true,\n    distance: 1_000_000,\n    averageTemperature: 15,\n});\n\nconst planetStateUnsub = planetState.subscribe((state) =\u003e {\n    console.log(\"Planet state updated:\", state);\n});\n\nconst planetStateDistanceUnsub = planetState.subscribe(\n    (state) =\u003e {\n        console.log(\"Planet state distance updated:\", state.distance);\n    },\n    {\n        properties: [\"distance\"],\n    },\n);\n\nplanetState.update((state) =\u003e {\n    state.satelites.push(\"Moon\"); // \u003c-- This will not trigger the planet state distance subscription\n});\n\n// --\u003e Planet state updated: { name: 'Earth', system: 'Solar System', satelites: [\"Moon\"], hasLife: true, distance: 1000000 }\n\nplanetState.update((state) =\u003e {\n    state.distance = 224_000_900; // \u003c-- This will trigger the planet state distance subscription\n});\n\n// --\u003e Planet state updated: { name: 'Earth', system: 'Solar System', satelites: [\"Moon\"], hasLife: true, distance: 224000900 }\n// --\u003e Planet state distance updated: 224000900\n\nplanetState.set({\n    name: \"Mars\",\n    system: \"Solar System\",\n    satelites: [\"Phobos\", \"Deimos\"],\n    hasLife: false,\n    distance: 100,\n    averageTemperature: -63,\n}); // \u003c-- This will trigger both planet state distance and planet state subscription\n\n// --\u003e Planet state updated: { name: 'Mars', system: 'Solar System', satelites: [\"Phobos\", \"Deimos\"], hasLife: false, distance: 100, averageTemperature: -63 }\n// --\u003e Planet state distance updated: 100\n\nplanetStateUnsub(); // \u003c-- Unsubscribe from planet state\nplanetStateDistanceUnsub(); // \u003c-- Unsubscribe from planet state distance\n\nconst marsExplorerState = createState\u003cRover\u003e({\n    planet: \"Mars\",\n    name: \"MarsExplorer\",\n    days: 0,\n    batteryCharge: 100,\n    status: \"On the way\",\n    weatherOutside: \"unknown\",\n    coordinates: {\n        latitude: 0,\n        longitude: 0,\n    },\n});\n\nfunction generateReport(state: StatemanjsAPI\u003cRover\u003e): string {\n    return `Rover report state updated. My status is ${\n        state.get().status\n    }. I'm on day ${state.get().days}. My battery charge is ${\n        state.get().batteryCharge\n    }. Weather outside is ${state.get().weatherOutside}. My coordinates are ${\n        state.get().coordinates.latitude\n    }, ${state.get().coordinates.longitude}.\n    My coordinates are: lat ${state.get().coordinates.latitude}, long ${\n        state.get().coordinates.longitude\n    }.\n    The weather outside is: ${state.get().weatherOutside}.`;\n}\n\nconst marsExplorerDaysState = marsExplorerState.createSelector(\n    (state) =\u003e state.days,\n);\n\nmarsExplorerDaysState.subscribe((state) =\u003e {\n    console.log(\"MarsExplorer Days state updated:\", state);\n});\n\nconst marsExplorerReportState = createComputedState\u003cstring\u003e((): string =\u003e {\n    return generateReport(marsExplorerState);\n}, [marsExplorerState]); // \u003c-- State of report. Generate mars explorer report state every MarsExplorerState change\n\nmarsExplorerReportState.subscribe((state) =\u003e {\n    console.log(state);\n});\n\nmarsExplorerState.set({\n    planet: \"Mars\",\n    name: \"MarsExplorer\",\n    days: 10,\n    batteryCharge: 85,\n    status: \"Active\",\n    weatherOutside: \"Sunny\",\n    coordinates: {\n        latitude: 4.5,\n        longitude: 137.4,\n    },\n});\n\n// --\u003e Rover report state updated. My status is Active. I'm on day 10. My battery charge is 85. Weather outside is Sunny.\n// --\u003e MarsExplorer Days state updated: 10\n\nmarsExplorerState.subscribe(\n    () =\u003e {\n        charge(marsExplorerState);\n    },\n    { notifyCondition: (s): boolean =\u003e s.batteryCharge \u003c 10 },\n);\n\nfunction charge(roverState: StatemanjsAPI\u003cRover\u003e) {\n    roverState.asyncAction(async (state: StatemanjsAPI\u003cRover\u003e) =\u003e {\n        console.log(\"Charging the rover...\");\n\n        await new Promise((resolve) =\u003e setTimeout(resolve, 10000));\n\n        state.update((state) =\u003e {\n            state.batteryCharge = 100;\n        });\n    });\n}\n\nmarsExplorerState.set({\n    planet: \"Mars\",\n    name: \"MarsExplorer\",\n    days: 8,\n    batteryCharge: 0,\n    status: \"Inactive\",\n    weatherOutside: \"Sunny\",\n    coordinates: {\n        latitude: -14.6,\n        longitude: 130.7,\n    },\n});\n\n// --\u003e Charging the rover...\n\n// 10s waiting\n\n// --\u003e Rover report state updated. My status is Inactive. I'm on day 8. My battery charge is 100. Weather outside is Sunny.\n```\n\n# Table of Contents\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n- [Introduction](#introduction)\n- [Why Statemanjs](#why-statemanjs)\n    - [Architectural Requirements](#architectural-requirements)\n    - [Developer Experience Requirements](#developer-experience-requirements)\n    - [How Statemanjs Delivers](#how-statemanjs-delivers)\n- [API](#api)\n- [Any data type as a state](#any-data-type-as-a-state)\n- [Installation](#installation)\n- [Usage](#usage)\n    - [Subscribe to changes](#subscribe-to-changes)\n    - [State change](#state-change)\n    - [Unwrap](#unwrap)\n    - [Computed state](#computed-state)\n    - [Selectors](#selectors)\n    - [Async actions](#async-actions)\n    - [Debug](#debug)\n        - [Transactions](#transactions)\n    - [Custom Comparators](#custom-comparators)\n        - [Using Custom Comparators](#using-custom-comparators)\n            - [Example](#example)\n            - [Example](#example-1)\n            - [Example Usage of Default Comparators](#example-usage-of-default-comparators)\n- [Performance](#performance)\n    - [Standard Operations Benchmark](#standard-operations-benchmark)\n    - [Extreme Scale: Million-Record Stress Test](#extreme-scale-million-record-stress-test)\n    - [Legacy Fill Benchmark](#legacy-fill-benchmark)\n    - [Running Benchmarks](#running-benchmarks)\n- [Integrations](#integrations)\n- [For contributors](#for-contributors)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n# Introduction\n\nStatemanjs is a framework-agnostic library for managing state in JavaScript and NodeJS applications. It combines deterministic scheduling with developer-friendly ergonomics to deliver both architectural discipline and exceptional performance.\n\n- Deterministic graph evaluation with deferred computed scheduler\n- Dynamic lifecycle without memory leaks via FinalizationRegistry\n- Single subscription primitive instead of fragmented event APIs\n- Production-grade performance: 2–20× faster across workloads\n- Built-in transactions and debug tooling without external devtools\n- Cross-runtime ready: browser, Node.js, workers, SSR\n- TypeScript-first with automatic type inference and zero dependencies\n\n# Why Statemanjs\n\nModern state management requires more than just storing and updating data. A production-ready solution must address fundamental architectural challenges while maintaining developer productivity. Below are the core requirements for any state manager, and how Statemanjs fulfills them.\n\n## Architectural Requirements\n\n**Deterministic updates.** The order of computations must be predictable and independent of declaration order. Complex dependency graphs—including diamond shapes and deep chains—should resolve without race conditions or glitches.\n\n**Dynamic lifecycle.** Stores should be created, reused, and destroyed on demand. Memory and subscriptions must be released automatically or with minimal overhead, without leaving dangling references.\n\n**Lazy reactivity.** Values should only recompute when consumers are actively listening. Changes in one branch shouldn't trigger cascading updates if results remain unchanged.\n\n**Consistency and safety.** Errors in callbacks shouldn't corrupt the dependency graph. Computed values must be read-only. Subscribers that throw errors should be safely removed.\n\n**Async resilience.** API calls, timers, and side-effects shouldn't create race conditions or partial update states. The library should provide structured ways to handle async operations.\n\n**Cross-platform compatibility.** The same store instance should work identically in browsers, Node.js, Web Workers, and SSR environments without relying on DOM globals.\n\n**Infinite scalability.** Performance must remain predictable even with millions of state elements. No sudden degradation or need for workarounds like manual virtualization at the store layer.\n\n## Developer Experience Requirements\n\n**Linear readability.** Business logic shouldn't be scattered across event graphs. Dependencies and subscriptions should be obvious from the code structure.\n\n**Minimal DSL.** One subscription primitive instead of dozens of event methods. Unified update API without cognitive overhead of choosing between `watch`, `sample`, `merge`, etc.\n\n**Type safety.** Full TypeScript inference from initial values. No manual type annotations for computed states or selectors. Zero runtime type errors from mismatched shapes.\n\n**Observability.** Built-in tracing, snapshots, and transaction history. No dependency on external devtools or browser extensions for debugging production issues.\n\n**Framework agnostic.** The same business logic should work with React, Vue, Solid, or no framework at all. Official adapters should expose identical store APIs.\n\n## How Statemanjs Delivers\n\nStatemanjs was built from scratch to fulfill every requirement simultaneously:\n\n- **Deferred computed scheduler** (`computedScheduler`) batches recomputations until all dependencies flush, preventing glitches in diamond graphs and cyclic dependency detection throws clear errors early.\n- **FinalizationRegistry integration** automatically removes dangling subscriptions after garbage collection. Computed values track active listeners and skip work when unused.\n- **Single `subscribe` method** with optional `properties`, `notifyCondition`, and `protect` flags replaces fragmented event APIs. Explicit dependency arrays make graphs readable.\n- **Transaction API** groups updates into atomic blocks with metadata (timestamp, tags, initiator). `DebugService` exposes full history without external tools.\n- **Linear scaling to millions of elements.** Stress tests with 1M+ items show consistent microsecond-level single-item updates and ~2M ops/sec bulk operations without state corruption.\n- **Full generic API** with automatic type inference. `createState\u003cT\u003e()` derives shape from initial value; `createComputedState` infers return types from callback functions.\n\nEvery design decision prioritizes **both** architectural correctness and developer ergonomics. No compromises between safety and speed, or between flexibility and simplicity.\n\n# API\n\nAny manipulations with your state are possible only through built-in methods, so they should be understandable and convenient.\nThe `createState` method is used to create a state:\n\n```ts\n/**\n * Accepts a new state and compares it with the current one.\n * Nothing will happen if the passed value is equal to the current one.\n * @param newState New state.\n * @returns Status of operation.\n */\nset(newState: T, options?: SetOptions\u003cT\u003e): boolean;\n\n/** Get current state */\nget(): T;\n\n/**\n * The method of subscribing to the status change.\n * Accepts a callback function (subscription callback),\n * which will be called at each update, and a subscription options object.\n * In the options, you can specify information about the subscription,\n * as well as specify the condition under which the subscriber will be notified\n * and mark the subscriber as protected. All subscribers are unprotected by default.\n * Protected subscribers can only be unsubscribed using the unsubscribe method returned by this method.\n * Returns the unsubscribe callback function.\n *\n * @param subscriptionCb A function that runs on every update.\n * @param subscriptionOptions Additional information and notification condition.\n * @returns Unsubscribe callback function.\n */\nsubscribe(\n    subscriptionCb: SubscriptionCb\u003cT\u003e,\n    subscriptionOptions?: SubscriptionOptions\u003cT\u003e,\n): UnsubscribeCb;\n\n/** Remove all unprotected subscribers */\nunsubscribeAll(): void;\n\n/**\n * Returns count of all active subscribers.\n * @returns number.\n */\ngetActiveSubscribersCount(): number;\n\n/**\n * Flexible state update.\n * @param updateCb Callback for state updates.\n */\nupdate(updateCb: UpdateCb\u003cT\u003e, options?: UpdateOptions\u003cT\u003e): boolean;\n\n/**\n * Unwrap a proxy object to a regular JavaScript object\n * @returns unwrapped state\n */\nunwrap(): T;\n\n/**\n * Dispatch an async action\n * @param action An async action. It accepts a stateManager object,\n * which is used to access the current state.\n * @returns Promise.\n */\nasyncAction(\n    action: (stateManager: StatemanjsAPI\u003cT\u003e) =\u003e Promise\u003cvoid\u003e,\n): Promise\u003cvoid\u003e;\n\n/**\n * Create a computed state for a state property.\n * @param selectorFn A function that returns a value of a state property.\n * @returns A computed state.\n */\ncreateSelector\u003cE\u003e(\n    selectorFn: (state: T) =\u003e E,\n    subscriptionOptions?: SubscriptionOptions\u003cunknown\u003e,\n): StatemanjsComputedAPI\u003cE\u003e;\n\n/**\n * Debug API. Allows you to use additional debugging functionality such as transactions.\n * Parameters are set when creating the state.\n * @see {DebugAPI}\n */\nDEBUG?: DebugAPI\u003cT\u003e;\n```\n\nThe `createComputedState` method is used to create a computed state:\n\n```ts\ncreateComputedState\u003cT\u003e(callback: () =\u003e T, deps: (StatemanjsAPI\u003cany\u003e | StatemanjsComputedAPI\u003cany\u003e)[]): StatemanjsComputedAPI\u003cT\u003e\n```\n\n`StatemanjsComputedAPI\u003cT\u003e`\n\n```ts\n/** Get current state */\nget(): T;\n/**\n * The method of subscribing to the status change.\n * Accepts a callback function (subscription callback),\n * which will be called at each update, and a subscription options object.\n * In the options, you can specify information about the subscription,\n * as well as specify the condition under which the subscriber will be notified\n * and mark the subscriber as protected. All subscribers are unprotected by default.\n * Protected subscribers can only be unsubscribed using the unsubscribe method returned by this method.\n * Returns the unsubscribe callback function.\n *\n * @param subscriptionCb A function that runs on every update.\n * @param subscriptionOptions Additional information and notification condition.\n * @returns Unsubscribe callback function.\n */\nsubscribe(\n    subscriptionCb: SubscriptionCb\u003cT\u003e,\n    subscriptionOptions?: SubscriptionOptions\u003cT\u003e,\n): UnsubscribeCb;\n\n/** Remove all unprotected subscribers */\nunsubscribeAll(): void;\n\n/**\n * Returns count of all active subscribers.\n * @returns number.\n */\ngetActiveSubscribersCount(): number;\n\n/**\n * Unwrap a proxy object to a regular JavaScript object\n * @returns unwrapped state\n */\nunwrap(): T;\n```\n\n`TransactionAPI\u003cT\u003e`\n\n```ts\n/**\n * The total number of transactions that have occurred since the state was initialized.\n */\ntotalTransactions: number;\n\n/**\n * Adds a new transaction to the transaction chain.\n *\n * @param {T} snapshot - The snapshot of the state to be added as a transaction.\n */\naddTransaction(snapshot: T): void;\n\n/**\n * Retrieves the last transaction in the transaction chain.\n *\n * @returns {Transaction\u003cT\u003e | null} The last transaction, or null if there are no transactions.\n */\ngetLastTransaction(): Transaction\u003cT\u003e | null;\n\n/**\n * Retrieves all transactions that have occurred.\n *\n * @returns {Transaction\u003cT\u003e[]} An array of all transactions.\n */\ngetAllTransactions(): Transaction\u003cT\u003e[];\n\n/**\n * Retrieves a specific transaction by its number in the transaction chain.\n *\n * @param {number} transactionNumber - The number of the transaction to retrieve.\n * @returns {Transaction\u003cT\u003e | null} The transaction with the specified number, or null if it doesn't exist.\n */\ngetTransactionByNumber(transactionNumber: number): Transaction\u003cT\u003e | null;\n\n/**\n * Retrieves the difference between the current state and the last transaction.\n *\n * @returns {TransactionDiff\u003cT\u003e | null} The difference between the current state and the last transaction, or null if there are no transactions.\n */\ngetLastDiff(): TransactionDiff\u003cT\u003e | null;\n\n/**\n * Retrieves the difference between two specific transactions.\n *\n * @param {number} transactionA - The number of the first transaction.\n * @param {number} transactionB - The number of the second transaction.\n * @returns {TransactionDiff\u003cT\u003e | null} The difference between the two specified transactions, or null if the transactions don't exist or there is no difference.\n */\ngetDiffBetween(\n    transactionA: number,\n    transactionB: number,\n): TransactionDiff\u003cT\u003e | null;\n```\n\n`DebugAPI\u003cT\u003e`\n\n```ts\ntransactionService: TransactionAPI\u003cT\u003e;\n```\n\n# Any data type as a state\n\nA state can be anything from primitives to complex and multidimensional objects. Just pass this to the `createState` function and use the state with no extra effort.\n\n```ts\nconst isLoading = createState(true);\n\nconst soComplexObject = createState({\n    1: { 2: { 3: { 4: { 5: [{ foo: \"bar\" }] } } } },\n});\n```\n\n# Installation\n\n```bash\nnpm i @persevie/statemanjs\n```\n\n# Usage\n\nTo use Statemanjs, you'll need to create a state object and interact with it using the provided API methods.\n\nHere's an example of creating a state object for storing a user's name:\n\n```js\nimport { createState } from \"@persevie/statemanjs\";\n\nconst userState = createState({ name: \"Jake\" });\n```\n\nYou can also pass in the type of your state if you are using TypeScript:\n\n```ts\nimport { createState } from \"@persevie/statemanjs\";\n\ntype User = {\n    name: string;\n    age: number;\n};\n\nconst userState = createState\u003cUser\u003e({ name: \"Finn\", age: 13 });\n```\n\nTo get the current state, use the `get` method.\n\n```js\nconst counterState = createState(1);\n\nconst counter = counterState.get(); // 1\n```\n\n## Subscribe to changes\n\nThe `subscribe` method takes a callback function and executes it on every state change. This callback function accepts the updated state.\n\n```js\nconst counterState = createState(0);\n\n// the 'state' parameter is the updated (current) state\ncounterState.subscribe((state) =\u003e {\n    if (Number.isInteger(state)) {\n        console.log(\"it's integer\");\n    } else {\n        console.log(\"it's not integer\");\n    }\n});\n```\n\nYou can set a condition, `notifyCondition`, under which the callback will be called. This condition is the second and optional parameter. If there is no condition, then the callback will fire on every state change. `notifyCondition` also accepts the updated state.\n\n```js\nconst counterState = createState(0);\n\ncounterState.subscribe(\n    (state) =\u003e {\n        console.log(\"it's integer\");\n    },\n    { notifyCondition: (state) =\u003e Number.isInteger(state) },\n);\n```\n\nTo protect a subscriber - pass `protect: true` to the second argument of the object. Protected subscribers can only be unsubscribed using the unsubscribe method returned by the `subscribe` method.\n\n```js\nconst counterState = createState(0);\n\ncounterState.subscribe(\n    (state) =\u003e {\n        console.log(\"it's integer\");\n    },\n    { notifyCondition: (state) =\u003e Number.isInteger(state), protect: true },\n);\n```\n\nYou can specify which properties you want the subscriber to be notified when they change (at least one). If none of the properties have been changed, the subscriber will not be notified. Note that the `set` method always replaces the state, so use the `update` method to observe the properties correctly. Set is set.\n\n```js\nconst userState = createState({\n    name: \"Jake\",\n    surname: \"Dog\",\n    info: { hobbies: [] },\n});\n\nuserState.subscribe(\n    (state) =\u003e {\n        console.log(`The name has been changed: ${state.name}`);\n    },\n    { properties: [\"name\"] },\n);\n\nuserState.subscribe(\n    (state) =\u003e {\n        console.log(\n            `Hobbies have been changed: ${state.info.hobbies.join(\", \")}`,\n        );\n    },\n    { properties: [\"info.hobbies\"] },\n);\n```\n\nThe `subscribe` method returns a callback to unsubscribe.\n\n```js\nconst counterState = createState(0);\n\nconst unsub = counterState.subscribe(\n    (state) =\u003e {\n        console.log(\"it's integer\");\n    },\n    { notifyCondition: (state) =\u003e Number.isInteger(state) },\n);\n\n// cancel subscribe\nunsub();\n```\n\nTo unsubscribe all active and unprotected subscriptions from a state, use the `unsubscribeAll` method;\n\n```js\ncounterState.unsubscribeAll();\n```\n\nSometimes you need to find out how many active subscriptions a state has, for this there is a `getActiveSubscribersCount` method.\n\n```js\nconst subscribersCount = counterState.getActiveSubscribersCount();\n```\n\n## State change\n\nThere are two ways to change the state - `set` and `update`. The `set` method completely changes the state and is great for primitives and simple states.\n\n```js\nconst counterState = createState(0);\n\ncounterState.subscribe(\n    (state) =\u003e {\n        console.log(\"it's integer\");\n    },\n    { notifyCondition: (state) =\u003e Number.isInteger(state) },\n);\n\ncounterState.set(2); // 2\n\ncounterState.set(counterState.get() * 2); // 4\n```\n\nThe `update` method is suitable for complex states (objects and arrays) in which only part of the state needs to be changed. The `update` method accepts the current state.\n\n```ts\nimport { createState } from \"@persevie/statemanjs\";\n\ntype User = {\n    name: string;\n    age: number;\n    isOnline: boolean;\n    hobbyes: Array\u003cstring\u003e;\n};\n\nconst userState = createState\u003cUser\u003e({\n    name: \"Finn\",\n    age: 13,\n    isOnline: false,\n    hobbyes: [],\n});\n\nuserState.update((state) =\u003e {\n    state.isOnline = !state.isOnline;\n});\n\nuserState.update((state) =\u003e {\n    state.hobbyes.push(\"adventure\");\n});\n```\n\n## Unwrap\n\nIf you want unwrap state to javascript object - use `unwrap()` method:\n\n```ts\nimport { createState } from \"@persevie/statemanjs\";\n\ntype User = {\n    name: string;\n    age: number;\n    isOnline: boolean;\n    hobbyes: Array\u003cstring\u003e;\n};\n\nconst userState = createState\u003cUser\u003e({\n    name: \"Finn\",\n    age: 13,\n    isOnline: false,\n    hobbyes: [],\n});\n\nconst unwrappedUser = userState.unwrap();\n```\n\n## Computed state\n\nYou can create a computed state with the `createComputedState` function. It returns an instance of statemanjs, but without the ability to set or update the state because of its specificity (_see the `StatemanjsComputedAPI` interface_).\nThis function takes two parameters:\n\n- A callback function to create a state value (_run when at least one of the dependencies has been changed_).\n- An array of dependencies (_an instance of statemanjs_).\n\nComputed state creates only protected subscribers.\n\n```ts\nconst problemState = createState\u003cboolean\u003e(false);\n\nconst statusComputedState = createComputedState\u003cstring\u003e((): string =\u003e {\n    return problemState.get()\n        ? \"Houston, we have a problem\"\n        : \"Houston, everything is fine\";\n}, [problemState]);\n```\n\n## Selectors\n\nYou can create a selector for a state object to track changes only to it. A selector is a computed state, but only for the current state and its property.\n\n```js\nconst state = createState({ count: 0, value: 42 });\n\nstate.subscribe((newState) =\u003e {\n    console.log(\"State changed:\", newState);\n});\n\nconst countSelector = state.createSelector(\n    (currentState) =\u003e currentState.count,\n);\ncountSelector.subscribe((newCount) =\u003e {\n    console.log(\"Count changed:\", newCount);\n});\n```\n\n## Async actions\n\nIf you need to change state asynchronously, for example to set data from an api call, you can use the `asyncAction` method. It takes a callback function with a state instance as a parameter.\n\n```js\nconst state = createState({ count: 0, value: 0 });\n\nstate.subscribe((newState) =\u003e {\n    console.log(\"State changed:\", newState);\n});\n\nstate.asyncAction(async (stateManager) =\u003e {\n    await new Promise((resolve) =\u003e setTimeout(resolve, 10000));\n\n    stateManager.update((s) =\u003e {\n        s.count++;\n    });\n});\n```\n\n## Debug\n\n### Transactions\n\n```js\nconst arrState = createState([], { transactionsLen: 10 });\n\nconst gat = () =\u003e arrState.DEBUG.transactionService.getAllTransactions();\n\narrState.subscribe((state) =\u003e {\n    console.log(\"diff: \", arrState.DEBUG.transactionService.getLastDiff());\n});\n\narrState.set([0, 1]);\n\nconst arr = [\n    2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,\n];\n\nasync function te() {\n    for (let index = 0; index \u003c arr.length; index++) {\n        const element = arr[index];\n        await new Promise((resolve) =\u003e setTimeout(resolve, 1000));\n        arrState.update((s) =\u003e {\n            s.push(element);\n        });\n    }\n}\n\nte().then(() =\u003e {\n    console.log(\n        \"all transactions: \",\n        arrState.DEBUG.transactionService.getAllTransactions(),\n    );\n\n    // --\u003e\n    // diff:  null\n    // diff:  { old: [ 0, 1 ], new: [ 0, 1, 2 ] }\n    // diff:  { old: [ 0, 1, 2 ], new: [ 0, 1, 2, 3 ] }\n    // diff:  { old: [ 0, 1, 2, 3 ], new: [ 0, 1, 2, 3, 4 ] }\n    // diff:  { old: [ 0, 1, 2, 3, 4 ], new: [ 0, 1, 2, 3, 4, 5 ] }\n    // ...\n    // all transactions:  [\n    // {\n    //     number: 11,\n    //     snapshot: [\n    //     0,  1, 2, 3, 4,\n    //     5,  6, 7, 8, 9,\n    //     10, 11\n    //     ],\n    //     timestamp: 1702588027170\n    // },\n    // {\n    //     number: 12,\n    //     snapshot: [\n    //     0, 1, 2, 3,  4,  5,\n    //     6, 7, 8, 9, 10, 11,\n    //     12\n    //     ],\n    //     timestamp: 1702588028173\n    // },\n    // {\n    //     number: 13,\n    //     snapshot: [\n    //     0,  1, 2, 3,  4,  5,\n    //     6,  7, 8, 9, 10, 11,\n    //     12, 13\n    //     ],\n    //     timestamp: 1702588029175\n    // },\n    // {\n    //     number: 14,\n    //     snapshot: [\n    //     0,  1,  2, 3,  4,  5,\n    //     6,  7,  8, 9, 10, 11,\n    //     12, 13, 14\n    //     ],\n    //     timestamp: 1702588030176\n    // },\n    // {\n    //     number: 15,\n    //     snapshot: [\n    //     0,  1,  2,  3,  4,  5,\n    //     6,  7,  8,  9, 10, 11,\n    //     12, 13, 14, 15\n    //     ],\n    //     timestamp: 1702588031179\n    // },\n    // {\n    //     number: 16,\n    //     snapshot: [\n    //     0,  1,  2,  3,  4,  5,  6,\n    //     7,  8,  9, 10, 11, 12, 13,\n    //     14, 15, 16\n    //     ],\n    //     timestamp: 1702588032180\n    // },\n    // {\n    //     number: 17,\n    //     snapshot: [\n    //     0,  1,  2,  3,  4,  5,  6,\n    //     7,  8,  9, 10, 11, 12, 13,\n    //     14, 15, 16, 17\n    //     ],\n    //     timestamp: 1702588033183\n    // },\n    // {\n    //     number: 18,\n    //     snapshot: [\n    //     0,  1,  2,  3,  4,  5,  6,\n    //     7,  8,  9, 10, 11, 12, 13,\n    //     14, 15, 16, 17, 18\n    //     ],\n    //     timestamp: 1702588034187\n    // },\n    // {\n    //     number: 19,\n    //     snapshot: [\n    //     0,  1,  2,  3,  4,  5,  6,\n    //     7,  8,  9, 10, 11, 12, 13,\n    //     14, 15, 16, 17, 18, 19\n    //     ],\n    //     timestamp: 1702588035189\n    // },\n    // {\n    //     number: 20,\n    //     snapshot: [\n    //     0,  1,  2,  3,  4,  5,  6,\n    //     7,  8,  9, 10, 11, 12, 13,\n    //     14, 15, 16, 17, 18, 19, 20\n    //     ],\n    //     timestamp: 1702588036193\n    // }\n    // ]\n});\n```\n\n## Custom Comparators\n\n`Statemanjs` allows you to define custom comparator functions that determine how the state should be compared before it is updated. This feature is particularly useful when you need more control over the conditions under which the state is considered \"changed.\"\n\n### Using Custom Comparators\n\nWhen creating a state object with `createState` or a computed state with `createComputedState`, you can provide a `customComparator` as part of the `StatemanjsServiceOptions`. This custom comparator will be used if you set the `defaultComparator` to `\"custom\"`.\n\n#### Example\n\n```ts\nimport { createState } from \"@persevie/statemanjs\";\nimport _ from \"lodash\";\n\nconst state = createState(\n    { name: \"Finn\", age: 13 },\n    {\n        defaultComparator: \"custom\",\n        customComparator: (a, b) =\u003e _.isEqual(a, b),\n    },\n);\n\nstate.update((currentState) =\u003e {\n    currentState.age = 14;\n});\n```\n\nIn this example, the `_.isEqual` function from lodash is used to perform deep equality checks on the state. The state will only be updated if the custom comparator determines that the new state is different from the current state.\n\nOverriding Comparators in set and update\nYou can override the global comparator behavior in individual set or update operations by using the SetOptions and UpdateOptions respectively. This allows you to temporarily use a different comparator or skip comparison entirely for a specific operation.\n\n**Options:**\n\n- `skipComparison`: If set to true, the state will be updated without any comparison.\n- `comparatorOverride`: Overrides the global `defaultComparator` for this operation. You can use \"none\", \"ref\", \"shallow\", or \"custom\".\n- `customComparatorOverride`: Provides a custom comparator to be used for this operation, but it only applies if comparatorOverride or the global\n- `defaultComparator` is set to \"custom\".\n\n#### Example\n\n```ts\nstate.update(\n    (currentState) =\u003e {\n        currentState.age = 15;\n    },\n    {\n        comparatorOverride: \"custom\",\n        customComparatorOverride: (a, b) =\u003e a.age === b.age,\n    },\n);\n```\n\nIn this example, the state will only update if the age property is different, as defined by the `customComparatorOverride`. This comparator override is only effective because comparatorOverride is explicitly set to \"custom\".\n\nHere are the available defaultComparator options:\n\n- \"none\": The state will be modified without any comparison.\n- \"ref\": The state will be modified if the new state is a different reference from the current state.\n- \"shallow\": The state will be modified based on a shallow comparison, where only the first level of properties is compared.\n\nBy default, Statemanjs will use \"ref\" if no defaultComparator is specified.\n\n#### Example Usage of Default Comparators\n\n```ts\nconst state = createState(\n    { name: \"Jake\", age: 28 },\n    {\n        defaultComparator: \"shallow\",\n    },\n);\n\nstate.set({ name: \"Jake\", age: 29 }); // Will update because age is different\n```\n\nIn this example, shallow comparison is used, meaning the state will only update if any of the top-level properties have changed.\n\nThis flexibility allows you to optimize performance and control how your application responds to state changes.\n\n# Performance\n\nStatemanjs delivers production-grade performance across diverse workloads—from microsecond single-item updates to million-record bulk operations.\n\n## Standard Operations Benchmark\n\nBenchmark configuration: `benchmark/bench.ts` (Bun 1.1, 1000 iterations, 100 warmup runs). Results show average time per operation (ms) and throughput (ops/s). All runs pass state validation.\n\n| Operation                  | Statemanjs (ms) | Statemanjs (ops/s) | Effector (ms) | Effector (ops/s) | MobX (ms) | MobX (ops/s) | Redux (ms) | Redux (ops/s) |\n| -------------------------- | --------------: | -----------------: | ------------: | ---------------: | --------: | -----------: | ---------: | ------------: |\n| Add Single Todo            |          0.0072 |            139,205 |        0.0139 |           71,812 |    0.0170 |       58,659 |     0.0207 |        48,402 |\n| Add 100 Todos              |          0.0461 |             21,698 |        0.0753 |           13,278 |    0.8360 |        1,196 |     4.8851 |           205 |\n| Complete Single Todo       |          0.0045 |            221,901 |        0.0088 |          113,206 |    0.0110 |       90,584 |     0.0169 |        59,184 |\n| Toggle Single Todo         |          0.0054 |            183,786 |        0.0060 |          167,440 |    0.0126 |       79,453 |     0.0166 |        60,302 |\n| Delete Single Todo         |          0.0044 |            228,658 |        0.0078 |          128,894 |    0.0131 |       76,415 |     0.0191 |        52,437 |\n| Change Filter              |          0.0033 |            302,188 |        0.0077 |          130,237 |    0.0142 |       70,455 |     0.0117 |        85,355 |\n| Batch: Add+Complete+Delete |          0.0088 |            113,973 |        0.0119 |           84,330 |    0.0156 |       64,147 |     0.0168 |        59,674 |\n| Update with 10 Subscribers |          0.0082 |            122,630 |        0.0128 |           77,964 |    0.0302 |       33,119 |     0.0187 |        53,487 |\n| Deep State Modification    |          0.0078 |            128,749 |        0.0111 |           89,868 |    0.1746 |        5,728 |     0.1184 |         8,445 |\n\n**Key takeaways:**\n\n- **Burst operations:** 2–20× faster than alternatives across bulk inserts, deep updates, and multi-subscriber scenarios.\n- **Consistent throughput:** Every operation stays in the 100k+ ops/s range except bulk batching, which remains competitive at 21k ops/s.\n- **Deep mutations:** In-place structural sharing (`update(draft =\u003e ...)`) outperforms immutable cloning by an order of magnitude.\n\n## Extreme Scale: Million-Record Stress Test\n\nConfiguration: `benchmark/todo-benchmark-results-2025-10-28T13-34-52-165Z.json` (10 iterations, 10 warmup, 1,000,000 items per batch).\n\n| Operation                  | Avg Time (ms) | Ops/Second | State Valid |\n| -------------------------- | ------------: | ---------: | :---------: |\n| Add Single Todo            |         0.025 |     40,174 |     ✅      |\n| Add 1,000,000 Todos        |       506.809 |      1.973 |     ✅      |\n| Complete Single Todo       |         0.015 |     67,058 |     ✅      |\n| Toggle Single Todo         |         0.013 |     79,365 |     ✅      |\n| Delete Single Todo         |         0.010 |    100,125 |     ✅      |\n| Change Filter              |         0.007 |    139,698 |     ✅      |\n| Batch: Add+Complete+Delete |         0.017 |     59,895 |     ✅      |\n| Update with 10 Subscribers |         0.020 |     50,977 |     ✅      |\n| Deep State Modification    |         0.039 |     25,532 |     ✅      |\n\n**What this means:**\n\n- **Linear scaling confirmed:** Single-item operations remain in the 0.01–0.04 ms range even with a million-element array in memory.\n- **No catastrophic degradation:** Bulk inserting 1,000,000 items takes ~507 ms (effectively ~2 million array operations per second), and the resulting state passes validation.\n- **Real-world viability:** Large datasets like financial dashboards, log viewers, or analytics tables stay responsive without virtualization hacks at the store layer.\n\n## Legacy Fill Benchmark\n\nHistorical performance data for incremental array filling (1–50 million elements). Time in milliseconds; ❌ indicates timeout (\u003e6h) or crash.\n\n|      Items | Effector (ms) | MobX (ms) | Redux (ms) | Statemanjs (ms) |\n| ---------: | ------------: | --------: | ---------: | --------------: |\n|          1 |         0.011 |     0.020 |      0.004 |           0.002 |\n|         10 |         0.046 |     0.110 |      0.014 |           0.010 |\n|        100 |         0.178 |     0.435 |      0.083 |           0.062 |\n|      1,000 |         1.209 |     2.587 |      0.875 |           0.242 |\n|     10,000 |        58.333 |    31.700 |     52.266 |           2.223 |\n|    100,000 |    13,849.532 |   322.186 | 12,867.839 |          27.506 |\n|  1,000,000 |  2,448,118.75 | 4,473.259 |  2,354,867 |         279.839 |\n|  2,000,000 |            ❌ | 9,588.995 |         ❌ |         605.374 |\n|  5,000,000 |            ❌ |        ❌ |         ❌ |       1,468.102 |\n| 10,000,000 |            ❌ |        ❌ |         ❌ |       3,185.279 |\n| 50,000,000 |            ❌ |        ❌ |         ❌ |      14,499.884 |\n\nStatemanjs is the only library to complete all test cases without timeout or memory exhaustion, demonstrating true production readiness for data-intensive applications.\n\n## Running Benchmarks\n\n```bash\ncd benchmark\nbun bench.ts  # or: pnpm run bench\n```\n\nAll benchmark implementations use official patterns from each library's documentation. Source code and methodology details are in `benchmark/README.md`.\n\n# Integrations\n\nStatemanjs is framework agnostic and can be used without additional packages. But for convenience, there are packages for the most popular frameworks - [react](https://github.com/persevie/statemanjs/blob/main/src/statemanjs-react/README.md), [vue](https://github.com/persevie/statemanjs/blob/main/src/statemanjs-vue/README.md), [solid](https://github.com/persevie/statemanjs/blob/main/src/statemanjs-solid/README.md). Statemanjs supports svelte out of the box and doesn't need any additional packages.\nTo work with additional packages, the main statemanjs package is required.\n\n# For contributors\n\nSee [CONTRIBUTING.md](https://github.com/persevie/statemanjs/blob/main/CONTRIBUTING.md).\n\n```\n\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpersevie%2Fstatemanjs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpersevie%2Fstatemanjs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpersevie%2Fstatemanjs/lists"}