@wildgrowth/voltix 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ First release of `@wildgrowth/voltix`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WILDGROWTH LTD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ <div align="center">
2
+
3
+ # ⚡ Voltix
4
+
5
+ **Fastest React state library.**
6
+
7
+ [![CI](https://github.com/wildgrowth-oss/voltix/actions/workflows/ci.yml/badge.svg)](https://github.com/wildgrowth-oss/voltix/actions/workflows/ci.yml)
8
+ [![npm](https://img.shields.io/npm/v/@wildgrowth/voltix?color=8b5cf6)](https://www.npmjs.com/package/@wildgrowth/voltix)
9
+ [![gzip](https://img.shields.io/badge/gzip-5.0%20KB-06b6d4)](https://bundlephobia.com/package/@wildgrowth/voltix)
10
+ [![types](https://img.shields.io/npm/types/@wildgrowth/voltix?color=3178c6)](https://www.npmjs.com/package/@wildgrowth/voltix)
11
+ [![license](https://img.shields.io/badge/license-MIT-64748b)](./LICENSE)
12
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-22d3ee)](https://github.com/wildgrowth-oss/voltix/pulls)
13
+
14
+ [Website](https://voltix.pages.dev) · [npm](https://www.npmjs.com/package/@wildgrowth/voltix) · [GitHub](https://github.com/wildgrowth-oss/voltix) · [Docs](./docs) · [Benchmarks](#benchmarks)
15
+
16
+ </div>
17
+
18
+ Voltix is a **~5.0 KB** global store for React with per-key subscriptions: set one field and only the components reading that field re-render. There is no provider to set up and there are no selector functions to write. Stores compose into a tree of nested stores and per-id lookups. Full TypeScript inference, zero dependencies, and the fastest update throughput of any React store we've measured.
19
+
20
+ ```tsx
21
+ import { createStore, useStoreKey } from '@wildgrowth/voltix';
22
+
23
+ const store = createStore({ count: 0 });
24
+
25
+ function Counter() {
26
+ const count = useStoreKey(store, 'count');
27
+ return <button onClick={() => store.increment('count')}>{count}</button>;
28
+ }
29
+ ```
30
+
31
+ ## Why Voltix
32
+
33
+ - **Surgical re-renders.** Subscriptions are per key. Updating `x` never touches a component reading `y`, even when they share a store.
34
+ - **Direct values.** `useStoreKey(store, 'count')` returns the value itself.
35
+ - **Composes into a tree.** A key can hold another store or a lookup (a store per id); step into children with `select(key)`, into lookups with `at(id)`. Composition adds no cost to reads or writes.
36
+ - **Identity-stable state.** `get()` returns the same object across writes, and the update path is the fastest of the group (see below).
37
+ - **Fully typed, inference-first.** Keys, values, selectors, and equality functions are all inferred from your store. Selecting a key that doesn't exist is a compile error.
38
+ - **Runtime contracts.** Put a zod (or valibot/arktype) schema on a key. Invalid writes are rejected, initial state included, and `onSchemaError` routes failures into your UI. Standard Schema keeps it dependency-free.
39
+ - **Batteries included.** Action helpers, derived keys, write interceptors, persistence (sync + async), and an ESLint rule ship in the box.
40
+ - **Tiny and dependency-free.** ~5.0 KB gzipped, tree-shakeable, `react` as the only peer.
41
+
42
+ ## Benchmarks
43
+
44
+ Update throughput vs the latest Preact Signals, Zustand, nanostores, Solid, Valtio, Jotai and Legend-State (vanilla store engines, ops/sec, higher is faster). Each case runs in its own process and reports the median of 40 runs:
45
+
46
+ | operation | **Voltix** | Preact Signals | Zustand | nanostores | Solid | Valtio | Jotai | Legend-State |
47
+ |---|---|---|---|---|---|---|---|---|
48
+ | update a value + notify | **187.8M** | 47.4M | 39.1M | 31.0M | 13.1M | 6.7M | 2.7M | 2.7M |
49
+ | functional update (`x => x + 1`) | **192.0M** | 49.4M | 37.8M | 30.3M | 13.7M | 6.5M | 2.3M | 2.7M |
50
+ | fine-grained update (1 of 1000) | **184.2M** | 47.9M | 39.1M | 30.4M | 12.5M | 161K | 2.6M | 1.1M |
51
+ | notify 1000 subscribers | **563K** | 35K | 232K | 111K | 24K | — | 64K | 35K |
52
+
53
+ Run `npm run bench` to reproduce ([`bench/vs.ts`](bench/vs.ts); Node 25, Apple M4 Pro). The gap matters under high write rates: drag interactions, streaming data, animation, and large grids of independently updating cells.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ npm i @wildgrowth/voltix
59
+ ```
60
+
61
+ ## The idea: fine-grained by key
62
+
63
+ You subscribe to **keys**. A write to a key notifies exactly the components reading it, and leaves the rest untouched.
64
+
65
+ ```tsx
66
+ const store = createStore({ user: { name: 'Ada' }, theme: 'dark', unread: 3 });
67
+
68
+ function Unread() {
69
+ const unread = useStoreKey(store, 'unread'); // re-renders only when `unread` changes
70
+ return <span>{unread}</span>;
71
+ }
72
+
73
+ store.setKey('theme', 'light'); // <Unread /> does not re-render
74
+ store.increment('unread'); // <Unread /> re-renders, nothing else does
75
+ ```
76
+
77
+ Need several keys in one component? `useStoreSelector` subscribes to each and returns a slice:
78
+
79
+ ```tsx
80
+ const { name, theme } = useStoreSelector(store, ['user', 'theme']);
81
+ ```
82
+
83
+ ## Composition: nesting and lookups
84
+
85
+ A key holding another store becomes a child, reached with `select(key)`. `createLookup(factory)` makes a **lookup**: a store per id, created on first use. Updating one store re-renders only its readers. The factory can return a fully built store (schema, actions, `onSchemaError` attached) and the lookup keeps exactly that store.
86
+
87
+ ```tsx
88
+ const app = createStore({
89
+ theme: 'dark',
90
+ profile: createStore({ name: 'Ada', age: 30 }), // nested store
91
+ todos: createLookup((id: string) => ({ text: '', done: false })), // lookup
92
+ });
93
+
94
+ app.setKey('theme', 'light');
95
+ app.select('profile').setKey('age', 31);
96
+ app.select('todos').at('t1').toggle('done');
97
+ app.select('todos').keys(); // ['t1']
98
+ ```
99
+
100
+ `select` takes one child key, `at` takes one id, and they chain for depth. See [Composition](./docs/composition.md).
101
+
102
+ ## Derived state
103
+
104
+ `derive` computes a key from other keys in the same update; subscribers wake only when the result changes:
105
+
106
+ ```tsx
107
+ const store = createStore({ first: 'Ada', last: 'Lovelace', full: '' });
108
+ store.derive('full', ['first', 'last'], ({ first, last }) => `${first} ${last}`);
109
+ ```
110
+
111
+ ## API at a glance
112
+
113
+ | | |
114
+ |---|---|
115
+ | `createStore(shape, options?)` | Create a store |
116
+ | `createLookup(factory, options?)` | A map from id to store, populated lazily |
117
+ | `select(key)` · `at(id)` | Step into a child · step into a lookup by id |
118
+ | `createActions(store, define)` | Define a store's actions in one block, returned as a plain object |
119
+ | `options.schema` | Per-key runtime contracts (zod, valibot, arktype) |
120
+ | `onSchemaError(handler)` | React to a rejected write, with access to your actions |
121
+ | `useStoreKey(store, key)` | Subscribe to one key, get the value directly |
122
+ | `useStoreSelector(store, [keys])` | Subscribe to several keys, get a slice |
123
+ | `createStoreHook(store)` | Pre-bind a typed hook to a store |
124
+ | `set` · `setKey` · `update` · `increment` · `toggle` | Write helpers |
125
+ | `mergeSet` · `batch` · `reset` · `derive` · `pick` | Object updates, batching, reset, computed keys, snapshots |
126
+ | `subscribe` · `onChange` · `intercept` | Outside-React reactions and interception |
127
+ | `options.equals` | Per-key custom equality (compare by `.id`, etc.) |
128
+
129
+ Full reference in the [docs](./docs).
130
+
131
+ ## ESLint rule
132
+
133
+ The package includes an ESLint rule (at `@wildgrowth/voltix/eslint`) that flags selector keys you never use. Requires ESLint 9+ (flat config):
134
+
135
+ ```js
136
+ // eslint.config.js
137
+ import voltix from '@wildgrowth/voltix/eslint';
138
+
139
+ export default [{
140
+ plugins: { voltix },
141
+ rules: { 'voltix/no-unused-selector-keys': 'warn' },
142
+ }];
143
+ ```
144
+
145
+ ## Documentation
146
+
147
+ - [Getting started](./docs/getting-started.md)
148
+ - [Walkthrough](./docs/walkthrough.md) — one app, from a single store to a composed tree
149
+ - [Core concepts](./docs/core-concepts.md)
150
+ - [Composition](./docs/composition.md) — nesting stores and per-id lookups
151
+ - [API reference](./docs/api-reference.md)
152
+ - [Guides](./docs/guides.md) — actions, contracts, derived state, middleware, persistence
153
+ - [Patterns](./docs/patterns.md) — sending requests, polled live updates, live tables
154
+ - [Comparison](./docs/comparison.md)
155
+
156
+ ## License
157
+
158
+ MIT © WILDGROWTH LTD
@@ -0,0 +1 @@
1
+ var f=Object.defineProperty,g=Object.defineProperties;var h=Object.getOwnPropertyDescriptors;var d=Object.getOwnPropertySymbols;var i=Object.prototype.hasOwnProperty,j=Object.prototype.propertyIsEnumerable;var e=(c,a,b)=>a in c?f(c,a,{enumerable:!0,configurable:!0,writable:!0,value:b}):c[a]=b,k=(c,a)=>{for(var b in a||(a={}))i.call(a,b)&&e(c,b,a[b]);if(d)for(var b of d(a))j.call(a,b)&&e(c,b,a[b]);return c},l=(c,a)=>g(c,h(a));export{k as a,l as b};
@@ -0,0 +1,29 @@
1
+ import { Rule } from 'eslint';
2
+
3
+ /**
4
+ * ESLint rule: warn when a selector includes keys that are never destructured.
5
+ *
6
+ * Zero-config. Two detection paths:
7
+ * - `useStoreSelector(store, [...])` — recognized by name, checked strictly.
8
+ * - Any other `use*`-named hook called with a selector-style array (string keys /
9
+ * `{ key: compareFn }` entries) whose result is destructured. To leave foreign
10
+ * hooks alone, these only warn when at least one selected key IS destructured —
11
+ * the overlap is the proof it's a selector call.
12
+ *
13
+ * Warnings require provability: the call's result must be destructured inline.
14
+ * A `...rest` element only counts as consuming the remaining keys when the rest
15
+ * variable is actually read somewhere; an unused rest is ignored. Anything short
16
+ * of proof is skipped rather than guessed at.
17
+ */
18
+ declare const noUnusedSelectorKeys: Rule.RuleModule;
19
+ declare const plugin: {
20
+ meta: {
21
+ name: string;
22
+ };
23
+ rules: {
24
+ 'no-unused-selector-keys': Rule.RuleModule;
25
+ };
26
+ configs: Record<string, unknown>;
27
+ };
28
+
29
+ export { plugin as default, noUnusedSelectorKeys };
@@ -0,0 +1,29 @@
1
+ import { Rule } from 'eslint';
2
+
3
+ /**
4
+ * ESLint rule: warn when a selector includes keys that are never destructured.
5
+ *
6
+ * Zero-config. Two detection paths:
7
+ * - `useStoreSelector(store, [...])` — recognized by name, checked strictly.
8
+ * - Any other `use*`-named hook called with a selector-style array (string keys /
9
+ * `{ key: compareFn }` entries) whose result is destructured. To leave foreign
10
+ * hooks alone, these only warn when at least one selected key IS destructured —
11
+ * the overlap is the proof it's a selector call.
12
+ *
13
+ * Warnings require provability: the call's result must be destructured inline.
14
+ * A `...rest` element only counts as consuming the remaining keys when the rest
15
+ * variable is actually read somewhere; an unused rest is ignored. Anything short
16
+ * of proof is skipped rather than guessed at.
17
+ */
18
+ declare const noUnusedSelectorKeys: Rule.RuleModule;
19
+ declare const plugin: {
20
+ meta: {
21
+ name: string;
22
+ };
23
+ rules: {
24
+ 'no-unused-selector-keys': Rule.RuleModule;
25
+ };
26
+ configs: Record<string, unknown>;
27
+ };
28
+
29
+ export { plugin as default, noUnusedSelectorKeys };
package/dist/eslint.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var d=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var h=Object.prototype.hasOwnProperty;var b=(a,s)=>{for(var u in s)d(a,u,{get:s[u],enumerable:!0})},x=(a,s,u,c)=>{if(s&&typeof s=="object"||typeof s=="function")for(let t of v(s))!h.call(a,t)&&t!==u&&d(a,t,{get:()=>s[t],enumerable:!(c=k(s,t))||c.enumerable});return a};var R=a=>x(d({},"__esModule",{value:!0}),a);var S={};b(S,{default:()=>I,noUnusedSelectorKeys:()=>g});module.exports=R(S);var g={meta:{type:"problem",docs:{description:"Warn when a selector includes keys that are never destructured or used.",recommended:!0},schema:[],messages:{unusedKey:"'{{key}}' is selected but never destructured. Remove it from the selector, or destructure and use it."}},create(a){function s(t){let o=[];for(let e of t.elements)if(e){if(e.type==="Literal"&&typeof e.value=="string")o.push({node:e,name:e.value});else if(e.type==="ObjectExpression")for(let n of e.properties){if(n.type!=="Property"||n.computed)continue;let i=n.key.type==="Identifier"?n.key.name:n.key.type==="Literal"?String(n.key.value):null;i&&o.push({node:n.key,name:i})}}return o}function u(t,o){if(o.argument.type!=="Identifier")return!0;let e=o.argument.name,n=a.sourceCode.getDeclaredVariables(t).find(i=>i.name===e);return n?n.references.some(i=>i.isRead()):!0}function c(t,o,e){let n=t.parent;if(!n||n.type!=="VariableDeclarator"||n.id.type!=="ObjectPattern")return;let i=n.id,f=i.properties.find(r=>r.type==="RestElement");if(f&&u(n,f))return;let y=s(o);if(y.length===0)return;let l=new Set;for(let r of i.properties)r.type!=="Property"||r.computed||(r.key.type==="Identifier"?l.add(r.key.name):r.key.type==="Literal"&&l.add(String(r.key.value)));if(!(e&&!y.some(({name:r})=>l.has(r))))for(let{node:r,name:m}of y)l.has(m)||a.report({node:r,messageId:"unusedKey",data:{key:m}})}return{CallExpression(t){if(t.callee.type!=="Identifier")return;let o=t.callee.name;if(o==="useStoreSelector"){let e=t.arguments[1];(e==null?void 0:e.type)==="ArrayExpression"&&c(t,e,!1);return}if(/^use[A-Z0-9]/.test(o)){let e=t.arguments[0];(e==null?void 0:e.type)==="ArrayExpression"&&c(t,e,!0)}}}}},p={meta:{name:"voltix"},rules:{"no-unused-selector-keys":g},configs:{}};p.configs.recommended={plugins:{voltix:p},rules:{"voltix/no-unused-selector-keys":"warn"}};var I=p;0&&(module.exports={noUnusedSelectorKeys});
@@ -0,0 +1 @@
1
+ import"./chunk-HGGX55FR.mjs";var m={meta:{type:"problem",docs:{description:"Warn when a selector includes keys that are never destructured or used.",recommended:!0},schema:[],messages:{unusedKey:"'{{key}}' is selected but never destructured. Remove it from the selector, or destructure and use it."}},create(c){function p(n){let s=[];for(let e of n.elements)if(e){if(e.type==="Literal"&&typeof e.value=="string")s.push({node:e,name:e.value});else if(e.type==="ObjectExpression")for(let t of e.properties){if(t.type!=="Property"||t.computed)continue;let o=t.key.type==="Identifier"?t.key.name:t.key.type==="Literal"?String(t.key.value):null;o&&s.push({node:t.key,name:o})}}return s}function f(n,s){if(s.argument.type!=="Identifier")return!0;let e=s.argument.name,t=c.sourceCode.getDeclaredVariables(n).find(o=>o.name===e);return t?t.references.some(o=>o.isRead()):!0}function l(n,s,e){let t=n.parent;if(!t||t.type!=="VariableDeclarator"||t.id.type!=="ObjectPattern")return;let o=t.id,y=o.properties.find(r=>r.type==="RestElement");if(y&&f(t,y))return;let i=p(s);if(i.length===0)return;let a=new Set;for(let r of o.properties)r.type!=="Property"||r.computed||(r.key.type==="Identifier"?a.add(r.key.name):r.key.type==="Literal"&&a.add(String(r.key.value)));if(!(e&&!i.some(({name:r})=>a.has(r))))for(let{node:r,name:d}of i)a.has(d)||c.report({node:r,messageId:"unusedKey",data:{key:d}})}return{CallExpression(n){if(n.callee.type!=="Identifier")return;let s=n.callee.name;if(s==="useStoreSelector"){let e=n.arguments[1];(e==null?void 0:e.type)==="ArrayExpression"&&l(n,e,!1);return}if(/^use[A-Z0-9]/.test(s)){let e=n.arguments[0];(e==null?void 0:e.type)==="ArrayExpression"&&l(n,e,!0)}}}}},u={meta:{name:"voltix"},rules:{"no-unused-selector-keys":m},configs:{}};u.configs.recommended={plugins:{voltix:u},rules:{"voltix/no-unused-selector-keys":"warn"}};var g=u;export{g as default,m as noUnusedSelectorKeys};
@@ -0,0 +1,373 @@
1
+ /**
2
+ * A write interceptor. Runs before an update commits: return a partial to replace
3
+ * the update, `false` to veto it, or nothing to pass it through unchanged.
4
+ */
5
+ type Interceptor<T extends object> = (update: Partial<T>, state: T) => Partial<T> | false | void;
6
+ /** One validation failure reported by a Standard Schema. */
7
+ interface StandardSchemaIssue {
8
+ readonly message: string;
9
+ readonly path?: ReadonlyArray<PropertyKey | {
10
+ readonly key: PropertyKey;
11
+ }> | undefined;
12
+ }
13
+ /** A successful Standard Schema validation: the (possibly transformed) value to use. */
14
+ interface StandardSchemaSuccess<Output> {
15
+ readonly value: Output;
16
+ readonly issues?: undefined;
17
+ }
18
+ /** A failed Standard Schema validation. */
19
+ interface StandardSchemaFailure {
20
+ readonly issues: ReadonlyArray<StandardSchemaIssue>;
21
+ }
22
+ /**
23
+ * The Standard Schema interface (standardschema.dev) — the common contract implemented
24
+ * by zod (>=3.24), valibot (>=1.0), arktype (>=2.0), and others. Vendored as types only,
25
+ * so any conforming library plugs into {@link StoreOptions.schema} with no dependency here.
26
+ */
27
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
28
+ readonly '~standard': {
29
+ readonly version: 1;
30
+ readonly vendor: string;
31
+ readonly validate: (value: unknown) => StandardSchemaSuccess<Output> | StandardSchemaFailure | Promise<unknown>;
32
+ readonly types?: {
33
+ readonly input: Input;
34
+ readonly output: Output;
35
+ } | undefined;
36
+ };
37
+ }
38
+ /** A rejected write, handed to handlers registered with {@link StoreType.onSchemaError}. */
39
+ interface SchemaError<T extends object = any> {
40
+ /** The key whose write was vetoed. */
41
+ readonly key: keyof T;
42
+ /** What the schema objected to. */
43
+ readonly issues: ReadonlyArray<StandardSchemaIssue>;
44
+ /** The value that was rejected. */
45
+ readonly value: unknown;
46
+ /** The node that rejected it — for a lookup, the store at that id, not its parent. */
47
+ readonly store: StoreType<T>;
48
+ }
49
+ /** Options for {@link createStore}. */
50
+ interface StoreOptions<T extends object> {
51
+ /**
52
+ * Per-key equality, fixed at creation. When a write would change a key, its
53
+ * function runs with (prev, next); returning `true` treats the values as equal
54
+ * and skips the update for that key.
55
+ */
56
+ equals?: {
57
+ [K in keyof T]?: (prev: T[K], next: T[K]) => boolean;
58
+ };
59
+ /**
60
+ * Per-key write contracts, as Standard Schemas (zod, valibot, arktype, …). A write
61
+ * to a key runs its schema first: issues veto that key's write (with readable
62
+ * errors in dev), and a transforming schema commits its parsed output. A schema's
63
+ * output type must match the key's type. Keys without a schema are untouched;
64
+ * stores without schemas skip validation entirely. Schemas are locked at creation.
65
+ */
66
+ schema?: {
67
+ [K in keyof T]?: StandardSchemaV1<any, T[K]>;
68
+ };
69
+ }
70
+ /** Keys of `T` whose value is a `number`. */
71
+ type NumericKeys<T> = {
72
+ [K in keyof T]: T[K] extends number ? K : never;
73
+ }[keyof T];
74
+ /** Keys of `T` whose value is a `boolean`. */
75
+ type BooleanKeys<T> = {
76
+ [K in keyof T]: T[K] extends boolean ? K : never;
77
+ }[keyof T];
78
+ /**
79
+ * A reactive store. The value returned by {@link createStore}. Hold it as
80
+ * a field to compose stores (`{ store: StoreType<CartState> }`), or pass it to
81
+ * {@link useStoreSelector} / {@link createStoreHook} to subscribe in React.
82
+ */
83
+ interface StoreType<T extends object> {
84
+ /** Read the whole state (identity-stable until a `set` changes a key). */
85
+ get(): T;
86
+ /** Read one key. */
87
+ getKey<K extends keyof T>(key: K): T[K];
88
+ /** Shallow-merge a partial into state; notifies only the keys that changed. */
89
+ set(update: Partial<T>): void;
90
+ /** Set one key. */
91
+ setKey<K extends keyof T>(key: K, value: T[K]): void;
92
+ /** Set one key from its current value: `setKey(key, updater(prev))`. */
93
+ update<K extends keyof T>(key: K, updater: (prev: T[K]) => T[K]): void;
94
+ /** Add `amount` (default 1) to a numeric key. */
95
+ increment(key: NumericKeys<T>, amount?: number): void;
96
+ /** Flip a boolean key. */
97
+ toggle(key: BooleanKeys<T>): void;
98
+ /** Merge a partial into an object key and write the result. */
99
+ mergeSet<K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : never): void;
100
+ /**
101
+ * Reset all keys (or the given keys) to the initial state. Stores without child
102
+ * nodes or schemas read from the original input object, which should not be mutated.
103
+ */
104
+ reset(keys?: (keyof T)[]): void;
105
+ /** Coalesce many sets inside `fn` into a single notification per subscription. */
106
+ batch(fn: () => void): void;
107
+ /** Subscribe to specific keys. Each call is independent, even with the same listener. Returns an unsubscribe. */
108
+ subscribe(keys: (keyof T)[], listener: () => void): () => void;
109
+ /** Imperative snapshot of specific keys. */
110
+ pick<K extends keyof T>(keys: K[]): Pick<T, K>;
111
+ /**
112
+ * Intercept writes before they commit. `fn` returns a partial to replace the
113
+ * update, `false` to veto it, or nothing to pass through. With `keys`, it runs
114
+ * only when the update touches one of them. Returns an unsubscribe.
115
+ */
116
+ intercept(fn: Interceptor<T>, keys?: (keyof T)[]): () => void;
117
+ /** React to key changes outside React, with the new and previous values. */
118
+ onChange<K extends keyof T>(keys: K[], callback: (values: Pick<T, K>, prev: Pick<T, K>) => void): () => void;
119
+ /**
120
+ * Derive `outKey` from `inputs`: `compute` runs inside any `set` that changes an
121
+ * input and commits its result in the same update, so subscribers to `outKey`
122
+ * wake only when the derived value changes. Returns a disposer.
123
+ */
124
+ derive<K extends keyof T, I extends keyof T>(outKey: K, inputs: I[], compute: (values: Pick<T, I>) => T[K]): () => void;
125
+ /**
126
+ * React to a write rejected by a key's schema. Registered after construction, so the
127
+ * handler can reach the finished store and its actions. Returns an unsubscribe.
128
+ * Handlers replace the default console reporting; the write stays vetoed either way.
129
+ */
130
+ onSchemaError(handler: (error: SchemaError<T>) => void): () => void;
131
+ }
132
+ /**
133
+ * Brand marking a value as a navigable node — a {@link Store} or a {@link Lookup} — so a
134
+ * parent `createStore` can tell a nested store from ordinary object state.
135
+ */
136
+ declare const NODE: unique symbol;
137
+ /** Type-level brand: any value that is a store or a lookup. */
138
+ type NodeBrand = {
139
+ readonly [NODE]: 'store' | 'lookup';
140
+ };
141
+ /** Keys of `T` whose value is a nested store or lookup. */
142
+ type ChildKey<T> = {
143
+ [K in keyof T]-?: NonNullable<T[K]> extends NodeBrand ? K : never;
144
+ }[keyof T];
145
+ /** Keys of `T` that hold ordinary reactive state. */
146
+ type LeafKey<T> = Exclude<keyof T, ChildKey<T>>;
147
+ /** The flat reactive state of a composed store (its non-node keys). */
148
+ type LeafState<T> = {
149
+ [K in LeafKey<T>]: T[K];
150
+ };
151
+ /**
152
+ * A store that may hold other nodes. Ordinary keys are reactive leaves driven by the fast
153
+ * mutate-in-place engine; a key whose value is another {@link Store} or {@link Lookup} is a
154
+ * child, reached with {@link Store.select}. Build one with {@link createStore}.
155
+ */
156
+ interface Store<T extends object> extends StoreType<LeafState<T>> {
157
+ readonly [NODE]: 'store';
158
+ /** One structural step: the child node bound at `key` (a nested store or lookup). */
159
+ select<K extends ChildKey<T>>(key: K): T[K];
160
+ /** Names of this store's child keys. */
161
+ children(): ChildKey<T>[];
162
+ }
163
+ /**
164
+ * A map from id to store, populated lazily: the first {@link Lookup.at} for an id builds the
165
+ * store via the factory passed to {@link createLookup} and memoizes it. `S` is that store's
166
+ * type — exactly what the factory returns when it builds the store itself, or `Store<C>`
167
+ * when it returns a plain state shape.
168
+ */
169
+ interface Lookup<Id extends PropertyKey, S extends object> {
170
+ readonly [NODE]: 'lookup';
171
+ /** The store at `id`, created on first access and memoized. Takes exactly one id. */
172
+ at(id: Id): S;
173
+ /** Whether a store for `id` currently exists. */
174
+ has(id: Id): boolean;
175
+ /** Drop the store at `id`. Returns whether it existed. */
176
+ remove(id: Id): boolean;
177
+ /** Live ids (insertion order). */
178
+ keys(): Id[];
179
+ /** Number of live stores. */
180
+ size(): number;
181
+ /** Drop every store. */
182
+ clear(): void;
183
+ }
184
+ /**
185
+ * Create a store from a state object. Ordinary keys are fast reactive leaves; a key whose
186
+ * value is another store or {@link Lookup} becomes a child, reached with `.select(key)`.
187
+ * The tree is wired once at creation; leaf reads and writes never traverse it, so update
188
+ * and notify throughput are unchanged.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * const settings = createStore({ theme: 'dark', fontSize: 14 });
193
+ * const workspace = createStore({ name: 'Personal', settings });
194
+ * workspace.setKey('name', 'Team');
195
+ * workspace.select('settings').setKey('fontSize', 16);
196
+ * ```
197
+ */
198
+ declare function createStore<T extends object>(shape: T & {
199
+ readonly [NODE]?: never;
200
+ }, options?: StoreOptions<LeafState<T>>): Store<T>;
201
+ /**
202
+ * Create a {@link Lookup}: a map from id to store, populated lazily by `factory`.
203
+ *
204
+ * - `createLookup((id) => makeDocument(id))` — the factory builds each store itself
205
+ * (schema, actions, error handlers already attached); the lookup memoizes exactly
206
+ * that node.
207
+ * - `createLookup((id) => ({ ... }), options?)` — the factory returns a plain state
208
+ * shape; the lookup builds a store per id, applying `options` to each one.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const documents = createLookup((id: string) => ({ title: 'Untitled', body: '' }));
213
+ * documents.at('readme').setKey('title', 'README');
214
+ * documents.keys(); // ['readme']
215
+ * ```
216
+ */
217
+ declare function createLookup<Id extends PropertyKey, N extends NodeBrand>(factory: (id: Id) => N): Lookup<Id, N>;
218
+ declare function createLookup<Id extends PropertyKey, C extends object>(factory: (id: Id) => C & {
219
+ readonly [NODE]?: never;
220
+ }, options?: StoreOptions<LeafState<C>>): Lookup<Id, Store<C>>;
221
+ /**
222
+ * Define a set of actions for a store (or any node) and get them back as a plain,
223
+ * standalone object — the store itself is not modified. `define` receives the node,
224
+ * so actions close over it with full inference; since it takes any node, actions can
225
+ * reach across a composed tree with `select(key)` / `at(id)`.
226
+ *
227
+ * Actions are ordinary functions: export them beside the store, and combine sets
228
+ * with object spread when you want them split across files.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * const counter = createStore({ count: 0 });
233
+ * const counterActions = createActions(counter, (store) => ({
234
+ * increment: () => store.increment('count'),
235
+ * reset: () => store.reset(['count']),
236
+ * }));
237
+ * counterActions.increment();
238
+ * ```
239
+ */
240
+ declare function createActions<S extends object, A extends object>(store: S, define: (store: S) => A): A;
241
+ type PrimitiveKey<T extends object> = keyof T;
242
+ type CompareFn<V> = (prev: V, next: V) => boolean;
243
+ type KeySelector<T extends object> = PrimitiveKey<T>;
244
+ type CustomSelector<T extends object> = {
245
+ [K in keyof T]?: CompareFn<T[K]>;
246
+ };
247
+ type SelectorInput<T extends object> = ReadonlyArray<KeySelector<T> | CustomSelector<T>>;
248
+ type ExtractSelectorKeys<T extends object, S extends SelectorInput<T>> = {
249
+ [K in S[number] extends infer Item ? Item extends keyof T ? Item : keyof Item : never]: T[K];
250
+ };
251
+ type Picked<T extends object, S extends SelectorInput<T>> = ExtractSelectorKeys<T, S>;
252
+ /**
253
+ * React hook that subscribes to specific keys in a store with fine-grained re-renders.
254
+ * Only re-renders when the selected keys actually change (using Object.is comparison).
255
+ *
256
+ * @param store - The store created with createStore
257
+ * @param selector - Array of keys to subscribe to, or objects with custom compare functions
258
+ * @returns Selected state values from the store
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * // Subscribe to specific keys
263
+ * const { count, name } = useStoreSelector(store, ['count', 'name']);
264
+ *
265
+ * // Custom comparison for complex objects
266
+ * const { tasks } = useStoreSelector(store, [
267
+ * { tasks: (prev, next) => prev.length === next.length }
268
+ * ]);
269
+ * ```
270
+ */
271
+ declare function useStoreSelector<T extends object, S extends SelectorInput<T>>(store: StoreType<T>, selector: S): Picked<T, S>;
272
+ /**
273
+ * React hook that subscribes to a single key and returns its value directly —
274
+ * the lean path for the common one-key case. Re-renders only when that key
275
+ * changes (Object.is, plus any store-level equality from options.equals).
276
+ * No wrapper object is allocated.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * const count = useStoreKey(store, 'count');
281
+ * ```
282
+ */
283
+ declare function useStoreKey<T extends object, K extends keyof T>(store: StoreType<T>, key: K): T[K];
284
+ /**
285
+ * Creates a pre-bound selector hook for a specific store instance.
286
+ * Infers the state type from the store — no manual generics needed.
287
+ *
288
+ * @param store - The store created with createStore
289
+ * @returns A React hook with the same API as useStoreSelector, but with the store already bound
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * const useWorkspace = createStoreHook(workspace);
294
+ *
295
+ * // In a component:
296
+ * const { name, syncing } = useWorkspace(['name', 'syncing']);
297
+ * ```
298
+ */
299
+ declare function createStoreHook<T extends object>(store: StoreType<T>): <S extends SelectorInput<T>>(selector: S) => Picked<T, S>;
300
+ /**
301
+ * Shallow-merge helper: `{ ...base, ...patch }`, typed so the patch can only
302
+ * carry keys of the base. A plain function on plain data — pair it with a write
303
+ * when you want the result stored: `store.setKey('user', merge(user, patch))`,
304
+ * or reach for {@link StoreType.mergeSet} which does both in one call.
305
+ */
306
+ declare function merge<T extends object>(base: T, patch: Partial<T>): T;
307
+ /**
308
+ * Interface for synchronous storage (localStorage, sessionStorage, etc.).
309
+ */
310
+ interface StorageSupportingInterface {
311
+ getItem(key: string): string | null;
312
+ setItem(key: string, value: string): void;
313
+ }
314
+ /**
315
+ * Interface for asynchronous storage (React Native AsyncStorage, etc.).
316
+ */
317
+ interface AsyncStorageSupportingInterface {
318
+ getItem(key: string): Promise<string | null>;
319
+ setItem(key: string, value: string): Promise<void>;
320
+ }
321
+ type AnyStorage = Storage | StorageSupportingInterface | AsyncStorageSupportingInterface;
322
+ /**
323
+ * Serialization used by {@link persist} and {@link loadPersistedState}.
324
+ * Any object with `stringify`/`parse` fits — `JSON` and superjson both do.
325
+ * Use the same parser on both sides.
326
+ */
327
+ interface PersistParser {
328
+ stringify(value: any): string;
329
+ parse(text: string): any;
330
+ }
331
+ /**
332
+ * Persist selected keys of a store: each key is written to storage as it changes,
333
+ * under `${persistKey}:${keyName}`. Works with synchronous storage (localStorage,
334
+ * sessionStorage) and asynchronous storage (React Native AsyncStorage) — async
335
+ * writes are fire-and-forget, so a slow write never blocks the state update.
336
+ * Returns an unsubscribe that stops persisting.
337
+ *
338
+ * Seed the initial state with {@link loadPersistedState}, which reads the same keys.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * const persisted = loadPersistedState<State>(localStorage, 'myapp', ['theme']);
343
+ * const store = createStore<State>({ theme: 'light', ...persisted });
344
+ * persist(store, localStorage, 'myapp', ['theme']);
345
+ * ```
346
+ */
347
+ declare function persist<T extends object>(store: StoreType<T>, storage: AnyStorage, persistKey: string, keys: (keyof T)[], parser?: PersistParser): () => void;
348
+ /**
349
+ * Loads persisted state from individual key storage during store initialization.
350
+ * Reads keys saved by {@link persist} and returns them as partial state.
351
+ *
352
+ * Returns synchronously for sync storage and a Promise for async storage.
353
+ *
354
+ * @param storage - Storage interface to read from (same as passed to persist)
355
+ * @param persistKey - Base key prefix used for storage (same as passed to persist)
356
+ * @param keys - Array of keys to restore (should match the persisted keys)
357
+ * @returns Partial state object (sync) or Promise of partial state (async)
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * // Sync — localStorage
362
+ * const persisted = loadPersistedState(localStorage, 'myapp', ['theme']);
363
+ * const store = createStore({ theme: 'light', ...persisted });
364
+ *
365
+ * // Async — React Native AsyncStorage
366
+ * const persisted = await loadPersistedState(AsyncStorage, 'myapp', ['theme']);
367
+ * const store = createStore({ theme: 'light', ...persisted });
368
+ * ```
369
+ */
370
+ declare function loadPersistedState<T extends object>(storage: Storage | StorageSupportingInterface, persistKey: string, keys: (keyof T)[], parser?: PersistParser): Partial<T>;
371
+ declare function loadPersistedState<T extends object>(storage: AsyncStorageSupportingInterface, persistKey: string, keys: (keyof T)[], parser?: PersistParser): Promise<Partial<T>>;
372
+
373
+ export { type AsyncStorageSupportingInterface, type Lookup, type PersistParser, type SchemaError, type StandardSchemaFailure, type StandardSchemaIssue, type StandardSchemaSuccess, type StandardSchemaV1, type StorageSupportingInterface, type Store, type StoreOptions, type StoreType, createActions, createLookup, createStore, createStoreHook, loadPersistedState, merge, persist, useStoreKey, useStoreSelector };