@plitzi/nexus 0.32.0 → 0.32.2

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 (79) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +211 -27
  3. package/dist/advanced/index.d.ts +5 -0
  4. package/dist/advanced/index.mjs +10 -0
  5. package/dist/async/index.d.ts +0 -2
  6. package/dist/async/index.mjs +1 -3
  7. package/dist/createStore/createStore.d.ts +3 -43
  8. package/dist/createStore/createStore.mjs +85 -100
  9. package/dist/createStore/helpers/PathTrie.mjs +14 -14
  10. package/dist/createStore/helpers/createChainReads.d.ts +1 -1
  11. package/dist/createStore/helpers/createChainReads.mjs +22 -13
  12. package/dist/createStore/helpers/createSetState.mjs +11 -9
  13. package/dist/createStore/helpers/forwardParentChanges.mjs +16 -13
  14. package/dist/createStore/index.mjs +3 -3
  15. package/dist/derived/index.d.ts +0 -1
  16. package/dist/derived/index.mjs +1 -2
  17. package/dist/entities/createEntityStore.d.ts +0 -3
  18. package/dist/entities/createEntityStore.mjs +71 -78
  19. package/dist/env.d.ts +4 -0
  20. package/dist/env.mjs +3 -0
  21. package/dist/history/index.d.ts +0 -2
  22. package/dist/history/index.mjs +1 -2
  23. package/dist/index.d.ts +23 -15
  24. package/dist/index.mjs +14 -22
  25. package/dist/middleware/historyMiddleware.mjs +26 -26
  26. package/dist/middleware/persistMiddleware.mjs +11 -11
  27. package/dist/middleware/recorderMiddleware.d.ts +21 -0
  28. package/dist/middleware/recorderMiddleware.mjs +33 -0
  29. package/dist/{StoreContext.d.ts → react/StoreContext.d.ts} +1 -1
  30. package/dist/{StoreContext.mjs → react/StoreContext.mjs} +1 -1
  31. package/dist/{StoreProvider.d.ts → react/StoreProvider.d.ts} +4 -2
  32. package/dist/react/StoreProvider.mjs +62 -0
  33. package/dist/react/createStoreHook.d.ts +44 -0
  34. package/dist/react/createStoreHook.mjs +27 -0
  35. package/dist/{createStore → react}/hooks/shared.mjs +2 -2
  36. package/dist/{async → react}/hooks/useAsync.d.ts +1 -1
  37. package/dist/{async → react}/hooks/useAsync.mjs +1 -1
  38. package/dist/{async → react}/hooks/useAsyncValue.d.ts +1 -1
  39. package/dist/{async → react}/hooks/useAsyncValue.mjs +1 -1
  40. package/dist/{derived → react}/hooks/useDerived.d.ts +1 -1
  41. package/dist/{derived → react}/hooks/useDerived.mjs +1 -1
  42. package/dist/react/hooks/useEntity.d.ts +10 -0
  43. package/dist/react/hooks/useEntity.mjs +23 -0
  44. package/dist/{createStore → react}/hooks/useStore.mjs +2 -2
  45. package/dist/{createStore → react}/hooks/useStoreById.mjs +2 -2
  46. package/dist/{createStore → react}/hooks/useStoreGetter.mjs +1 -1
  47. package/dist/react/hooks/useStoreHistory.mjs +29 -0
  48. package/dist/{createStore → react}/hooks/useStoreSetter.mjs +1 -1
  49. package/dist/{createStore → react}/hooks/useStoreSync.mjs +2 -2
  50. package/dist/react/index.d.ts +17 -0
  51. package/dist/react/index.mjs +16 -0
  52. package/dist/{helpers → react}/useIsomorphicLayoutEffect.mjs +1 -1
  53. package/dist/types/StoreTypes.d.ts +21 -0
  54. package/dist/vue/index.d.ts +8 -0
  55. package/dist/vue/index.mjs +7 -0
  56. package/dist/vue/injection.d.ts +3 -0
  57. package/dist/vue/injection.mjs +13 -0
  58. package/dist/vue/useAsync.d.ts +3 -0
  59. package/dist/vue/useAsync.mjs +10 -0
  60. package/dist/vue/useDerived.d.ts +3 -0
  61. package/dist/vue/useDerived.mjs +10 -0
  62. package/dist/vue/useEntity.d.ts +11 -0
  63. package/dist/vue/useEntity.mjs +29 -0
  64. package/dist/vue/useStore.d.ts +13 -0
  65. package/dist/vue/useStore.mjs +31 -0
  66. package/dist/vue/useStoreHistory.d.ts +12 -0
  67. package/dist/vue/useStoreHistory.mjs +33 -0
  68. package/logo.svg +77 -22
  69. package/package.json +133 -56
  70. package/dist/StoreProvider.mjs +0 -57
  71. package/dist/history/hooks/useStoreHistory.mjs +0 -24
  72. /package/dist/{createStore → react}/hooks/shared.d.ts +0 -0
  73. /package/dist/{createStore → react}/hooks/useStore.d.ts +0 -0
  74. /package/dist/{createStore → react}/hooks/useStoreById.d.ts +0 -0
  75. /package/dist/{createStore → react}/hooks/useStoreGetter.d.ts +0 -0
  76. /package/dist/{history → react}/hooks/useStoreHistory.d.ts +0 -0
  77. /package/dist/{createStore → react}/hooks/useStoreSetter.d.ts +0 -0
  78. /package/dist/{createStore → react}/hooks/useStoreSync.d.ts +0 -0
  79. /package/dist/{helpers → react}/useIsomorphicLayoutEffect.d.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @plitzi/nexus
