@plitzi/nexus 0.32.1 → 0.32.3

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 +181 -22
  3. package/dist/advanced/index.d.ts +0 -2
  4. package/dist/advanced/index.mjs +10 -16
  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 +73 -97
  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 +26 -17
  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 -16
  24. package/dist/index.mjs +15 -23
  25. package/dist/middleware/historyMiddleware.mjs +31 -31
  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 +3 -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 +127 -58
  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.3
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.32.3
8
+
9
+ ## 0.32.2
10
+
11
+ ### Patch Changes
12
+
13
+ - v0.32.2
14
+
15
+ ## Unreleased
16
+
17
+ ### Minor Changes
18
+
19
+ - Framework-agnostic core. The **package root `@plitzi/nexus` is now the agnostic core** (zero React): `createStore`,
20
+ imperative `get/set/watch`, middlewares, and the async/derived/entities primitives. React bindings live in
21
+ `@plitzi/nexus/react` (`StoreProvider`, `createStoreHook`, all hooks, `useEntity`); Next.js helpers in
22
+ `@plitzi/nexus/next`. Source is organized to match: `src/react/`, `src/next/`, and the agnostic modules at the top
23
+ level.
24
+ - Removed the Vite-only `import.meta.env.MODE`; dev/prod/test detection now uses `process.env.NODE_ENV`, so the
25
+ dev-only warnings behave identically under webpack, esbuild, Rollup, Bun, raw Node and Vite/Astro.
26
+ - Added a first-class **Vue 3 integration** at `@plitzi/nexus/vue`: `provideStore` / `injectStore`, `useStore`
27
+ (writable ref / `v-model`), `useStoreValue`, `createStoreComposable`, `useEntity*`, `useDerived`, `useAsync`, and
28
+ `useStoreHistory`. `vue` is an optional peer dependency (`^3.4`).
29
+ - Added runnable examples (`examples/`) and integration docs (`docs/integrations/`) for React, Next.js, Astro 6 (LTS),
30
+ Astro 7, Vue and Svelte.
31
+
32
+ ### Breaking Changes
33
+
34
+ - **React bindings moved out of the root.** Import `StoreProvider`, `createStoreHook`, `useStore`, `useStoreHistory`,
35
+ etc. from `@plitzi/nexus/react` instead of `@plitzi/nexus` (or the old `/createStore`, `/StoreProvider`,
36
+ `/StoreContext` subpaths). The root now exports only the agnostic core. Agnostic symbols (`createStore`, middlewares,
37
+ `createServerSnapshot`, types) stay on the root.
38
+ - `createEntityStore` is now framework-agnostic and no longer exposes `useOne` / `useIds` / `useAll` on the returned
39
+ object. Use the React bindings instead: `const { useOne, useIds, useAll } = useEntity(store)`, or the standalone
40
+ `useEntityOne(store, id)` / `useEntityIds(store)` / `useEntityAll(store)` from `@plitzi/nexus/react`.
41
+
3
42
  ## 0.32.1
4
43
 
5
44
  ### Patch 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>();
@@ -62,7 +79,7 @@ Also importable from the focused `@plitzi/nexus/advanced` entry point, which kee
62
79
  | `createDerived` / `useDerived` | A memoized value computed from store paths (reselect-style). |
63
80
  | `createEntityAdapter` / `createEntityStore` | CRUD + selectors for a normalized `Record<id, T>` map; the store variant adds O(1) per-item updates. |
64
81
  | `createAsync` / `useAsync` / `useAsyncValue` | Race-safe fetch landed on a path; inline UI or Suspense. |
65
- | `loggerMiddleware` / `persistMiddleware` / `historyMiddleware` / `reduxDevToolsMiddleware` | Middlewares: log, persist to storage, record time-travel, connect to Redux DevTools. |
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. |
66
83
  | `getStoreHistory` / `useStoreHistory` | Undo / redo / jump-to-snapshot action log (enabled by `historyMiddleware`). |
67
84
  | `setCodegenEnabled` | Force on/off the `new Function` codegen path (auto-detects CSP/SSR). |
