@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.
- package/CHANGELOG.md +39 -0
- package/README.md +211 -27
- package/dist/advanced/index.d.ts +5 -0
- package/dist/advanced/index.mjs +10 -0
- package/dist/async/index.d.ts +0 -2
- package/dist/async/index.mjs +1 -3
- package/dist/createStore/createStore.d.ts +3 -43
- package/dist/createStore/createStore.mjs +85 -100
- package/dist/createStore/helpers/PathTrie.mjs +14 -14
- package/dist/createStore/helpers/createChainReads.d.ts +1 -1
- package/dist/createStore/helpers/createChainReads.mjs +22 -13
- package/dist/createStore/helpers/createSetState.mjs +11 -9
- package/dist/createStore/helpers/forwardParentChanges.mjs +16 -13
- package/dist/createStore/index.mjs +3 -3
- package/dist/derived/index.d.ts +0 -1
- package/dist/derived/index.mjs +1 -2
- package/dist/entities/createEntityStore.d.ts +0 -3
- package/dist/entities/createEntityStore.mjs +71 -78
- package/dist/env.d.ts +4 -0
- package/dist/env.mjs +3 -0
- package/dist/history/index.d.ts +0 -2
- package/dist/history/index.mjs +1 -2
- package/dist/index.d.ts +23 -15
- package/dist/index.mjs +14 -22
- package/dist/middleware/historyMiddleware.mjs +26 -26
- package/dist/middleware/persistMiddleware.mjs +11 -11
- package/dist/middleware/recorderMiddleware.d.ts +21 -0
- package/dist/middleware/recorderMiddleware.mjs +33 -0
- package/dist/{StoreContext.d.ts → react/StoreContext.d.ts} +1 -1
- package/dist/{StoreContext.mjs → react/StoreContext.mjs} +1 -1
- package/dist/{StoreProvider.d.ts → react/StoreProvider.d.ts} +4 -2
- package/dist/react/StoreProvider.mjs +62 -0
- package/dist/react/createStoreHook.d.ts +44 -0
- package/dist/react/createStoreHook.mjs +27 -0
- package/dist/{createStore → react}/hooks/shared.mjs +2 -2
- package/dist/{async → react}/hooks/useAsync.d.ts +1 -1
- package/dist/{async → react}/hooks/useAsync.mjs +1 -1
- package/dist/{async → react}/hooks/useAsyncValue.d.ts +1 -1
- package/dist/{async → react}/hooks/useAsyncValue.mjs +1 -1
- package/dist/{derived → react}/hooks/useDerived.d.ts +1 -1
- package/dist/{derived → react}/hooks/useDerived.mjs +1 -1
- package/dist/react/hooks/useEntity.d.ts +10 -0
- package/dist/react/hooks/useEntity.mjs +23 -0
- package/dist/{createStore → react}/hooks/useStore.mjs +2 -2
- package/dist/{createStore → react}/hooks/useStoreById.mjs +2 -2
- package/dist/{createStore → react}/hooks/useStoreGetter.mjs +1 -1
- package/dist/react/hooks/useStoreHistory.mjs +29 -0
- package/dist/{createStore → react}/hooks/useStoreSetter.mjs +1 -1
- package/dist/{createStore → react}/hooks/useStoreSync.mjs +2 -2
- package/dist/react/index.d.ts +17 -0
- package/dist/react/index.mjs +16 -0
- package/dist/{helpers → react}/useIsomorphicLayoutEffect.mjs +1 -1
- package/dist/types/StoreTypes.d.ts +21 -0
- package/dist/vue/index.d.ts +8 -0
- package/dist/vue/index.mjs +7 -0
- package/dist/vue/injection.d.ts +3 -0
- package/dist/vue/injection.mjs +13 -0
- package/dist/vue/useAsync.d.ts +3 -0
- package/dist/vue/useAsync.mjs +10 -0
- package/dist/vue/useDerived.d.ts +3 -0
- package/dist/vue/useDerived.mjs +10 -0
- package/dist/vue/useEntity.d.ts +11 -0
- package/dist/vue/useEntity.mjs +29 -0
- package/dist/vue/useStore.d.ts +13 -0
- package/dist/vue/useStore.mjs +31 -0
- package/dist/vue/useStoreHistory.d.ts +12 -0
- package/dist/vue/useStoreHistory.mjs +33 -0
- package/logo.svg +77 -22
- package/package.json +133 -56
- package/dist/StoreProvider.mjs +0 -57
- package/dist/history/hooks/useStoreHistory.mjs +0 -24
- /package/dist/{createStore → react}/hooks/shared.d.ts +0 -0
- /package/dist/{createStore → react}/hooks/useStore.d.ts +0 -0
- /package/dist/{createStore → react}/hooks/useStoreById.d.ts +0 -0
- /package/dist/{createStore → react}/hooks/useStoreGetter.d.ts +0 -0
- /package/dist/{history → react}/hooks/useStoreHistory.d.ts +0 -0
- /package/dist/{createStore → react}/hooks/useStoreSetter.d.ts +0 -0
- /package/dist/{createStore → react}/hooks/useStoreSync.d.ts +0 -0
- /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
|
|
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
|
-
|
|
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
|
|
39
|
-
| `
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
232
|
-
const [el
|
|
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,
|
|
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,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 };
|
package/dist/async/index.d.ts
CHANGED
package/dist/async/index.mjs
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import { createAsync as e } from "./createAsync.mjs";
|
|
2
|
-
|
|
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,
|
|
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;
|