2
2
 
3
+ ## 0.32.2
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.32.2
8
+
9
+ ## Unreleased
10
+
11
+ ### Minor Changes
12
+
13
+ - Framework-agnostic core. The **package root `@plitzi/nexus` is now the agnostic core** (zero React): `createStore`,
14
+ imperative `get/set/watch`, middlewares, and the async/derived/entities primitives. React bindings live in
15
+ `@plitzi/nexus/react` (`StoreProvider`, `createStoreHook`, all hooks, `useEntity`); Next.js helpers in
16
+ `@plitzi/nexus/next`. Source is organized to match: `src/react/`, `src/next/`, and the agnostic modules at the top
17
+ level.
18
+ - Removed the Vite-only `import.meta.env.MODE`; dev/prod/test detection now uses `process.env.NODE_ENV`, so the
19
+ dev-only warnings behave identically under webpack, esbuild, Rollup, Bun, raw Node and Vite/Astro.
20
+ - Added a first-class **Vue 3 integration** at `@plitzi/nexus/vue`: `provideStore` / `injectStore`, `useStore`
21
+ (writable ref / `v-model`), `useStoreValue`, `createStoreComposable`, `useEntity*`, `useDerived`, `useAsync`, and
22
+ `useStoreHistory`. `vue` is an optional peer dependency (`^3.4`).
23
+ - Added runnable examples (`examples/`) and integration docs (`docs/integrations/`) for React, Next.js, Astro 6 (LTS),
24
+ Astro 7, Vue and Svelte.
25
+
26
+ ### Breaking Changes
27
+
28
+ - **React bindings moved out of the root.** Import `StoreProvider`, `createStoreHook`, `useStore`, `useStoreHistory`,
29
+ etc. from `@plitzi/nexus/react` instead of `@plitzi/nexus` (or the old `/createStore`, `/StoreProvider`,
30
+ `/StoreContext` subpaths). The root now exports only the agnostic core. Agnostic symbols (`createStore`, middlewares,
31
+ `createServerSnapshot`, types) stay on the root.
32
+ - `createEntityStore` is now framework-agnostic and no longer exposes `useOne` / `useIds` / `useAll` on the returned
33
+ object. Use the React bindings instead: `const { useOne, useIds, useAll } = useEntity(store)`, or the standalone
34
+ `useEntityOne(store, id)` / `useEntityIds(store)` / `useEntityAll(store)` from `@plitzi/nexus/react`.
35
+
36
+ ## 0.32.1
37
+
38
+ ### Patch Changes
39
+
40
+ - v0.32.1
41
+
3
42
  ## 0.32.0
4
43
 
5
44
  ### Minor Changes
package/README.md CHANGED
@@ -1,17 +1,34 @@
1
1
  <p align="center">
2
- <img src="./logo.svg" alt="@plitzi/nexus" width="640" />
2
+ <img src="./logo.svg" alt="@plitzi/nexus — one store, every framework" width="640" />
3
3
  </p>
4
4
 
5
5
  # @plitzi/nexus
6
6
 