68
85
  | `createServerSnapshot` / `isServerSnapshot` | Mark server-fetched data for RSC → Client handoff, and check the marker. |
@@ -70,21 +87,27 @@ Also importable from the focused `@plitzi/nexus/advanced` entry point, which kee
70
87
 
71
88
  ## Architecture
72
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
+
73
93
  ```
74
- StoreContext.ts context object (no deps → no cycles)
75
- createStore.ts factory + createStoreHook
76
- StoreProvider.tsx context provider with optional sync
77
- hooks/
78
- useStore.ts subscribe + read
79
- useStoreSync.ts sync external value into the store (write-only)
80
- useStoreGetter.ts non-reactive snapshot getter
81
- useStoreSetter.ts fire-and-forget setter
82
- shared.ts snapshot factories, subscription helpers
83
- derived/ createDerived + useDerived (memoized computed values)
84
- entities/ createEntityAdapter (normalized-map CRUD + selectors)
85
- middleware/ logger / persist / history (all on subscribeChange)
86
- history/ getStoreHistory + useStoreHistory (time-travel, enabled by historyMiddleware)
87
- 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)
88
111
  ```
89
112
 
90
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).
@@ -253,8 +276,8 @@ const [state, setState] = useStore();
253
276
  // Single path
254
277
  const [name, setName] = useStore('user.name');
255
278
 
256
- // With default value
257
- 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>);
258
281
 
259
282
  // Dynamic path (function resolves the path string at runtime)
260
283
  const [val, setVal] = useStore(s => `style.platform.${s.displayMode}` as PathOf<MyState>);
