@plitzi/nexus 0.31.1

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +74 -0
  3. package/README.md +563 -0
  4. package/dist/StoreContext.d.ts +10 -0
  5. package/dist/StoreContext.mjs +11 -0
  6. package/dist/StoreProvider.d.ts +24 -0
  7. package/dist/StoreProvider.mjs +51 -0
  8. package/dist/async/createAsync.d.ts +19 -0
  9. package/dist/async/createAsync.mjs +29 -0
  10. package/dist/async/hooks/useAsync.d.ts +2 -0
  11. package/dist/async/hooks/useAsync.mjs +7 -0
  12. package/dist/async/hooks/useAsyncValue.d.ts +2 -0
  13. package/dist/async/hooks/useAsyncValue.mjs +13 -0
  14. package/dist/async/index.d.ts +4 -0
  15. package/dist/async/index.mjs +4 -0
  16. package/dist/createStore/createStore.d.ts +90 -0
  17. package/dist/createStore/createStore.mjs +104 -0
  18. package/dist/createStore/helpers/PathTrie.d.ts +13 -0
  19. package/dist/createStore/helpers/PathTrie.mjs +43 -0
  20. package/dist/createStore/helpers/Subscribers.d.ts +15 -0
  21. package/dist/createStore/helpers/Subscribers.mjs +59 -0
  22. package/dist/createStore/helpers/createChainReads.d.ts +10 -0
  23. package/dist/createStore/helpers/createChainReads.mjs +37 -0
  24. package/dist/createStore/helpers/createSetState.d.ts +22 -0
  25. package/dist/createStore/helpers/createSetState.mjs +163 -0
  26. package/dist/createStore/helpers/deepMerge.d.ts +2 -0
  27. package/dist/createStore/helpers/deepMerge.mjs +9 -0
  28. package/dist/createStore/helpers/forwardParentChanges.d.ts +7 -0
  29. package/dist/createStore/helpers/forwardParentChanges.mjs +23 -0
  30. package/dist/createStore/helpers/scopeCollisions.d.ts +4 -0
  31. package/dist/createStore/helpers/scopeCollisions.mjs +14 -0
  32. package/dist/createStore/helpers/writeByPath.d.ts +11 -0
  33. package/dist/createStore/helpers/writeByPath.mjs +51 -0
  34. package/dist/createStore/hooks/shared.d.ts +12 -0
  35. package/dist/createStore/hooks/shared.mjs +85 -0
  36. package/dist/createStore/hooks/useStore.d.ts +73 -0
  37. package/dist/createStore/hooks/useStore.mjs +57 -0
  38. package/dist/createStore/hooks/useStoreById.d.ts +3 -0
  39. package/dist/createStore/hooks/useStoreById.mjs +7 -0
  40. package/dist/createStore/hooks/useStoreGetter.d.ts +9 -0
  41. package/dist/createStore/hooks/useStoreGetter.mjs +38 -0
  42. package/dist/createStore/hooks/useStoreSetter.d.ts +5 -0
  43. package/dist/createStore/hooks/useStoreSetter.mjs +11 -0
  44. package/dist/createStore/hooks/useStoreSync.d.ts +7 -0
  45. package/dist/createStore/hooks/useStoreSync.mjs +55 -0
  46. package/dist/createStore/index.d.ts +3 -0
  47. package/dist/createStore/index.mjs +5 -0
  48. package/dist/derived/createDerived.d.ts +10 -0
  49. package/dist/derived/createDerived.mjs +21 -0
  50. package/dist/derived/hooks/useDerived.d.ts +2 -0
  51. package/dist/derived/hooks/useDerived.mjs +7 -0
  52. package/dist/derived/index.d.ts +3 -0
  53. package/dist/derived/index.mjs +3 -0
  54. package/dist/entities/createEntityAdapter.d.ts +32 -0
  55. package/dist/entities/createEntityAdapter.mjs +73 -0
  56. package/dist/entities/index.d.ts +2 -0
  57. package/dist/entities/index.mjs +2 -0
  58. package/dist/helpers/createObservable.d.ts +9 -0
  59. package/dist/helpers/createObservable.mjs +18 -0
  60. package/dist/helpers/getByPath.d.ts +4 -0
  61. package/dist/helpers/getByPath.mjs +40 -0
  62. package/dist/helpers/isPathAffected.d.ts +3 -0
  63. package/dist/helpers/isPathAffected.mjs +4 -0
  64. package/dist/helpers/parsePath.d.ts +2 -0
  65. package/dist/helpers/parsePath.mjs +7 -0
  66. package/dist/helpers/setByPath.d.ts +3 -0
  67. package/dist/helpers/setByPath.mjs +19 -0
  68. package/dist/helpers/shallowEqual.d.ts +2 -0
  69. package/dist/helpers/shallowEqual.mjs +11 -0
  70. package/dist/helpers/useIsomorphicLayoutEffect.d.ts +3 -0
  71. package/dist/helpers/useIsomorphicLayoutEffect.mjs +5 -0
  72. package/dist/history/hooks/useStoreHistory.d.ts +8 -0
  73. package/dist/history/hooks/useStoreHistory.mjs +24 -0
  74. package/dist/history/index.d.ts +4 -0
  75. package/dist/history/index.mjs +3 -0
  76. package/dist/index.d.ts +16 -0
  77. package/dist/index.mjs +21 -0
  78. package/dist/middleware/cascade.d.ts +2 -0
  79. package/dist/middleware/cascade.mjs +4 -0
  80. package/dist/middleware/historyMiddleware.d.ts +28 -0
  81. package/dist/middleware/historyMiddleware.mjs +81 -0
  82. package/dist/middleware/index.d.ts +8 -0
  83. package/dist/middleware/index.mjs +6 -0
  84. package/dist/middleware/loggerMiddleware.d.ts +7 -0
  85. package/dist/middleware/loggerMiddleware.mjs +20 -0
  86. package/dist/middleware/persistMiddleware.d.ts +16 -0
  87. package/dist/middleware/persistMiddleware.mjs +36 -0
  88. package/dist/middleware/reduxDevToolsMiddleware.d.ts +6 -0
  89. package/dist/middleware/reduxDevToolsMiddleware.mjs +26 -0
  90. package/dist/types/StoreTypes.d.ts +143 -0
  91. package/dist/types/StoreTypes.mjs +4 -0
  92. package/dist/types/index.d.ts +1 -0
  93. package/dist/types/index.mjs +2 -0
  94. package/package.json +257 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # @plitzi/nexus