7
- 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). Fully compatible with SSR, React Server Components, and Next.js App Router.
7
+ A lightweight, type-safe state store with a **framework-agnostic core** and first-class React bindings, 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). Fully compatible with SSR, React Server Components, Next.js App Router, and Astro (6 LTS & 7) islands.
8
+
9
+ ## Entry points
10
+
11
+ The **package root is the framework-agnostic core** (zero React). Framework bindings live in their own entries.
12
+
13
+ | Import | What it is |
14
+ |---|---|
15
+ | `@plitzi/nexus` | **Agnostic core** — `createStore`, imperative `get/set/watch`, middlewares, async/derived/entities. Zero React. |
16
+ | `@plitzi/nexus/advanced` | The opt-in add-ons (`async`, `derived`, `entities`, `history`, `middleware`) re-exported in one place, kept off the root so root autocomplete stays focused on the core. |
17
+ | `@plitzi/nexus/react` | React bindings — `StoreProvider`, `createStoreHook`, `useStore`, `useStoreHistory`, `useEntity`, `useAsync`, `useDerived`, … |
18
+ | `@plitzi/nexus/vue` | Vue 3 bindings — `provideStore`, `useStore` (writable ref), `useStoreValue`, `useEntity`, `useStoreHistory`, … |
19
+ | `@plitzi/nexus/next` | Next.js App Router helpers (`bindServerAction`). |
20
+ | `@plitzi/nexus/rsc` | Server Component → Client handoff markers (`createServerSnapshot`, `isServerSnapshot`, `stripServerFlag`). Also re-exported from the root. |
21
+
22
+ Any other framework works by talking to the root directly (`store.subscribe` / `store.getState`) — see the Svelte
23
+ [example](./examples/svelte). For Astro, see [docs/integrations/astro.md](./docs/integrations/astro.md).
8
24
 
9
25
  ```bash
10
- npm install @plitzi/nexus # peer deps: react@^18 || ^19, react-dom@^18 || ^19
26
+ npm install @plitzi/nexus # React bindings peer deps: react@^18 || ^19, react-dom@^18 || ^19
11
27
  ```
12
28
 
13
29
  ```ts
14
- import { createStoreHook } from '@plitzi/nexus';
30
+ // React: hooks come from the /react entry
31
+ import { createStoreHook } from '@plitzi/nexus/react';
15
32
 
16
33
  type State = { count: number; user: { name: string } };
17
34
  const { useStore } = createStoreHook<State>();
@@ -22,44 +39,75 @@ function Counter() {
22
39
  }
23
40
  ```
24
41
 
42
+ ## The whole imperative API in three verbs
43
+
44
+ Outside React, a store speaks three words. `get`, `set` and `watch` are typed against your state and cover the common case — the longer `getState` / `getPath` / `subscribePath` names stay for advanced reads (default values) and back-compat.
45
+
46
+ ```ts
47
+ import { createStore } from '@plitzi/nexus';
48
+
49
+ const store = createStore<State>({ count: 0, user: { name: 'Carlos' } });
50
+
51
+ store.get('user.name'); // 'Carlos' (one path, no full merge)
52
+ store.get(); // entire state
53
+ store.set('user.name', 'Ada'); // typed dot-path write
54
+ store.set('count', n => n + 1); // updater form
55
+ const off = store.watch('user.name', name => render(name)); // fires only for this path
56
+ ```
57
+
25
58
  ## What each piece is for
26
59
 
60
+ ### Core — the 90%
61
+
27
62
  | Import | What it's for |
28
63
  |---|---|
29
- | `createStore` | Create a vanilla store (no React needed). |
64
+ | `createStore` | Create a vanilla store (no React needed). Exposes `get` / `set` / `watch`. |
30
65
  | `createStoreHook<T>()` | Bind your state type once → fully-typed `useStore*` hooks. |
31
66
  | `StoreProvider` | Put a store on React context; optionally scope/sync/record it. |
32
67
  | `useStore` | Subscribe + write a path (or paths). Re-renders on change only. |
68
+
69
+ ### Advanced — reach for it when you need it
70
+
71
+ Also importable from the focused `@plitzi/nexus/advanced` entry point, which keeps root autocomplete on the Core.
72
+
73
+ | Import | What it's for |
74
+ |---|---|
33
75
  | `useStoreSetter` | A stable setter — write without subscribing/re-rendering. |
34
76
  | `useStoreGetter` | A stable getter — read current values in callbacks, no re-render. |
35
77
  | `useStoreById` | Get a named ancestor store's `StoreApi` by `id` (reachable across disconnected providers). |