@@ -413,6 +436,73 @@ createEntityAdapter<Row>({ selectId: r => r.key, sortComparer: (a, b) => a.name.
413
436
 
414
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.
415
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
+
416
506
  ## Middleware (`loggerMiddleware` / `persistMiddleware` / `historyMiddleware`)
417
507
 
418
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).
@@ -437,6 +527,23 @@ const analytics: StoreMiddleware<State> = api => ({
437
527
  const off = store.subscribeChange(({ path, prev, next }) => {});
438
528
  ```
439
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
+
440
547
  ### Intercepting writes (`beforeChange`)
441
548
 
442
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.
@@ -514,6 +621,26 @@ reduxDevToolsMiddleware<State>({
514
621
 
515
622
  Intended for the root store; like `persist`/`history` it's per-store and not cascaded.
516
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
+
517
644
  ## Time-travel (`historyMiddleware`)
518
645
 
519
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.
@@ -749,6 +876,38 @@ function Likes({ store, updateLikes }: { store: StoreApi<State>; updateLikes: Se
749
876
  This module dynamically imports from `next/cache` — it only works within Next.js App Router.
750
877
  It is **not** a peer dependency, just a compatibility layer.
751
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
+
752
911
  ## Performance
753
912
 
754
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.
@@ -776,7 +935,7 @@ All types live in `src/types/StoreTypes.ts`:
776
935
  | `EntityAdapter<T>` | CRUD updaters + selectors from `createEntityAdapter` |
777
936
  | `EntityUpdater<T>` | `(map) => map` — an immutable entity-map update for `setState` |
778
937
  | `SetState<T>` | Full `setState` overload signature |
779
- | `UseStoreOptions` | Options for `useStore` (mode, enabled, equalityFn, defaultValue, transformer) |
938
+ | `UseStoreOptions` | Options for `useStore` (mode, enabled, equalityFn, transformer, store, storeId) |
780
939
  | `UseStoreMultiOptions` | Options for multi-path `useStore` |
781
940
  | `UseStoreSyncOptions` | Options for `useStoreSync` (mode, enabled, syncStrategy) |
782
941
  | `UseStoreSyncMultiOptions` | Options for multi-path `useStoreSync` |
@@ -1,5 +1,3 @@
1
- export { default as useStoreById } from '../createStore/hooks/useStoreById';
2
- export { default as useStoreSetter } from '../createStore/hooks/useStoreSetter';
3
1
  export * from '../async';
4
2
  export * from '../derived';
5
3
  export * from '../entities';
@@ -1,16 +1,10 @@
1
- import e from "../createStore/hooks/useStoreById.mjs";
2
- import t from "../createStore/hooks/useStoreSetter.mjs";
3
- import { createAsync as n } from "../async/createAsync.mjs";
4
- import { useAsync as r } from "../async/hooks/useAsync.mjs";
5
- import { useAsyncValue as i } from "../async/hooks/useAsyncValue.mjs";
6
- import { createDerived as a } from "../derived/createDerived.mjs";
7
- import { useDerived as o } from "../derived/hooks/useDerived.mjs";
8
- import { createEntityAdapter as s } from "../entities/createEntityAdapter.mjs";
9
- import { createEntityStore as c } from "../entities/createEntityStore.mjs";
10
- import { getStoreHistory as l, historyMiddleware as u } from "../middleware/historyMiddleware.mjs";
11
- import { useStoreHistory as d } from "../history/hooks/useStoreHistory.mjs";
12
- import { loggerMiddleware as f } from "../middleware/loggerMiddleware.mjs";
13
- import { persistMiddleware as p } from "../middleware/persistMiddleware.mjs";
14
- import { reduxDevToolsMiddleware as m } from "../middleware/reduxDevToolsMiddleware.mjs";
15
- import { cascade as h } from "../middleware/cascade.mjs";
16
- export { h as cascade, n as createAsync, a as createDerived, s as createEntityAdapter, c as createEntityStore, l as getStoreHistory, u as historyMiddleware, f as loggerMiddleware, p as persistMiddleware, m as reduxDevToolsMiddleware, r as useAsync, i as useAsyncValue, o as useDerived, e as useStoreById, d as useStoreHistory, t as useStoreSetter };
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;
@@ -1,131 +1,107 @@
1
- import e from "./hooks/useStoreSetter.mjs";
2
- import t from "./helpers/Subscribers.mjs";
3
- import { createChainReads as n } from "./helpers/createChainReads.mjs";
4
- import { createSetState as r } from "./helpers/createSetState.mjs";
5
- import { forwardParentChanges as i } from "./helpers/forwardParentChanges.mjs";
6
- import a from "./helpers/PathTrie.mjs";
7
- import "./helpers/scopeCollisions.mjs";
8
- import o from "./hooks/useStore.mjs";
9
- import s from "./hooks/useStoreGetter.mjs";
10
- import c from "./hooks/useStoreSync.mjs";
1
+ import e from "./helpers/Subscribers.mjs";
2
+ import { isDev as t, isTest as n } from "../env.mjs";
3
+ import { createChainReads as r } from "./helpers/createChainReads.mjs";
4
+ import { createSetState as i } from "./helpers/createSetState.mjs";
5
+ import { forwardParentChanges as a } from "./helpers/forwardParentChanges.mjs";
6
+ import o from "./helpers/PathTrie.mjs";
7
+ import { createScopeClaims as s } from "./helpers/scopeCollisions.mjs";
11
8
  //#region src/createStore/createStore.ts
12
- var l = 0;
13
- function u(e, o) {
14
- let s = {}, c, u = new t(), d = new t(), f = new a(), p = new t(), m = () => {
15
- p.length !== 0 && p.forEach((e) => e());
16
- }, h = [], g = [], _ = [], v = o?.parent;
17
- ++l;
18
- let y = (e, t, n) => {
19
- if (g.length === 0) throw e;
9
+ var c = 0;
10
+ function l(l, u) {
11
+ let d = {}, f, p = new e(), m = new e(), h = new o(), g = new e(), _ = () => {
12
+ g.length !== 0 && g.forEach((e) => e());
13
+ }, v = [], y = [], b = [], x = u?.parent, S = ++c, C = (e, t, n) => {
14
+ if (y.length === 0) throw e;
20
15
  let r = {
21
16
  error: e,
22
17
  phase: t,
23
18
  path: n
24
19
  };
25
- for (let e = 0, t = g.length; e < t; e++) g[e](r);
26
- }, b = () => s, x = () => c ??= { ...s }, { getState: S, getPath: C, invalidate: w, resetCache: T, getMergeCount: E } = n(b, x, v), D = () => {
27
- w(), m();
28
- }, O, k, A = !1;
29
- v && (T(), k = v.subscribeInvalidate?.(D));
30
- let { setState: j, batch: M } = r({
31
- getOwnState: b,
32
- getOwnSnapshot: x,
20
+ for (let e = 0, t = y.length; e < t; e++) y[e](r);
21
+ }, w = () => d, T = () => f ??= { ...d }, { getState: E, getPath: D, invalidate: O, resetCache: k, getMergeCount: A } = r(w, T, x, u?.exclusive), j = () => {
22
+ O(), _();
23
+ }, M, N, P = !1;
24
+ x && (k(), N = x.subscribeInvalidate?.(j));
25
+ let { setState: F, batch: I } = i({
26
+ getOwnState: w,
27
+ getOwnSnapshot: T,
33
28
  setOwnState: (e) => {
34
- s = e, c = void 0, w();
29
+ d = e, f = void 0, O();
35
30
  },
36
31
  mutateOwnKey: (e, t) => {
37
- s[e] = t, w(), c = void 0;
32
+ d[e] = t, O(), f = void 0;
38
33
  },
39
- parent: v,
40
- listeners: u,
41
- pathListeners: f,
42
- changeListeners: d,
43
- interceptors: h,
44
- reportError: y,
45
- invalidateDescendants: m,
46
- onDelegateToParent: void 0
47
- }), N = () => u.length > 0 || f.size > 0 || d.length > 0, P = () => {
48
- O || !v || (w(), T(), O = i(v, u, f, d, S, y, w), d.length > 0 && O.seedBaseline());
49
- }, F = () => {
50
- O && (O.unsubscribe(), O = void 0, T());
51
- }, I = () => {
52
- v && (!A && N() ? P() : F());
53
- }, L = (e) => (I(), () => {
54
- e(), I();
55
- }), R = (e) => L(u.add(e)), z = (e, t) => L(f.add(e, t)), B = (e) => {
56
- let t = L(d.add(e));
57
- return O?.seedBaseline(), t;
34
+ parent: x,
35
+ listeners: p,
36
+ pathListeners: h,
37
+ changeListeners: m,
38
+ interceptors: v,
39
+ reportError: C,
40
+ invalidateDescendants: _,
41
+ onDelegateToParent: t && x ? (e) => x.scopeClaims?.claimDelegatedWrite(e, S) : void 0
42
+ }), L = () => p.length > 0 || h.size > 0 || m.length > 0, R = () => {
43
+ M || !x || (O(), k(), M = a(x, p, h, m, E, C, O), m.length > 0 && M.seedBaseline());
44
+ }, z = () => {
45
+ M && (M.unsubscribe(), M = void 0, k());
46
+ }, B = () => {
47
+ x && (!P && L() ? R() : z());
48
+ }, V = (e) => (B(), () => {
49
+ e(), B();
50
+ }), H = (e) => V(p.add(e)), U = (e, t) => V(h.add(e, t)), W = (e) => {
51
+ let t = V(m.add(e));
52
+ return M?.seedBaseline(), t;
58
53
  };
59
- s = typeof e == "function" ? e(j, S) : e;
60
- let V = {
61
- id: o?.id,
62
- getState: S,
63
- getPath: C,
64
- setState: j,
65
- get: ((e) => e === void 0 ? S() : C(e)),
66
- set: j,
67
- watch: ((e, t) => typeof e == "function" ? R(e) : z(e, t)),
54
+ d = typeof l == "function" ? l(F, E) : l;
55
+ let G = {
56
+ id: u?.id,
57
+ scopePath: u?.scopePath,
58
+ getState: E,
59
+ getPath: D,
60
+ setState: F,
61
+ get: ((e) => e === void 0 ? E() : D(e)),
62
+ set: F,
63
+ watch: ((e, t) => typeof e == "function" ? H(e) : U(e, t)),
68
64
  withBase: (e) => {
69
- let t = (t, n) => j(t === void 0 ? e : `${e}.${t}`, n);
65
+ let t = (t, n) => F(t === void 0 ? e : `${e}.${t}`, n);
70
66
  return {
71
67
  getState: (t) => {
72
- let n = C(e);
68
+ let n = D(e);
73
69
  return n === void 0 && t !== void 0 ? t : n;
74
70
  },
75
71
  getPath: (t, n) => {
76
- let r = C(`${e}.${t}`);
72
+ let r = D(`${e}.${t}`);
77
73
  return r === void 0 && n !== void 0 ? n : r;
78
74
  },
79
75
  setState: t,
80
- subscribe: (t) => z(e, t),
81
- subscribePath: (t, n) => z(`${e}.${t}`, n),
82
- get: (t) => C(t === void 0 ? e : `${e}.${t}`),
76
+ subscribe: (t) => U(e, t),
77
+ subscribePath: (t, n) => U(`${e}.${t}`, n),
78
+ get: (t) => D(t === void 0 ? e : `${e}.${t}`),
83
79
  set: t,
84
- watch: (t, n) => typeof t == "function" ? z(e, t) : z(`${e}.${t}`, n)
80
+ watch: (t, n) => typeof t == "function" ? U(e, t) : U(`${e}.${t}`, n)
85
81
  };
86
82
  },
87
- batch: M,
88
- subscribe: R,
89
- subscribePath: z,
90
- subscribeChange: B,
83
+ batch: I,
84
+ subscribe: H,
85
+ subscribePath: U,
86
+ subscribeChange: W,
91
87
  destroy: () => {
92
- A = !0, F(), u.clear(), f.clear(), d.clear(), p.clear(), k?.();
88
+ P = !0, z(), p.clear(), h.clear(), m.clear(), g.clear(), N?.();
93
89
  },
94
90
  reconnect: () => {
95
- A = !1, v && (k?.(), k = v.subscribeInvalidate?.(D)), I();
91
+ P = !1, x && (N?.(), N = x.subscribeInvalidate?.(j)), B();
96
92
  },
97
- subscribeInvalidate: (e) => p.add(e),
93
+ subscribeInvalidate: (e) => g.add(e),
98
94
  hydrate: () => {
99
- for (let e of _) e();
95
+ for (let e of b) e();
100
96
  }
101
97
  };
102
- if (o?.middlewares) for (let e of o.middlewares) {
103
- let t = e(V);
104
- t?.beforeChange && h.push(t.beforeChange), t?.onChange && B(t.onChange), t?.onError && g.push(t.onError);
98
+ if (u?.middlewares) for (let e of u.middlewares) {
99
+ let t = e(G);
100
+ t?.beforeChange && v.push(t.beforeChange), t?.onChange && W(t.onChange), t?.onError && y.push(t.onError);
105
101
  let n = t?.hydrate;
106
- n && (o.deferHydrate ? _.push(() => n(V)) : n(V));
102
+ n && (u.deferHydrate ? b.push(() => n(G)) : n(G));
107
103
  }
108
- return V;
104
+ return t && (G.scopeClaims = s()), n && (G.listeners = p, G.pathListeners = h, G.changeListeners = m, G.interceptors = v, G.errorHandlers = y, G.invalidateListeners = g, G.getMergeCount = A), G;
109
105
  }
110
- var d = () => {
111
- function t(e, t) {
112
- return o(e, t);
113
- }
114
- function n(e, t, n) {
115
- c(e, t, n);
116
- }
117
- function r(e, t) {
118
- return s(e, t);
119
- }
120
- function i(t, n) {
121
- return e(t, n);
122
- }
123
- return {
124
- useStore: t,
125
- useStoreSync: n,
126
- useStoreGetter: r,
127
- useStoreSetter: i
128
- };
129
- };
130
106
  //#endregion
131
- export { d as createStoreHook, u as default };
107
+ export { l as default };