2
+
3
+ ## 0.31.1
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.31.1
8
+
9
+ ## 0.31.0
10
+
11
+ ### Minor Changes
12
+
13
+ - v0.31.0
14
+
15
+ ## 0.30.19
16
+
17
+ ### Patch Changes
18
+
19
+ - v0.30.19
20
+
21
+ ## 0.30.18
22
+
23
+ ### Patch Changes
24
+
25
+ - v0.30.18
26
+
27
+ ## 0.30.17
28
+
29
+ ### Patch Changes
30
+
31
+ - v0.31.0
package/LICENSE ADDED
@@ -0,0 +1,74 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity.
18
+
19
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
20
+ permissions granted by this License.
21
+
22
+ "Source" form shall mean the preferred form for making modifications.
23
+
24
+ "Object" form shall mean any form resulting from mechanical
25
+ transformation or translation of a Source form.
26
+
27
+ 2. Grant of Copyright License.
28
+
29
+ Subject to the terms and conditions of this License, each Contributor
30
+ hereby grants to You a perpetual, worldwide, non-exclusive,
31
+ no-charge, royalty-free, irrevocable copyright license to reproduce,
32
+ prepare Derivative Works of, publicly display, publicly perform,
33
+ sublicense, and distribute the Work and such Derivative Works.
34
+
35
+ 3. Grant of Patent License.
36
+
37
+ Subject to the terms and conditions of this License, each Contributor
38
+ hereby grants You a perpetual, worldwide, non-exclusive,
39
+ no-charge, royalty-free, irrevocable (except as stated in this section)
40
+ patent license to make, have made, use, offer to sell, sell, import,
41
+ and otherwise transfer the Work.
42
+
43
+ 4. Redistribution.
44
+
45
+ You may reproduce and distribute copies of the Work or Derivative Works
46
+ provided that you include a copy of this License and provide proper attribution.
47
+
48
+ 5. Submission of Contributions.
49
+
50
+ Unless explicitly stated otherwise, any Contribution intentionally submitted
51
+ shall be licensed under this License.
52
+
53
+ 6. Trademarks.
54
+
55
+ This License does not grant permission to use the trade names, trademarks,
56
+ service marks, or product names of the Licensor.
57
+
58
+ 7. Disclaimer of Warranty.
59
+
60
+ Unless required by applicable law or agreed to in writing, Licensor
61
+ provides the Work "AS IS" without warranties or conditions of any kind.
62
+
63
+ 8. Limitation of Liability.
64
+
65
+ In no event and under no legal theory shall the Licensor be liable for
66
+ damages arising from the use of the Work.
67
+
68
+ 9. Accepting Warranty or Additional Liability.
69
+
70
+ While redistributing the Work or Derivative Works, You may choose to
71
+ offer and charge a fee for acceptance of support, warranty, indemnity,
72
+ or other liability obligations.
73
+
74
+ END OF TERMS AND CONDITIONS
package/README.md ADDED
@@ -0,0 +1,563 @@
1
+ # @plitzi/nexus
2
+
3
+ A lightweight, type-safe React store built on `useSyncExternalStore`. You subscribe to **dot-notation paths** and re-render only when that exact value changes — no selectors, no reducers, no action types. On top of that core it ships scoped stores, time-travel, derived values, an entity adapter, and a middleware pipeline (logger / persist / history).
4
+
5
+ ```bash
6
+ npm install @plitzi/nexus # peer deps: react@^18 || ^19, react-dom@^18 || ^19
7
+ ```
8
+
9
+ ```ts
10
+ import { createStoreHook } from '@plitzi/nexus';
11
+
12
+ type State = { count: number; user: { name: string } };
13
+ const { useStore } = createStoreHook<State>();
14
+
15
+ function Counter() {
16
+ const [count, setCount] = useStore('count'); // typed as number
17
+ return <button onClick={() => setCount(n => n + 1)}>{count}</button>;
18
+ }
19
+ ```
20
+
21
+ ## What each piece is for
22
+
23
+ | Import | What it's for |
24
+ |---|---|
25
+ | `createStore` | Create a vanilla store (no React needed). |
26
+ | `createStoreHook<T>()` | Bind your state type once → fully-typed `useStore*` hooks. |
27
+ | `StoreProvider` | Put a store on React context; optionally scope/sync/record it. |
28
+ | `useStore` | Subscribe + write a path (or paths). Re-renders on change only. |
29
+ | `useStoreSetter` | A stable setter — write without subscribing/re-rendering. |
30
+ | `useStoreGetter` | A stable getter — read current values in callbacks, no re-render. |
31
+ | `useStoreById` | Get a named ancestor store's `StoreApi` by `id` (reachable across disconnected providers). |
32
+ | `useStoreSync` | Mirror an external value (props) **into** the store. |
33
+ | `createDerived` / `useDerived` | A memoized value computed from store paths (reselect-style). |
34
+ | `createEntityAdapter` | CRUD updaters + selectors for a normalized `Record<id, T>` map. |
35
+ | `loggerMiddleware` / `persistMiddleware` / `historyMiddleware` / `reduxDevToolsMiddleware` | Middlewares: log, persist to storage, record time-travel, connect to Redux DevTools. |
36
+ | `getStoreHistory` / `useStoreHistory` | Undo / redo / jump-to-snapshot action log (enabled by `historyMiddleware`). |
37
+
38
+ ## Architecture
39
+
40
+ ```
41
+ StoreContext.ts context object (no deps → no cycles)
42
+ createStore.ts factory + createStoreHook
43
+ StoreProvider.tsx context provider with optional sync
44
+ hooks/
45
+ useStore.ts subscribe + read
46
+ useStoreSync.ts sync external value into the store (write-only)
47
+ useStoreGetter.ts non-reactive snapshot getter
48
+ useStoreSetter.ts fire-and-forget setter
49
+ shared.ts snapshot factories, subscription helpers
50
+ derived/ createDerived + useDerived (memoized computed values)
51
+ entities/ createEntityAdapter (normalized-map CRUD + selectors)
52
+ middleware/ logger / persist / history (all on subscribeChange)
53
+ history/ getStoreHistory + useStoreHistory (time-travel, enabled by historyMiddleware)
54
+ helpers/ getByPath, setByPath, parsePath, shallowEqual…
55
+ ```
56
+
57
+ > **One change substrate.** Every committed write flows through `store.subscribeChange((change) => …)` where `change` is `{ path, prev, next }`. `loggerMiddleware`, `persistMiddleware`, and `historyMiddleware` are all just observers on this channel — and so is any middleware you write. It costs nothing on the hot path when no observer is attached. Writes can also be **intercepted** before they commit via a middleware's `beforeChange` to transform or cancel them — see [Intercepting writes](#intercepting-writes-beforechange).
58
+
59
+ ## `createStore`
60
+
61
+ ```ts
62
+ const store = createStore<MyState>({ count: 0, user: { name: 'Alice' } });
63
+ // or with an initializer function:
64
+ const store = createStore<MyState>((setState, getState) => ({ count: 0 }));
65
+
66
+ // Scoped store: reads fall through to a parent store (see "Scoped stores" below)
67
+ const child = createStore<MyState>({ record }, { parent: rootStore });
68
+
69
+ // With middlewares (loggerMiddleware / persistMiddleware / historyMiddleware / your own)
70
+ const store = createStore<MyState>({ count: 0 }, {
71
+ middlewares: [persistMiddleware({ key: 'app' }), historyMiddleware(), loggerMiddleware()]
72
+ });
73
+
74
+ // With an id, so descendants can target it by id (see "Named stores" below)
75
+ const store = createStore<MyState>({ count: 0 }, { id: 'root' });
76
+ ```
77
+
78
+ `StoreApi` exposes `getState`, `getPath`, `setState`, `subscribe`, `subscribePath`, `subscribeChange`,
79
+ `destroy?()`, and an optional `id`. Call `destroy()` to detach a scoped store from its parent and clear its
80
+ listeners (prevents leaks for short-lived scopes like list items). `StoreProvider` calls it automatically on
81
+ unmount.
82
+
83
+ ## `StoreProvider`
84
+
85
+ Wraps children with a store context. Creates a new store by default; pass `store` to provide an existing one.
86
+
87
+ ```tsx
88
+ <StoreProvider value={{ count: 0 }}>
89
+ <App />
90
+ </StoreProvider>
91
+
92
+ // Inherit from the parent — two modes via the `inherit` prop:
93
+ <StoreProvider inherit="snapshot" value={{ extra: true }}> {/* copy once at init, isolated */}
94
+ <Child />
95
+ </StoreProvider>
96
+ <StoreProvider inherit="live" value={{ record }}> {/* live scope chain (see below) */}
97
+ <Child />
98
+ </StoreProvider>
99
+
100
+ // Sync a sub-path from parent props
101
+ <StoreProvider path="schema.flat" value={flatMap}>
102
+ <Child />
103
+ </StoreProvider>
104
+
105
+ // Name it so descendants can reach it by id, even past a disconnected provider (see "Named stores")
106
+ <StoreProvider id="root" value={builderState}>
107
+ <App />
108
+ </StoreProvider>
109
+ ```
110
+
111
+ > **`inherit` modes:** `'snapshot'` copies parent keys **once** at init — isolated thereafter: writes stay
112
+ > local and parent updates do **not** propagate (use for draft/diverge editors). `'live'` keeps a **live
113
+ > link** — inherited keys stay in sync and writes delegate to the owning scope.
114
+
115
+ ## Scoped stores (live scope chain)
116
+
117
+ A store can have a `parent`. Reads resolve through the chain and writes target the owning scope. This
118
+ lets nested scopes (e.g. a list item providing its own `record`) shadow shared/global state while still
119
+ reading it live — without prop-drilling or N value-contexts.
120
+
121
+ ```ts
122
+ const root = createStore<S>({ user: { name: 'Alice' }, theme: 'dark' });
123
+ const item = createStore<S>({ record }, { parent: root });
124
+ ```
125
+
126
+ **Semantics**
127
+
128
+ - **Read fall-through (deep-merge)** — `getState()` deep-merges the chain: own values win, but nested
129
+ object branches are **merged**, not replaced, so a scope can contribute a sub-key without clobbering the
130
+ parent's siblings under the same branch.
131
+ ```ts
132
+ item.getState().record; // own
133
+ item.getState().user; // inherited from root (live)
134
+
135
+ // deep-merge of a shared branch:
136
+ const root = createStore<S>({ runtime: { sources: { variables: { a: 1 } } } });
137
+ const item = createStore<S>({ runtime: { sources: { record } } }, { parent: root });
138
+ item.getState().runtime.sources; // { variables: { a: 1 }, record } ← merged, not shadowed
139
+ ```
140
+ - **Shadowing** — a scope's own **leaf** value hides the parent's at that path (reads and subscriptions);
141
+ sibling leaves under a shared object branch are preserved (see deep-merge above).
142
+ ```ts
143
+ const item = createStore<S>({ theme: 'light' }, { parent: root });
144
+ item.getState().theme; // 'light' (shadows root's 'dark')
145
+ root.getState().theme; // 'dark' (untouched)
146
+ ```
147
+ - **Write delegation (recursive to owner / root)** — `setState(path)` walks up to the nearest scope that
148
+ owns the path; if no ancestor owns it, it delegates all the way to the **root**. So a deeply-nested scope
149
+ can write a shared branch (e.g. `runtime.sources.*`) and it lands at the root without any scope having to
150
+ pre-seed that branch.
151
+ ```ts
152
+ item.setState('user.name', 'Bob'); // delegates to the scope that owns `user`
153
+ item.setState('runtime.sources.x', v); // delegates to root even if no ancestor seeded `runtime`
154
+ item.setState('record', next); // stays local to the item scope (it owns `record`)
155
+ ```
156
+ - **Multi-level subscriptions** — consumers of a scope re-render when an inherited key changes in any
157
+ ancestor; shadowed keys are ignored. Equality still filters no-op updates.
158
+ - **Cleanup** — call `store.destroy()` (or let `<StoreProvider inherit="live">` do it on unmount) to detach
159
+ from the parent and avoid listener leaks for short-lived scopes.
160
+
161
+ Via `StoreProvider` (the common case):
162
+
163
+ ```tsx
164
+ <StoreProvider value={{ user, theme: 'dark' }}> {/* root scope */}
165
+ <StoreProvider inherit="live" value={{ record }}> {/* item scope */}
166
+ {/* useStore('user') → inherited, live */}
167
+ {/* useStore('record') → own */}
168
+ <ItemView />
169
+ </StoreProvider>
170
+ </StoreProvider>
171
+ ```
172
+
173
+ ## Named stores & reaching ancestors (`id` / `storeId` / `useStoreById`)
174
+
175
+ Give a store an **`id`** and any descendant can target it — even across a **disconnected** (`inherit`-less)
176
+ provider that would otherwise shadow it. The id lives in a parallel registry propagated by context
177
+ *independently of `inherit`*, so it never breaks the scope chain and costs nothing per provider unless used.
178
+
179
+ ```tsx
180
+ <StoreProvider id="root" value={builderState}>
181
+ ...
182
+ <StoreProvider value={{ local: 1 }}> {/* disconnected — no inherit */}
183
+ <Leaf />
184
+ </StoreProvider>
185
+ </StoreProvider>
186
+ ```
187
+
188
+ ```ts
189
+ // inside Leaf — reach the named root store past the disconnected provider:
190
+ const [theme, setTheme] = useStore('theme', { storeId: 'root' }); // reactive read + write
191
+ const getUser = useStoreGetter('user', { storeId: 'root' }); // imperative read
192
+ const setUser = useStoreSetter('user', { storeId: 'root' }); // imperative write
193
+
194
+ // the raw StoreApi — for createDerived / batch / subscribe / passing around:
195
+ const rootStore = useStoreById('root');
196
+ rootStore.batch(() => { ... });
197
+ const itemCount = createDerived(rootStore, ['items'], ([items]) => items.length);
198
+ ```
199
+
200
+ - **`storeId` option** — available on every hook (`useStore`, `useStoreGetter`, `useStoreSetter`,
201
+ `useStoreSync`). Resolves the nearest ancestor store registered under that id. An explicit `store` option
202
+ still wins over `storeId`.
203
+ - **`useStoreById(id?)`** — returns the raw `StoreApi`. With no `id`, returns the nearest provider's store.
204
+ Use it when you need the store object itself (`createDerived`/`createAsync`, `batch`, `subscribe`); the
205
+ options above only read/write *through* it.
206
+ - **`store.id`** — the identity is also set on the store, handy for logging/devtools.
207
+ - **Lifecycle** — registration is context-scoped: an id stops resolving the moment its provider unmounts.
208
+ There is no global registry, so nothing to clean up and no leaks.
209
+ - **Duplicate ids (dev)** — registering an id that shadows an ancestor with the same id logs a warning in
210
+ development (stripped in production); the nearer store wins. Sibling subtrees may reuse an id freely.
211
+
212
+ ## `useStore`
213
+
214
+ Subscribe to store values. Triggers re-render only when the selected value changes.
215
+
216
+ ```ts
217
+ // Full state
218
+ const [state, setState] = useStore();
219
+
220
+ // Single path
221
+ const [name, setName] = useStore('user.name');
222
+
223
+ // With default value
224
+ const [el, setEl] = useStore(`schema.flat.${id}` as PathOf<MyState>, { defaultValue: {} });
225
+
226
+ // Dynamic path (function resolves the path string at runtime)
227
+ const [val, setVal] = useStore(s => `style.platform.${s.displayMode}` as PathOf<MyState>);
228
+
229
+ // Transform the value (memoized, no extra re-renders)
230
+ const [upper] = useStore('user.name', { transformer: v => v.toUpperCase() });
231
+
232
+ // Multi-path
233
+ const [[name, count], setName, setCount] = useStore(['user.name', 'count']);
234
+
235
+ // Multi-path with dynamic path
236
+ const [[name, val], setName, setVal] = useStore([
237
+ 'user.name',
238
+ s => `style.${s.displayMode}` as PathOf<MyState>
239
+ ]);
240
+
241
+ // Multi-path with transformer
242
+ const [derived] = useStore(['user.name', 'count'], {
243
+ transformer: ([name, count]) => `${name} (${count})`
244
+ });
245
+
246
+ // Options
247
+ useStore('user.name', { enabled: false }); // unsubscribed
248
+ useStore('user.name', { mode: 'mount' }); // read once on mount
249
+ useStore('user.name', { store: myStore }); // explicit store
250
+ useStore('user.name', { storeId: 'root' }); // a named ancestor store (see "Named stores")
251
+ useStore('user.name', { equalityFn: Object.is }); // custom equality
252
+ ```
253
+
254
+ ## `useStoreSync`
255
+
256
+ Syncs an external value into the store on every render (write-only, no subscription). Returns `void`.
257
+
258
+ ```ts
259
+ // Sync full state (merges)
260
+ useStoreSync(undefined, fullState);
261
+
262
+ // Sync a single path
263
+ useStoreSync('schema', schema);
264
+
265
+ // Sync multiple paths
266
+ useStoreSync(['schema', 'style'], [schema, style]);
267
+
268
+ // Options
269
+ useStoreSync('schema', schema, { mode: 'mount' }); // sync on mount only
270
+ useStoreSync('schema', schema, { enabled: false }); // disabled
271
+ useStoreSync('schema', schema, { syncStrategy: 'render' }); // sync during render (no layout effect)
272
+ ```
273
+
274
+ ## `useStoreGetter`
275
+
276
+ Non-reactive. Returns a stable getter function that reads the current store value at call time (no re-renders).
277
+
278
+ ```ts
279
+ // Full state getter
280
+ const get = useStoreGetter();
281
+ get(); // → MyState
282
+ get('user.name'); // → string
283
+ get('user.name', 'default'); // → string | 'default'
284
+
285
+ // Scoped getter
286
+ const getFlat = useStoreGetter('schema.flat');
287
+ getFlat(); // → FlatMap
288
+ getFlat(id); // → Element
289
+ getFlat(id, fallback); // → Element | fallback
290
+
291
+ // Multi-entry tuple
292
+ const [getName, getCount] = useStoreGetter(['user.name', 'count']);
293
+ getName(); // → string
294
+ getCount(); // → number
295
+
296
+ // Mixed paths + functions
297
+ const [getFlat, getCount] = useStoreGetter(['schema.flat', s => s.count]);
298
+ ```
299
+
300
+ ## `useStoreSetter`
301
+
302
+ Non-reactive. Returns a stable setter. No re-renders on write.
303
+
304
+ ```ts
305
+ // Full setState
306
+ const setState = useStoreSetter();
307
+ setState('user.name', 'Bob');
308
+ setState('user.name', prev => prev + '!');
309
+ setState(undefined, fullState);
310
+
311
+ // Scoped setter
312
+ const setFlat = useStoreSetter('schema.flat');
313
+ setFlat(id, element); // sets schema.flat.${id}
314
+ setFlat(`${id}.attributes`, attrs); // sets schema.flat.${id}.attributes
315
+ setFlat(undefined, flatObj); // replaces schema.flat
316
+ ```
317
+
318
+ ## `createStoreHook`
319
+
320
+ Factory that binds `TState` at the module level, giving fully-typed hooks without repeating the generic at every call site.
321
+
322
+ ```ts
323
+ const { useStore, useStoreSync, useStoreGetter, useStoreSetter } = createStoreHook<MyState>();
324
+ ```
325
+
326
+ ## `createDerived` / `useDerived`
327
+
328
+ **What it's for:** a value *computed* from store paths — totals, filtered lists, formatted strings. It computes once, memoizes, and only wakes subscribers when the **result** changes (a dependency edit that doesn't affect the output costs nothing downstream). Think reselect / Jotai derived atoms / MobX `computed`, shared across every consumer instead of recomputed per component.
329
+
330
+ ```ts
331
+ import { createDerived, useDerived } from '@plitzi/nexus';
332
+
333
+ const total = createDerived(
334
+ store,
335
+ ['items'], // dependency paths (typed)
336
+ ([items]) => Object.values(items).reduce((s, i) => s + i.qty * i.price, 0)
337
+ );
338
+
339
+ total.get(); // current value (recomputed lazily if a dep changed)
340
+ const off = total.subscribe(() => render()); // wakes only when `total` changes
341
+ total.destroy(); // detach from the store
342
+
343
+ // React — the computation is shared, not repeated per component:
344
+ function CartTotal() {
345
+ const value = useDerived(total);
346
+ return <span>${value}</span>;
347
+ }
348
+
349
+ // Multiple deps + custom equality (skip object-identity churn):
350
+ const ids = createDerived(store, ['items'], ([m]) => Object.keys(m), {
351
+ equalityFn: (a, b) => a.length === b.length && a.every((x, i) => x === b[i])
352
+ });
353
+ ```
354
+
355
+ > Use `createDerived` for a value shared across components; reach for `useStore('path', { transformer })` when the transform is local to one component.
356
+
357
+ ## `createEntityAdapter`
358
+
359
+ **What it's for:** the boilerplate around a normalized `Record<id, entity>` map. Write ops return **immutable updaters** you hand straight to `setState`; selectors read a map. (It doesn't change the cost of an immutable map write — see [Performance](#performance) — it just removes the hand-rolled spread/merge.)
360
+
361
+ ```ts
362
+ import { createEntityAdapter } from '@plitzi/nexus';
363
+
364
+ type Todo = { id: string; text: string; done: boolean };
365
+ const todos = createEntityAdapter<Todo>(); // selectId defaults to `e => e.id`
366
+
367
+ store.setState('todos', todos.addMany([a, b]));
368
+ store.setState('todos', todos.updateOne({ id: 'a', changes: { done: true } })); // shallow-merge
369
+ store.setState('todos', todos.upsertOne(c));
370
+ store.setState('todos', todos.removeOne('b'));
371
+
372
+ const map = store.getPath('todos');
373
+ todos.selectAll(map); // Todo[]
374
+ todos.selectById(map, 'a'); // Todo | undefined
375
+ todos.selectTotal(map); // number
376
+
377
+ // Custom id field + sort order for selectAll / selectIds:
378
+ createEntityAdapter<Row>({ selectId: r => r.key, sortComparer: (a, b) => a.name.localeCompare(b.name) });
379
+ ```
380
+
381
+ Ops: `addOne/addMany` (ignore existing), `setOne/setMany/setAll` (replace), `updateOne/updateMany` (merge changes), `upsertOne/upsertMany` (add or merge), `removeOne/removeMany/removeAll`. Each returns the **same map reference** when nothing changed, so the store skips the write.
382
+
383
+ ## Middleware (`loggerMiddleware` / `persistMiddleware` / `historyMiddleware`)
384
+
385
+ **What it's for:** cross-cutting logic on every write, centralized in one place. A middleware is `(api) => { beforeChange?, onChange? } | void`; its setup body runs once after creation (so it can hydrate). It can do two things: **intercept** a write before it commits (`beforeChange`) and **observe** a write after it commits (`onChange`, the `{ path, prev, next }` change). logger, persist, and history are built-in observers; write your own the same way. Middlewares are per-store — in a scope chain, attach them where the writes you care about commit (shared keys delegate to the owning scope, so the owning scope's interceptors run).
386
+
387
+ ```ts
388
+ import { createStore, loggerMiddleware, persistMiddleware, historyMiddleware } from '@plitzi/nexus';
389
+
390
+ const store = createStore<State>(initial, {
391
+ middlewares: [
392
+ persistMiddleware({ key: 'app' }), // put persist FIRST so it hydrates before others observe
393
+ historyMiddleware(),
394
+ loggerMiddleware()
395
+ ]
396
+ });
397
+
398
+ // Write your own — an observer of every committed change:
399
+ const analytics: StoreMiddleware<State> = api => ({
400
+ onChange: ({ path, prev, next }) => track('store.change', { path })
401
+ });
402
+
403
+ // Or subscribe imperatively to the same substrate:
404
+ const off = store.subscribeChange(({ path, prev, next }) => {});
405
+ ```
406
+
407
+ ### Intercepting writes (`beforeChange`)
408
+
409
+ `onChange` only **observes** committed changes — it can't stop or rewrite them. A middleware's `beforeChange` runs **before** the write commits and can transform the value, or cancel the write entirely by returning `CANCEL`. Returning `undefined` lets the value through unchanged. It's just another middleware, so you write your own the same way you'd write a logger.
410
+
411
+ ```ts
412
+ import { createStore, CANCEL } from '@plitzi/nexus';
413
+
414
+ const guard: StoreMiddleware<State> = () => ({
415
+ beforeChange: ({ path, value, prev }) => {
416
+ if (path === 'role' && !isAdmin) {
417
+ return CANCEL; // block the write — nothing commits, no observer fires
418
+ }
419
+
420
+ if (path === 'ui.size') {
421
+ return Math.min(value as number, 100); // clamp before it commits
422
+ }
423
+
424
+ return undefined; // let everything else through unchanged
425
+ }
426
+ });
427
+
428
+ const store = createStore<State>(initial, { middlewares: [guard] });
429
+ ```
430
+
431
+ `beforeChange` receives a `WriteContext`: `{ path, value, prev }` — the changed `path` (`undefined` for a whole-state write, where `value` is the full next state), the resolved `value` about to be written (function setters are already applied, so you see the concrete value), and the current `prev` at that path.
432
+
433
+ When several middlewares declare a `beforeChange`, they run in **middleware order**, each seeing the previous one's result — so an earlier one can transform a value and a later one can still cancel it. Like every middleware, they run on the store that commits the write (in a scope chain, a write delegated to the owning scope runs that scope's interceptors).
434
+
435
+ ### `persistMiddleware`
436
+
437
+ Mirrors the store to a key/value storage and rehydrates on creation.
438
+
439
+ ```ts
440
+ persistMiddleware<State>({
441
+ key: 'app',
442
+ storage, // default: localStorage, no-op on SSR. Any { getItem, setItem, removeItem }
443
+ partialize: s => ({ user: s.user }), // persist only part of the state
444
+ version: 2, // bump when the shape changes…
445
+ migrate: (old, v) => ({ user: old }), // …and map an older payload forward
446
+ merge: (persisted, current) => ({ ...current, ...persisted }),
447
+ debounce: 200 // coalesce rapid writes (ms); 0 = write synchronously
448
+ });
449
+ ```
450
+
451
+ ### `loggerMiddleware`
452
+
453
+ ```ts
454
+ loggerMiddleware<State>({
455
+ filter: change => change.path !== 'mouse', // log only some changes
456
+ sink: change => console.log(change.path, change.next) // default sink is console.log
457
+ });
458
+ ```
459
+
460
+ ### `reduxDevToolsMiddleware`
461
+
462
+ Mirrors the store to the [Redux DevTools](https://github.com/reduxjs/redux-devtools) browser extension: every committed change is sent as an action (labelled by the changed path) and time-travel from the DevTools UI (jump / rollback) is written back into the store. It's a **no-op** when the extension isn't installed (production, SSR, browsers without it), so it's safe to leave wired in.
463
+
464
+ ```ts
465
+ import { createStore, reduxDevToolsMiddleware } from '@plitzi/nexus';
466
+
467
+ const store = createStore<State>(initial, {
468
+ middlewares: [reduxDevToolsMiddleware({ name: 'my-app' })]
469
+ });
470
+
471
+ store.setState('count', 1); // → action "count" in DevTools
472
+ store.setState('user.name', 'Bob'); // → action "user.name"
473
+ ```
474
+
475
+ ```ts
476
+ reduxDevToolsMiddleware<State>({
477
+ name: 'my-app', // instance name in the DevTools dropdown (default 'nexus')
478
+ action: change => `set:${change.path}` // how to label each action (default: the changed path)
479
+ });
480
+ ```
481
+
482
+ Intended for the root store; like `persist`/`history` it's per-store and not cascaded.
483
+
484
+ ## Time-travel (`historyMiddleware`)
485
+
486
+ Records an action log you can undo / redo / jump through. Enable it by adding `historyMiddleware()` to the store; read it reactively with `useStoreHistory`, or imperatively with `getStoreHistory(store)` (which returns the handle the middleware registered, or `undefined` when it isn't enabled). Without the middleware, `useStoreHistory` returns an empty, no-op view.
487
+
488
+ ```tsx
489
+ import { useStoreHistory } from '@plitzi/nexus';
490
+
491
+ function HistoryPanel() {
492
+ const { entries, index, canUndo, canRedo, undo, redo, travelTo } = useStoreHistory<State>();
493
+
494
+ return (
495
+ <>
496
+ <button disabled={!canUndo} onClick={undo}>Undo</button>
497
+ <button disabled={!canRedo} onClick={redo}>Redo</button>
498
+ {entries.map((entry, i) => (
499
+ <button key={i} onClick={() => travelTo(i)} data-active={i === index}>
500
+ {entry.path} = {String(entry.value)}
501
+ </button>
502
+ ))}
503
+ </>
504
+ );
505
+ }
506
+ ```
507
+
508
+ `<StoreProvider history>` (or `history="schema"` to scope it to a subtree) starts recording from mount.
509
+
510
+ ## CSP & `new Function`
511
+
512
+ By default `@plitzi/nexus` uses a compiled codegen path (`new Function`) for structural-sharing writes, which is orders of magnitude faster than the recursive fallback for deep paths. When a strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) blocks `new Function`, the store auto-detects the error at first use and falls back to a recursive writer with identical behaviour — you **don't need to do anything** for CSP environments to work.
513
+
514
+ If you want to avoid even the one-time probe, force the recursive fallback directly:
515
+
516
+ ```ts
517
+ import { setCodegenEnabled } from '@plitzi/nexus';
518
+
519
+ // Skip codegen probe, go straight to recursive writer (no new Function)
520
+ setCodegenEnabled(false);
521
+ ```
522
+
523
+ | Value | Behaviour |
524
+ |---|---|
525
+ | `undefined` (default) | Auto-detect: probe `new Function` once; cache result. |
526
+ | `false` | Bypass probe entirely — recursive writer only. Safe for strict CSP. |
527
+ | `true` | Force codegen even if probe fails (testing only). |
528
+
529
+ Call `setCodegenEnabled(undefined)` to restore auto-detection.
530
+
531
+ ## Performance
532
+
533
+ Notification is `O(depth)` — a few `Map` lookups for the changed path's prefixes — **regardless of how many subscribers exist**, so a million watchers cost the same as one for an unrelated change. The *write* copies the containers on the changed path (immutable structural sharing, the same cost Redux / Zustand / Jotai pay), so model state as a **tree / normalized map** to keep each write touching a small path. Single-segment writes mutate the live working copy in place (O(1)); snapshots from `getState()` are always distinct clones, so they stay immutable for React.
534
+
535
+ ## Types
536
+
537
+ All types live in `src/types/StoreTypes.ts`:
538
+
539
+ | Type | Description |
540
+ |---|---|
541
+ | `PathOf<T>` | Union of all valid dot-paths in `T` |
542
+ | `PathValue<T, P>` | Value type at path `P` in `T` |
543
+ | `PathOrFn<T>` | `PathOf<T>` or a function `(state: T) => PathOf<T>` |
544
+ | `PathValues<T, Paths>` | Tuple of values for an array of paths |
545
+ | `PathSetter<T, P>` | Setter function for a single path |
546
+ | `PathSetters<T, Paths>` | Tuple of setters for an array of paths |
547
+ | `MultiPathReturn<T, Paths>` | `[values, ...setters]` tuple |
548
+ | `StoreApi<T>` | `{ getState, getPath, setState, subscribe, subscribePath, subscribeChange, destroy? }` |
549
+ | `StoreChange<T>` | `{ path, prev, next }` — payload of `subscribeChange` / middleware `onChange` |
550
+ | `StoreMiddleware<T>` | `(api) => { beforeChange?, onChange? } \| void` — a middleware factory |
551
+ | `WriteContext<T>` | `{ path, value, prev }` — payload of a `beforeChange` interceptor |
552
+ | `WriteInterceptor<T>` | `(ctx: WriteContext<T>) => unknown` — return a value, `CANCEL`, or `undefined` |
553
+ | `CANCEL` | Sentinel an interceptor returns to abort a write |
554
+ | `Derived<R>` | `{ get, subscribe, destroy }` — a `createDerived` handle |
555
+ | `EntityAdapter<T>` | CRUD updaters + selectors from `createEntityAdapter` |
556
+ | `EntityUpdater<T>` | `(map) => map` — an immutable entity-map update for `setState` |
557
+ | `SetState<T>` | Full `setState` overload signature |
558
+ | `UseStoreOptions` | Options for `useStore` (mode, enabled, equalityFn, defaultValue, transformer) |
559
+ | `UseStoreMultiOptions` | Options for multi-path `useStore` |
560
+ | `UseStoreSyncOptions` | Options for `useStoreSync` (mode, enabled, syncStrategy) |
561
+ | `UseStoreSyncMultiOptions` | Options for multi-path `useStoreSync` |
562
+ | `UseStoreGetterOptions` | Options for `useStoreGetter` |
563
+ | `UseStoreSetterOptions` | Options for `useStoreSetter` |
@@ -0,0 +1,10 @@
1
+ import { StoreApi } from './types';
2
+ declare const StoreContext: import('react').Context<StoreApi<any> | undefined>;
3
+ export type StoreRegistry = {
4
+ readonly id: string;
5
+ readonly store: StoreApi<any>;
6
+ readonly parent: StoreRegistry | undefined;
7
+ };
8
+ declare const StoreRegistryContext: import('react').Context<StoreRegistry | undefined>;
9
+ declare const findStoreInRegistry: (registry: StoreRegistry | undefined, id: string) => StoreApi<any> | undefined;
10
+ export { StoreContext, StoreRegistryContext, findStoreInRegistry };