36
78
  | `useStoreSync` | Mirror an external value (props) **into** the store. |
37
79
  | `createDerived` / `useDerived` | A memoized value computed from store paths (reselect-style). |
38
- | `createEntityAdapter` | CRUD updaters + selectors for a normalized `Record<id, T>` map. |
39
- | `loggerMiddleware` / `persistMiddleware` / `historyMiddleware` / `reduxDevToolsMiddleware` | Middlewares: log, persist to storage, record time-travel, connect to Redux DevTools. |
80
+ | `createEntityAdapter` / `createEntityStore` | CRUD + selectors for a normalized `Record<id, T>` map; the store variant adds O(1) per-item updates. |
81
+ | `createAsync` / `useAsync` / `useAsyncValue` | Race-safe fetch landed on a path; inline UI or Suspense. |
82
+ | `loggerMiddleware` / `persistMiddleware` / `historyMiddleware` / `reduxDevToolsMiddleware` / `createRecorder` | Middlewares: log, persist to storage, record time-travel, connect to Redux DevTools, buffer recent changes for a custom dev-tools panel. |
83
+ | `getStoreHistory` / `useStoreHistory` | Undo / redo / jump-to-snapshot action log (enabled by `historyMiddleware`). |
40
84
  | `setCodegenEnabled` | Force on/off the `new Function` codegen path (auto-detects CSP/SSR). |
41
- | `createServerSnapshot` | Mark server-fetched data for RSC → Client handoff. |
42
- | `isServerSnapshot` | Check if a value has the SSR marker flag. |
85
+ | `createServerSnapshot` / `isServerSnapshot` | Mark server-fetched data for RSC → Client handoff, and check the marker. |
43
86
  | `bindServerAction` | Optimistic updates + revalidation for Next.js Server Actions. |
44
- | `getStoreHistory` / `useStoreHistory` | Undo / redo / jump-to-snapshot action log (enabled by `historyMiddleware`). |
45
87
 
46
88
  ## Architecture
47
89
 
90
+ The package root is the **framework-agnostic core**; React, Vue, and Next bindings are physically separate
91
+ entries that depend on it (never the other way round), so the core ships zero framework code.
92
+
48
93
  ```
49
- StoreContext.ts context object (no deps → no cycles)
50
- createStore.ts factory + createStoreHook
51
- StoreProvider.tsx context provider with optional sync
52
- hooks/
53
- useStore.ts subscribe + read
54
- useStoreSync.ts sync external value into the store (write-only)
55
- useStoreGetter.ts non-reactive snapshot getter
56
- useStoreSetter.ts fire-and-forget setter
57
- shared.ts snapshot factories, subscription helpers
58
- derived/ createDerived + useDerived (memoized computed values)
59
- entities/ createEntityAdapter (normalized-map CRUD + selectors)
60
- middleware/ logger / persist / history (all on subscribeChange)
61
- history/ getStoreHistory + useStoreHistory (time-travel, enabled by historyMiddleware)
62
- helpers/ getByPath, setByPath, parsePath, shallowEqual…
94
+ src/
95
+ index.ts agnostic core barrel (createStore, middlewares, async/derived/entities, rsc)
96
+ createStore/ store factory + scope chain / write engine
97
+ createStore.ts factory (get/set/watch + getState/setState/subscribe…)
98
+ helpers/ writeByPath (codegen), deepMerge, scope reads, PathTrie, Subscribers…
99
+ derived/ createDerived (memoized computed values)
100
+ entities/ createEntityAdapter (map CRUD) + createEntityStore (reactive O(1) per item)
101
+ async/ createAsync (race-safe fetch landed on a path)
102
+ middleware/ logger / persist / history / reduxDevTools / cascade (all on subscribeChange)
103
+ history/ getStoreHistory (time-travel handle, enabled by historyMiddleware)
104
+ rsc/ createServerSnapshot / isServerSnapshot / stripServerFlag
105
+ helpers/ getByPath, setByPath, parsePath, shallowEqual, isPathAffected…
106
+ types/ StoreTypes.ts every public type
107
+ advanced/ curated barrel re-exporting async + derived + entities + history + middleware
108
+ react/ StoreProvider, StoreContext, createStoreHook, hooks/* (useStore, useEntity, useAsync…)
109
+ vue/ provideStore/injectStore + composables (useStore, useEntity, useDerived, useAsync…)
110
+ next/ bindServerAction (App Router server actions)
63
111
  ```
64
112
 
65
113
  > **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).
@@ -228,8 +276,8 @@ const [state, setState] = useStore();
228
276
  // Single path
229
277
  const [name, setName] = useStore('user.name');
230
278
 
231
- // With default value
232
- const [el, setEl] = useStore(`schema.flat.${id}` as PathOf<MyState>, { defaultValue: {} });
279
+ // Fallback for a possibly-undefined value — destructure a default (no `defaultValue` option):
280
+ const [el = {}] = useStore(`schema.flat.${id}` as PathOf<MyState>);
233
281
 
234
282
  // Dynamic path (function resolves the path string at runtime)
235
283
  const [val, setVal] = useStore(s => `style.platform.${s.displayMode}` as PathOf<MyState>);
@@ -388,6 +436,73 @@ createEntityAdapter<Row>({ selectId: r => r.key, sortComparer: (a, b) => a.name.
388
436
 
389
437
  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.
390
438
 
439
+ ## `createEntityStore` (reactive, O(1) per item)
440
+
441
+ **What it's for:** the same normalized collection, but as a **standalone reactive store** that notifies *per item*. Where `createEntityAdapter` lives inside a `StoreApi` path (one map, one subscription — every consumer wakes when any item changes), `createEntityStore` lets a row subscribe to **just its own id**, so editing one item re-renders only that row, not the list. Use it for large lists / tables where the adapter's whole-map churn shows up.
442
+
443
+ ```ts
444
+ import { createEntityStore } from '@plitzi/nexus';
445
+
446
+ const todos = createEntityStore<Todo>({ selectId: t => t.id });
447
+
448
+ todos.setMany([a, b, c]);
449
+ todos.updateOne('a', { done: true });
450
+ todos.removeOne('b');
451
+
452
+ todos.getOne('a'); // Todo | undefined
453
+ todos.getIds(); // string[]
454
+ todos.getAll(); // Todo[]
455
+ const off = todos.subscribeOne('a', render); // wakes ONLY when item 'a' changes
456
+ ```
457
+
458
+ React — subscribe at the granularity you actually render at:
459
+
460
+ ```tsx
461
+ import { useEntity, useEntityIds, useEntityOne } from '@plitzi/nexus/react';
462
+
463
+ function List() {
464
+ const ids = useEntityIds(todos); // re-renders only on add/remove
465
+ return ids.map(id => <Row key={id} id={id} />);
466
+ }
467
+
468
+ function Row({ id }: { id: string }) {
469
+ const todo = useEntityOne(todos, id); // re-renders only when THIS item changes
470
+ return <li>{todo?.text}</li>;
471
+ }
472
+
473
+ // Or the bound shape: const { useOne, useIds, useAll } = useEntity(todos);
474
+ ```
475
+
476
+ ## `createAsync` / `useAsync` (race-safe fetch)
477
+
478
+ **What it's for:** a fetch whose result lands on a store path, with status tracking and last-write-wins race safety, shared across every consumer. Read it inline (loading / error UI) with `useAsync`, or unwrap the value (Suspense) with `useAsyncValue`.
479
+
480
+ ```ts
481
+ import { createAsync } from '@plitzi/nexus';
482
+
483
+ const user = createAsync(store, 'user', async (id: string) => fetchUser(id));
484
+ user.run('42'); // kicks off; a newer run() supersedes an in-flight one
485
+ user.get(); // { status, data, error }
486
+ ```
487
+
488
+ ```tsx
489
+ import { useAsync, useAsyncValue } from '@plitzi/nexus/react';
490
+
491
+ function Profile() {
492
+ const { status, data, error } = useAsync(user); // re-renders on status / value change
493
+ if (status === 'pending') { return <Spinner />; }
494
+
495
+ return <span>{data?.name}</span>;
496
+ }
497
+
498
+ // Suspense form — throws the promise while pending, returns the value when settled:
499
+ function ProfileSuspense() {
500
+ const data = useAsyncValue(user);
501
+
502
+ return <span>{data.name}</span>;
503
+ }
504
+ ```
505
+
391
506
  ## Middleware (`loggerMiddleware` / `persistMiddleware` / `historyMiddleware`)
392
507
 
393
508
  **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).
@@ -412,6 +527,23 @@ const analytics: StoreMiddleware<State> = api => ({
412
527
  const off = store.subscribeChange(({ path, prev, next }) => {});
413
528
  ```
414
529
 
530
+ ### `cascade` — share a middleware with nested scopes
531
+
532
+ Middlewares are per-store, so a nested `StoreProvider` (or scoped `createStore`) doesn't inherit the parent's
533
+ middlewares by default. Wrap one in `cascade()` to have every descending store get its **own instance** of it —
534
+ set a logger or analytics sink once at the root instead of repeating it in each child.
535
+
536
+ ```ts
537
+ import { createStore, cascade, loggerMiddleware } from '@plitzi/nexus';
538
+
539
+ const root = createStore<State>(initial, {
540
+ middlewares: [cascade(loggerMiddleware())] // child scopes log too, each with its own instance
541
+ });
542
+ ```
543
+
544
+ Storage- or recorder-bound middlewares (`persistMiddleware`, `historyMiddleware`) are deliberately **not**
545
+ cascaded — they're per-store by nature (one storage key, one recorder). Only mark them if you really mean it.
546
+
415
547
  ### Intercepting writes (`beforeChange`)
416
548
 
417
549
  `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.
@@ -489,6 +621,26 @@ reduxDevToolsMiddleware<State>({
489
621
 
490
622
  Intended for the root store; like `persist`/`history` it's per-store and not cascaded.
491
623
 
624
+ ### `createRecorder`
625
+
626
+ A ready-to-use change recorder for building your **own** dev-tools UI — a bounded, subscribable log of committed writes (`{ seq, time, path, prevValue, nextValue }`). Unlike `loggerMiddleware` (fire-and-forget to a sink) it retains recent history and lets a UI read/subscribe; unlike `reduxDevToolsMiddleware` it needs no browser extension. The `getEntries` snapshot is reference-stable between records, so it drops straight into `useSyncExternalStore`.
627
+
628
+ ```tsx
629
+ import { createStore, createRecorder } from '@plitzi/nexus';
630
+ import { useSyncExternalStore } from 'react';
631
+
632
+ const recorder = createRecorder<State>({ max: 100 });
633
+ const store = createStore<State>(initial, { middlewares: [recorder.middleware] });
634
+
635
+ function ChangesPanel() {
636
+ const entries = useSyncExternalStore(recorder.subscribe, recorder.getEntries);
637
+
638
+ return entries.map(e => `${e.path}: ${JSON.stringify(e.prevValue)} → ${JSON.stringify(e.nextValue)}`);
639
+ }
640
+ ```
641
+
642
+ `entriesSince(seq)` / `lastSeq()` correlate writes with an external timeline (e.g. "which writes happened since the last render commit"); `record(change)` feeds it directly so one shared recorder can collect from several stores. Per-store, not cascaded.
643
+
492
644
  ## Time-travel (`historyMiddleware`)
493
645
 
494
646
  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.
@@ -724,6 +876,38 @@ function Likes({ store, updateLikes }: { store: StoreApi<State>; updateLikes: Se
724
876
  This module dynamically imports from `next/cache` — it only works within Next.js App Router.
725
877
  It is **not** a peer dependency, just a compatibility layer.
726
878
 
879
+ ## Vue 3 (`@plitzi/nexus/vue`)
880
+
881
+ The same agnostic core, bound to Vue's reactivity via `provide`/`inject` and composables that return refs.
882
+ Provide a store at a parent, then bind a path two-ways with `useStore` (write through `ref.value` or `v-model`),
883
+ or read-only with `useStoreValue`.
884
+
885
+ ```vue
886
+ <!-- App.vue -->
887
+ <script setup lang="ts">
888
+ import { provideStore } from '@plitzi/nexus/vue';
889
+
890
+ type State = { count: number; user: { name: string } };
891
+ provideStore<State>({ count: 0, user: { name: 'Ada' } });
892
+ </script>
893
+
894
+ <!-- Counter.vue -->
895
+ <script setup lang="ts">
896
+ import { useStore, useStoreValue } from '@plitzi/nexus/vue';
897
+
898
+ const count = useStore<State, 'count'>('count'); // WritableComputedRef — re-renders only on change
899
+ const name = useStoreValue<State, 'user.name'>('user.name'); // read-only ref
900
+ </script>
901
+
902
+ <template>
903
+ <button @click="count++">{{ name }}: {{ count }}</button>
904
+ </template>
905
+ ```
906
+
907
+ `createStoreComposable<State>()` mirrors React's `createStoreHook` — it returns `useStore` / `useStoreValue` /
908
+ `useStoreState` already bound to `State`. Vue also ships `useEntity`, `useDerived`, `useAsync`, and
909
+ `useStoreHistory` composables matching their React counterparts.
910
+
727
911
  ## Performance
728
912
 
729
913
  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.
@@ -751,7 +935,7 @@ All types live in `src/types/StoreTypes.ts`:
751
935
  | `EntityAdapter<T>` | CRUD updaters + selectors from `createEntityAdapter` |
752
936
  | `EntityUpdater<T>` | `(map) => map` — an immutable entity-map update for `setState` |
753
937
  | `SetState<T>` | Full `setState` overload signature |
754
- | `UseStoreOptions` | Options for `useStore` (mode, enabled, equalityFn, defaultValue, transformer) |
938
+ | `UseStoreOptions` | Options for `useStore` (mode, enabled, equalityFn, transformer, store, storeId) |
755
939
  | `UseStoreMultiOptions` | Options for multi-path `useStore` |
756
940
  | `UseStoreSyncOptions` | Options for `useStoreSync` (mode, enabled, syncStrategy) |
757
941
  | `UseStoreSyncMultiOptions` | Options for multi-path `useStoreSync` |
@@ -0,0 +1,5 @@
1
+ export * from '../async';
2
+ export * from '../derived';
3
+ export * from '../entities';
4
+ export * from '../history';
5
+ export * from '../middleware';
@@ -0,0 +1,10 @@
1
+ import { createAsync as e } from "../async/createAsync.mjs";
2
+ import { createDerived as t } from "../derived/createDerived.mjs";
3
+ import { createEntityAdapter as n } from "../entities/createEntityAdapter.mjs";
4
+ import { createEntityStore as r } from "../entities/createEntityStore.mjs";
5
+ import { getStoreHistory as i, historyMiddleware as a } from "../middleware/historyMiddleware.mjs";
6
+ import { loggerMiddleware as o } from "../middleware/loggerMiddleware.mjs";
7
+ import { persistMiddleware as s } from "../middleware/persistMiddleware.mjs";
8
+ import { reduxDevToolsMiddleware as c } from "../middleware/reduxDevToolsMiddleware.mjs";
9
+ import { cascade as l } from "../middleware/cascade.mjs";
10
+ export { l as cascade, e as createAsync, t as createDerived, n as createEntityAdapter, r as createEntityStore, i as getStoreHistory, a as historyMiddleware, o as loggerMiddleware, s as persistMiddleware, c as reduxDevToolsMiddleware };
@@ -1,4 +1,2 @@
1
1
  export { createAsync } from './createAsync';
2
- export { useAsync } from './hooks/useAsync';
3
- export { useAsyncValue } from './hooks/useAsyncValue';
4
2
  export type { AsyncOptions, AsyncResource, AsyncSnapshot, AsyncStatus } from './createAsync';
@@ -1,4 +1,2 @@
1
1
  import { createAsync as e } from "./createAsync.mjs";
2
- import { useAsync as t } from "./hooks/useAsync.mjs";
3
- import { useAsyncValue as n } from "./hooks/useAsyncValue.mjs";
4
- export { e as createAsync, t as useAsync, n as useAsyncValue };
2
+ export { e as createAsync };
@@ -1,7 +1,9 @@
1
- import { GetState, GetterTuple, GetValueFn, GetValueFromBaseFn, MultiPathReturn, PathOf, PathOrFn, PathOrFnSetters, PathOrFnValues, PathSetters, PathSetter, PathValue, PathValues, SetFromBaseFn, SetState, SetStateFn, StoreApi, StoreMiddleware, UseStoreGetterOptions, UseStoreMultiOptions, UseStoreOptions, UseStoreSetterOptions, UseStoreSyncMultiOptions, UseStoreSyncOptions } from '../types';
1
+ import { GetState, SetState, StoreApi, StoreMiddleware } from '../types';
2
2
  export type CreateStoreOptions<TState extends object> = {
3
3
  id?: string;
4
+ scopePath?: string;
4
5
  parent?: StoreApi<TState>;
6
+ exclusive?: ReadonlyArray<string>;
5
7
  middlewares?: StoreMiddleware<TState>[];
6
8
  /** When true, hydrate handlers are collected but NOT run during creation.
7
9
  * Call `store.hydrate()` manually (StoreProvider does this in a useEffect).
@@ -9,46 +11,4 @@ export type CreateStoreOptions<TState extends object> = {
9
11
  deferHydrate?: boolean;
10
12
  };
11
13
  declare function createStore<TState extends object>(initializer: Partial<TState> | ((set: SetState<TState>, get: GetState<TState>) => Partial<TState>), storeOptions?: CreateStoreOptions<TState>): StoreApi<TState>;
12
- export declare const createStoreHook: <TState extends object>() => {
13
- useStore: {
14
- (options?: UseStoreOptions<TState, TState>): [TState, StoreApi<TState>["setState"]];
15
- <P extends PathOf<TState>>(path: P, options?: UseStoreOptions<PathValue<TState, P>, TState> & {
16
- transformer?: never;
17
- }): [PathValue<TState, P>, (value: PathValue<TState, P> | ((prev: PathValue<TState, P>) => PathValue<TState, P>)) => void];
18
- <P extends PathOf<TState>, TResult>(path: P, options: UseStoreOptions<PathValue<TState, P>, TState> & {
19
- transformer: (value: PathValue<TState, P>) => TResult;
20
- }): [TResult, (value: PathValue<TState, P> | ((prev: PathValue<TState, P>) => PathValue<TState, P>)) => void];
21
- <P extends PathOf<TState>>(pathFn: (state: TState) => P, options?: UseStoreOptions<PathValue<TState, P>, TState> & {
22
- transformer?: never;
23
- }): [PathValue<TState, P>, PathSetter<TState, P>];
24
- <P extends PathOf<TState>, TResult_1>(pathFn: (state: TState) => P, options: UseStoreOptions<PathValue<TState, P>, TState> & {
25
- transformer: (value: PathValue<TState, P>) => TResult_1;
26
- }): [TResult_1, PathSetter<TState, P>];
27
- <const Paths extends ReadonlyArray<PathOf<TState>>>(paths: Paths, options?: Omit<UseStoreMultiOptions<TState, Paths>, "transformer">): MultiPathReturn<TState, Paths>;
28
- <const Paths extends ReadonlyArray<PathOf<TState>>, TResult_2>(paths: Paths, options: UseStoreMultiOptions<TState, Paths> & {
29
- transformer: (values: PathValues<TState, Paths>) => TResult_2;
30
- }): [TResult_2, ...PathSetters<TState, Paths>];
31
- <const Entries extends ReadonlyArray<PathOrFn<TState>>>(paths: Entries, options?: Omit<UseStoreMultiOptions<TState, any>, "transformer"> & {
32
- transformer?: never;
33
- }): [PathOrFnValues<TState, Entries>, ...PathOrFnSetters<TState, Entries>];
34
- <const Entries extends ReadonlyArray<PathOrFn<TState>>, TResult_3>(paths: Entries, options: Omit<UseStoreMultiOptions<TState, any>, "transformer"> & {
35
- transformer: (values: PathOrFnValues<TState, Entries>) => TResult_3;
36
- }): [TResult_3, ...PathOrFnSetters<TState, Entries>];
37
- };
38
- useStoreSync: {
39
- (path: undefined, value: TState | Partial<TState>, options?: UseStoreSyncOptions<TState, TState>): void;
40
- <P extends PathOf<TState>>(path: P | ((state: TState) => P), value: PathValue<TState, P>, options?: UseStoreSyncOptions<PathValue<TState, P>, TState>): void;
41
- <const Paths extends ReadonlyArray<PathOf<TState>>>(paths: Paths, values: PathValues<TState, Paths>, options?: UseStoreSyncMultiOptions<TState>): void;
42
- (paths: ReadonlyArray<PathOrFn<TState>>, values: readonly unknown[], options?: UseStoreSyncMultiOptions<TState>): void;
43
- };
44
- useStoreGetter: {
45
- (options?: UseStoreGetterOptions<TState>): GetValueFn<TState>;
46
- <P extends PathOf<TState>>(basePath: P, options?: UseStoreGetterOptions<TState>): GetValueFromBaseFn<PathValue<TState, P>>;
47
- <const Entries extends ReadonlyArray<PathOf<TState> | ((state: TState) => unknown)>>(entries: Entries, options?: UseStoreGetterOptions<TState>): GetterTuple<TState, Entries>;
48
- };
49
- useStoreSetter: {
50
- (options?: UseStoreSetterOptions<TState>): SetStateFn<TState>;
51
- <P extends PathOf<TState>>(basePath: P, options?: UseStoreSetterOptions<TState>): SetFromBaseFn<PathValue<TState, P>>;
52
- };
53
- };
54
14
  export default createStore;