@plitzi/nexus 0.31.1 → 0.31.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 +6 -0
- package/README.md +197 -1
- package/dist/StoreProvider.mjs +44 -38
- package/dist/createStore/createStore.d.ts +7 -2
- package/dist/createStore/createStore.mjs +43 -38
- package/dist/createStore/helpers/writeByPath.mjs +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +7 -6
- package/dist/middleware/persistMiddleware.mjs +11 -7
- package/dist/next/index.d.ts +26 -0
- package/dist/next/index.mjs +18 -0
- package/dist/rsc/index.d.ts +29 -0
- package/dist/rsc/index.mjs +18 -0
- package/dist/types/StoreTypes.d.ts +2 -0
- package/logo.svg +57 -0
- package/package.json +14 -10
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./logo.svg" alt="@plitzi/nexus" width="640" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# @plitzi/nexus
|
|
2
6
|
|
|
3
|
-
A lightweight, type-safe React store built on `useSyncExternalStore`. You subscribe to **dot-notation paths** and re-render only when that exact value changes — no selectors, no reducers, no action types. On top of that core it ships scoped stores, time-travel, derived values, an entity adapter, and a middleware pipeline (logger / persist / history).
|
|
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.
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
10
|
npm install @plitzi/nexus # peer deps: react@^18 || ^19, react-dom@^18 || ^19
|
|
@@ -33,6 +37,10 @@ function Counter() {
|
|
|
33
37
|
| `createDerived` / `useDerived` | A memoized value computed from store paths (reselect-style). |
|
|
34
38
|
| `createEntityAdapter` | CRUD updaters + selectors for a normalized `Record<id, T>` map. |
|
|
35
39
|
| `loggerMiddleware` / `persistMiddleware` / `historyMiddleware` / `reduxDevToolsMiddleware` | Middlewares: log, persist to storage, record time-travel, connect to Redux DevTools. |
|
|
40
|
+
| `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. |
|
|
43
|
+
| `bindServerAction` | Optimistic updates + revalidation for Next.js Server Actions. |
|
|
36
44
|
| `getStoreHistory` / `useStoreHistory` | Undo / redo / jump-to-snapshot action log (enabled by `historyMiddleware`). |
|
|
37
45
|
|
|
38
46
|
## Architecture
|
|
@@ -528,6 +536,194 @@ setCodegenEnabled(false);
|
|
|
528
536
|
|
|
529
537
|
Call `setCodegenEnabled(undefined)` to restore auto-detection.
|
|
530
538
|
|
|
539
|
+
## Server Components & SSR
|
|
540
|
+
|
|
541
|
+
Nexus supports React Server Components (App Router) and server-side rendering (Pages Router) with no additional setup.
|
|
542
|
+
|
|
543
|
+
### Auto-detected SSR safety
|
|
544
|
+
|
|
545
|
+
When a store is created on the server (`typeof window === 'undefined'`), Nexus automatically:
|
|
546
|
+
|
|
547
|
+
- **Disables codegen** — `new Function` is never called, so a strict Content-Security-Policy won't break SSR. Falls back to the recursive writer with identical behaviour.
|
|
548
|
+
- **Disables localStorage** — `persistMiddleware` falls back to a no-op storage so `getItem`/`setItem` never throw.
|
|
549
|
+
- **Defers hydration** — `StoreProvider` runs `persistMiddleware` hydration in a `useEffect` after React hydration, preventing hydration mismatches.
|
|
550
|
+
|
|
551
|
+
You don't need to configure anything for SSR to work.
|
|
552
|
+
|
|
553
|
+
### Server Components → Client data handoff
|
|
554
|
+
|
|
555
|
+
Wrap server-fetched data with `createServerSnapshot` to mark it as coming from the server:
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
// app/page.tsx — Server Component (App Router)
|
|
559
|
+
import { StoreProvider } from '@plitzi/nexus';
|
|
560
|
+
import { createServerSnapshot } from '@plitzi/nexus/rsc';
|
|
561
|
+
|
|
562
|
+
export default async function Page() {
|
|
563
|
+
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<StoreProvider value={createServerSnapshot({ posts })}>
|
|
567
|
+
<ClientContent />
|
|
568
|
+
</StoreProvider>
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
// app/ClientContent.tsx — Client Component
|
|
575
|
+
'use client';
|
|
576
|
+
const { useStore } = createStoreHook<{ posts: Post[] }>();
|
|
577
|
+
|
|
578
|
+
function ClientContent() {
|
|
579
|
+
const [posts] = useStore('posts');
|
|
580
|
+
// posts is available without a loading state — it was already fetched on the server
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The SSR flag is a non-enumerable Symbol — invisible in spread/JSON. Using `createServerSnapshot` is optional; plain objects work too.
|
|
585
|
+
|
|
586
|
+
### `rsc` subpath exports
|
|
587
|
+
|
|
588
|
+
| Import | What it's for |
|
|
589
|
+
|---|---|
|
|
590
|
+
| `createServerSnapshot` | Mark data as server-fetched (type-level marker) |
|
|
591
|
+
| `isServerSnapshot` | Check if a value has the SSR flag |
|
|
592
|
+
| `stripServerFlag` | Remove the SSR flag from a value |
|
|
593
|
+
|
|
594
|
+
Import from `@plitzi/nexus/rsc` or from the main entry.
|
|
595
|
+
|
|
596
|
+
### App Router — full pattern
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
// app/providers.tsx
|
|
600
|
+
'use client';
|
|
601
|
+
import { StoreProvider, createServerSnapshot } from '@plitzi/nexus';
|
|
602
|
+
import type { ReactNode } from 'react';
|
|
603
|
+
|
|
604
|
+
type AppState = {
|
|
605
|
+
user: { name: string; email: string } | null;
|
|
606
|
+
theme: 'light' | 'dark';
|
|
607
|
+
posts: Array<{ id: string; title: string }>;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
export function AppProviders({
|
|
611
|
+
children,
|
|
612
|
+
initialState
|
|
613
|
+
}: {
|
|
614
|
+
children: ReactNode;
|
|
615
|
+
initialState: AppState;
|
|
616
|
+
}) {
|
|
617
|
+
return (
|
|
618
|
+
<StoreProvider value={createServerSnapshot(initialState)}>
|
|
619
|
+
{children}
|
|
620
|
+
</StoreProvider>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// app/layout.tsx — Server Component
|
|
625
|
+
import { AppProviders } from './providers';
|
|
626
|
+
|
|
627
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
628
|
+
const user = await getServerSession();
|
|
629
|
+
return (
|
|
630
|
+
<html>
|
|
631
|
+
<body>
|
|
632
|
+
<AppProviders initialState={{ user, theme: 'light', posts: [] }}>
|
|
633
|
+
{children}
|
|
634
|
+
</AppProviders>
|
|
635
|
+
</body>
|
|
636
|
+
</html>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// app/dashboard/page.tsx — Server Component
|
|
641
|
+
export default async function DashboardPage() {
|
|
642
|
+
const posts = await db.posts.findMany();
|
|
643
|
+
return <DashboardClient posts={posts} />;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// app/dashboard/DashboardClient.tsx — Client Component
|
|
647
|
+
'use client';
|
|
648
|
+
import { useStoreSync, createStoreHook } from '@plitzi/nexus';
|
|
649
|
+
|
|
650
|
+
type State = { posts: Array<{ id: string; title: string }> };
|
|
651
|
+
const { useStore } = createStoreHook<State>();
|
|
652
|
+
|
|
653
|
+
export function DashboardClient({ posts }: { posts: State['posts'] }) {
|
|
654
|
+
// Sync server data on mount — avoids overwriting user edits on re-renders
|
|
655
|
+
useStoreSync('posts', posts, { mode: 'mount' });
|
|
656
|
+
const [localPosts] = useStore('posts');
|
|
657
|
+
|
|
658
|
+
return (
|
|
659
|
+
<div>
|
|
660
|
+
{localPosts.map(post => <div key={post.id}>{post.title}</div>)}
|
|
661
|
+
</div>
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Pages Router — full pattern
|
|
667
|
+
|
|
668
|
+
```tsx
|
|
669
|
+
// pages/dashboard.tsx
|
|
670
|
+
import { createStoreHook, StoreProvider } from '@plitzi/nexus';
|
|
671
|
+
import type { InferGetServerSidePropsType } from 'next';
|
|
672
|
+
|
|
673
|
+
type State = { count: number; user: { name: string } };
|
|
674
|
+
|
|
675
|
+
export const getServerSideProps = async () => {
|
|
676
|
+
const user = await api.getUser();
|
|
677
|
+
return { props: { user } };
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
export default function DashboardPage({
|
|
681
|
+
user
|
|
682
|
+
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
|
683
|
+
return (
|
|
684
|
+
<StoreProvider value={{ user, count: 0 }}>
|
|
685
|
+
<Dashboard />
|
|
686
|
+
</StoreProvider>
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// components/Dashboard.tsx
|
|
691
|
+
'use client';
|
|
692
|
+
const { useStore } = createStoreHook<State>();
|
|
693
|
+
|
|
694
|
+
function Dashboard() {
|
|
695
|
+
const [user] = useStore('user');
|
|
696
|
+
const [count, setCount] = useStore('count');
|
|
697
|
+
return <div>...</div>;
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Server Actions (`@plitzi/nexus/next`)
|
|
702
|
+
|
|
703
|
+
The `@plitzi/nexus/next` subpath exports a `bindServerAction` helper for optimistic updates
|
|
704
|
+
with automatic revalidation:
|
|
705
|
+
|
|
706
|
+
```tsx
|
|
707
|
+
'use client';
|
|
708
|
+
import { useStore } from '@plitzi/nexus';
|
|
709
|
+
import { bindServerAction } from '@plitzi/nexus/next';
|
|
710
|
+
|
|
711
|
+
function Likes({ store, updateLikes }: { store: StoreApi<State>; updateLikes: ServerAction }) {
|
|
712
|
+
const [likes, setLikes] = useStore('likes');
|
|
713
|
+
const syncLikes = bindServerAction(store, 'likes', updateLikes, { revalidatePath: '/dashboard' });
|
|
714
|
+
|
|
715
|
+
return <button onClick={() => syncLikes(n => n + 1)}>{likes}</button>;
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
| Import | What it's for |
|
|
720
|
+
|---|---|
|
|
721
|
+
| `bindServerAction` | Optimistic update + rollback + revalidation for Next.js Server Actions |
|
|
722
|
+
| `ServerActionResult` | Return type of `bindServerAction` |
|
|
723
|
+
|
|
724
|
+
This module dynamically imports from `next/cache` — it only works within Next.js App Router.
|
|
725
|
+
It is **not** a peer dependency, just a compatibility layer.
|
|
726
|
+
|
|
531
727
|
## Performance
|
|
532
728
|
|
|
533
729
|
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.
|
package/dist/StoreProvider.mjs
CHANGED
|
@@ -1,51 +1,57 @@
|
|
|
1
1
|
import { StoreContext as e, StoreRegistryContext as t } from "./StoreContext.mjs";
|
|
2
2
|
import n from "./createStore/hooks/useStoreSync.mjs";
|
|
3
3
|
import r from "./createStore/index.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { isServerSnapshot as i, stripServerFlag as a } from "./rsc/index.mjs";
|
|
5
|
+
import { createContext as o, useContext as s, useEffect as c, useMemo as l, useRef as u } from "react";
|
|
6
|
+
import { jsx as d } from "react/jsx-runtime";
|
|
6
7
|
//#region src/StoreProvider.tsx
|
|
7
|
-
var
|
|
8
|
-
let
|
|
9
|
-
let e =
|
|
10
|
-
return typeof m == "function" ? m(e) : {
|
|
8
|
+
var f = o(void 0), p = (e) => e.cascade === !0, m = ({ store: o, id: m, path: h, value: g, inherit: _, autoSync: v = !0, middlewares: y, children: b }) => {
|
|
9
|
+
let x = s(e), S = s(f), C = s(t), w = u(void 0), T = _ === "live", E = l(() => {
|
|
10
|
+
let e = _ === "snapshot" && x ? x.getState() : {}, t = typeof g == "function" ? g(e) : {
|
|
11
11
|
...e,
|
|
12
|
-
...
|
|
12
|
+
...g
|
|
13
13
|
};
|
|
14
|
+
return i(t) ? a(t) : t;
|
|
14
15
|
}, [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
]),
|
|
19
|
-
|
|
20
|
-
id:
|
|
21
|
-
parent:
|
|
22
|
-
middlewares:
|
|
16
|
+
_,
|
|
17
|
+
x,
|
|
18
|
+
g
|
|
19
|
+
]), D = y ?? [], O = S ? [...S, ...D] : D, k = l(() => [...S ?? [], ...D.filter(p)], [S, y]);
|
|
20
|
+
w.current ||= o ?? r(() => E, {
|
|
21
|
+
id: m,
|
|
22
|
+
parent: T ? x : void 0,
|
|
23
|
+
middlewares: O.length > 0 ? O : void 0,
|
|
24
|
+
deferHydrate: !0
|
|
23
25
|
});
|
|
24
|
-
let
|
|
25
|
-
id:
|
|
26
|
-
store:
|
|
27
|
-
parent:
|
|
28
|
-
} :
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}), [
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
26
|
+
let A = l(() => m ? {
|
|
27
|
+
id: m,
|
|
28
|
+
store: w.current,
|
|
29
|
+
parent: C
|
|
30
|
+
} : C, [m, C]);
|
|
31
|
+
u(!1), c(() => {}, [m, C]), c(() => (T && !o && w.current?.reconnect?.(), () => {
|
|
32
|
+
T && !o && w.current?.destroy?.();
|
|
33
|
+
}), [T, o]);
|
|
34
|
+
let j = u(!1);
|
|
35
|
+
c(() => {
|
|
36
|
+
!j.current && w.current?.hydrate && (w.current.hydrate(), j.current = !0);
|
|
37
|
+
}, []);
|
|
38
|
+
let M = !!g && v;
|
|
39
|
+
return n(h, h ? g : E, {
|
|
40
|
+
enabled: M && !!h,
|
|
41
|
+
store: w.current
|
|
42
|
+
}), n(void 0, E, {
|
|
43
|
+
enabled: M && !h,
|
|
44
|
+
store: w.current
|
|
45
|
+
}), /* @__PURE__ */ d(f, {
|
|
46
|
+
value: k.length > 0 ? k : void 0,
|
|
47
|
+
children: /* @__PURE__ */ d(t, {
|
|
48
|
+
value: A,
|
|
49
|
+
children: /* @__PURE__ */ d(e, {
|
|
50
|
+
value: w.current,
|
|
51
|
+
children: b
|
|
46
52
|
})
|
|
47
53
|
})
|
|
48
54
|
});
|
|
49
55
|
};
|
|
50
56
|
//#endregion
|
|
51
|
-
export {
|
|
57
|
+
export { m as default };
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { GetState, GetterTuple, GetValueFn, GetValueFromBaseFn, GetValueFromBaseWithDefaultFn, MultiPathReturn, PathOf, PathOrFn, PathOrFnSetters, PathOrFnValue, PathOrFnValues, PathSetters, PathSetter, PathValue, PathValues, SetFromBaseFn, SetState, SetStateFn, StoreApi, StoreMiddleware, UseStoreGetterOptions, UseStoreMultiOptions, UseStoreOptions, UseStoreSetterOptions, UseStoreSyncMultiOptions, UseStoreSyncOptions } from '../types';
|
|
2
|
-
|
|
2
|
+
export type CreateStoreOptions<TState extends object> = {
|
|
3
3
|
id?: string;
|
|
4
4
|
parent?: StoreApi<TState>;
|
|
5
5
|
middlewares?: StoreMiddleware<TState>[];
|
|
6
|
-
|
|
6
|
+
/** When true, hydrate handlers are collected but NOT run during creation.
|
|
7
|
+
* Call `store.hydrate()` manually (StoreProvider does this in a useEffect).
|
|
8
|
+
* Defaults to false (backward-compatible: standalone usage hydrates synchronously). */
|
|
9
|
+
deferHydrate?: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare function createStore<TState extends object>(initializer: Partial<TState> | ((set: SetState<TState>, get: GetState<TState>) => Partial<TState>), storeOptions?: CreateStoreOptions<TState>): StoreApi<TState>;
|
|
7
12
|
export declare const createStoreHook: <TState extends object>() => {
|
|
8
13
|
useStore: {
|
|
9
14
|
(options?: UseStoreOptions<TState, TState>): [TState, StoreApi<TState>["setState"]];
|
|
@@ -13,9 +13,9 @@ var l = 0;
|
|
|
13
13
|
function u(a, o) {
|
|
14
14
|
let s = {}, c, u = new r(), d = new r(), f = new i(), p = new r(), m = () => {
|
|
15
15
|
p.forEach((e) => e());
|
|
16
|
-
}, h = [], g = [], _ = o?.parent;
|
|
16
|
+
}, h = [], g = [], _ = [], v = o?.parent;
|
|
17
17
|
++l;
|
|
18
|
-
let
|
|
18
|
+
let y = (e, t, n) => {
|
|
19
19
|
if (g.length === 0) throw e;
|
|
20
20
|
let r = {
|
|
21
21
|
error: e,
|
|
@@ -23,62 +23,67 @@ function u(a, o) {
|
|
|
23
23
|
path: n
|
|
24
24
|
};
|
|
25
25
|
for (let e = 0, t = g.length; e < t; e++) g[e](r);
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
let { setState:
|
|
29
|
-
getOwnState:
|
|
30
|
-
getOwnSnapshot:
|
|
26
|
+
}, b = () => s, x = () => c ??= { ...s }, { getState: S, getPath: C, invalidate: w, setActive: T, getMergeCount: E } = e(b, x, v);
|
|
27
|
+
v && T(!1);
|
|
28
|
+
let { setState: D, batch: O } = t({
|
|
29
|
+
getOwnState: b,
|
|
30
|
+
getOwnSnapshot: x,
|
|
31
31
|
setOwnState: (e) => {
|
|
32
|
-
s = e, c = void 0,
|
|
32
|
+
s = e, c = void 0, w();
|
|
33
33
|
},
|
|
34
34
|
mutateOwnKey: (e, t) => {
|
|
35
|
-
s[e] = t,
|
|
35
|
+
s[e] = t, w(), c = void 0;
|
|
36
36
|
},
|
|
37
|
-
parent:
|
|
37
|
+
parent: v,
|
|
38
38
|
listeners: u,
|
|
39
39
|
pathListeners: f,
|
|
40
40
|
changeListeners: d,
|
|
41
41
|
interceptors: h,
|
|
42
|
-
reportError:
|
|
42
|
+
reportError: y,
|
|
43
43
|
invalidateDescendants: m,
|
|
44
44
|
onDelegateToParent: void 0
|
|
45
|
-
}),
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
}, P = () => {
|
|
50
|
-
k && (k.unsubscribe(), k = void 0, A?.(), A = void 0, w(!1));
|
|
45
|
+
}), k = () => {
|
|
46
|
+
w(), m();
|
|
47
|
+
}, A, j, M = !1, N = () => u.length > 0 || f.size > 0 || d.length > 0, P = () => {
|
|
48
|
+
A || !v || (w(), T(!0), A = n(v, u, f, d, S, y, w), j = v.subscribeInvalidate?.(k), d.length > 0 && A.seedBaseline());
|
|
51
49
|
}, F = () => {
|
|
52
|
-
|
|
53
|
-
}, I = (
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
A && (A.unsubscribe(), A = void 0, j?.(), j = void 0, T(!1));
|
|
51
|
+
}, I = () => {
|
|
52
|
+
v && (!M && 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 A?.seedBaseline(), t;
|
|
58
58
|
};
|
|
59
|
-
s = typeof a == "function" ? a(
|
|
60
|
-
let
|
|
59
|
+
s = typeof a == "function" ? a(D, S) : a;
|
|
60
|
+
let V = {
|
|
61
61
|
id: o?.id,
|
|
62
|
-
getState:
|
|
63
|
-
getPath:
|
|
64
|
-
setState:
|
|
65
|
-
batch:
|
|
66
|
-
subscribe:
|
|
67
|
-
subscribePath:
|
|
68
|
-
subscribeChange:
|
|
62
|
+
getState: S,
|
|
63
|
+
getPath: C,
|
|
64
|
+
setState: D,
|
|
65
|
+
batch: O,
|
|
66
|
+
subscribe: R,
|
|
67
|
+
subscribePath: z,
|
|
68
|
+
subscribeChange: B,
|
|
69
69
|
destroy: () => {
|
|
70
|
-
|
|
70
|
+
M = !0, F(), u.clear(), f.clear(), d.clear(), p.clear();
|
|
71
71
|
},
|
|
72
72
|
reconnect: () => {
|
|
73
|
-
|
|
73
|
+
M = !1, I();
|
|
74
74
|
},
|
|
75
|
-
subscribeInvalidate: (e) => p.add(e)
|
|
75
|
+
subscribeInvalidate: (e) => p.add(e),
|
|
76
|
+
hydrate: () => {
|
|
77
|
+
for (let e of _) e();
|
|
78
|
+
}
|
|
76
79
|
};
|
|
77
80
|
if (o?.middlewares) for (let e of o.middlewares) {
|
|
78
|
-
let t = e(
|
|
79
|
-
t?.beforeChange && h.push(t.beforeChange), t?.onChange &&
|
|
81
|
+
let t = e(V);
|
|
82
|
+
t?.beforeChange && h.push(t.beforeChange), t?.onChange && B(t.onChange), t?.onError && g.push(t.onError);
|
|
83
|
+
let n = t?.hydrate;
|
|
84
|
+
n && (o.deferHydrate ? _.push(() => n(V)) : n(V));
|
|
80
85
|
}
|
|
81
|
-
return
|
|
86
|
+
return V;
|
|
82
87
|
}
|
|
83
88
|
var d = () => {
|
|
84
89
|
function e(e, t) {
|
|
@@ -24,6 +24,7 @@ var e = Symbol("unchanged"), t = (e) => /^\d+$/.test(e), n = (e) => Array.isArra
|
|
|
24
24
|
return n += `return ${i};`, Function("o", "v", "f", n);
|
|
25
25
|
}, c, l, u = () => {
|
|
26
26
|
if (l !== void 0) return l;
|
|
27
|
+
if (typeof window > "u") return !1;
|
|
27
28
|
if (c === void 0) try {
|
|
28
29
|
s(["x", "y"])({ x: {} }, 1, !1), c = !0;
|
|
29
30
|
} catch {
|
package/dist/index.d.ts
CHANGED
|
@@ -14,3 +14,4 @@ export * from './createStore/hooks/useStoreById';
|
|
|
14
14
|
export * from './types';
|
|
15
15
|
export { createStore, StoreProvider, useStoreById, useStoreSetter };
|
|
16
16
|
export { setCodegenEnabled } from './createStore/helpers/writeByPath';
|
|
17
|
+
export { createServerSnapshot, isServerSnapshot } from './rsc';
|
package/dist/index.mjs
CHANGED
|
@@ -13,9 +13,10 @@ import { createEntityAdapter as f } from "./entities/createEntityAdapter.mjs";
|
|
|
13
13
|
import { getStoreHistory as p, historyMiddleware as m } from "./middleware/historyMiddleware.mjs";
|
|
14
14
|
import { useStoreHistory as h } from "./history/hooks/useStoreHistory.mjs";
|
|
15
15
|
import g from "./createStore/hooks/useStoreById.mjs";
|
|
16
|
-
import _ from "./
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
16
|
+
import { createServerSnapshot as _, isServerSnapshot as v } from "./rsc/index.mjs";
|
|
17
|
+
import y from "./StoreProvider.mjs";
|
|
18
|
+
import { loggerMiddleware as b } from "./middleware/loggerMiddleware.mjs";
|
|
19
|
+
import { persistMiddleware as x } from "./middleware/persistMiddleware.mjs";
|
|
20
|
+
import { reduxDevToolsMiddleware as S } from "./middleware/reduxDevToolsMiddleware.mjs";
|
|
21
|
+
import { cascade as C } from "./middleware/cascade.mjs";
|
|
22
|
+
export { i as CANCEL, a as StoreContext, y as StoreProvider, o as StoreRegistryContext, C as cascade, e as createAsync, u as createDerived, f as createEntityAdapter, _ as createServerSnapshot, l as createStore, c as createStoreHook, p as getStoreHistory, m as historyMiddleware, v as isServerSnapshot, b as loggerMiddleware, x as persistMiddleware, S as reduxDevToolsMiddleware, r as setCodegenEnabled, t as useAsync, n as useAsyncValue, d as useDerived, g as useStoreById, h as useStoreHistory, s as useStoreSetter };
|
|
@@ -12,15 +12,19 @@ var e = {
|
|
|
12
12
|
i.setItem(n, JSON.stringify(r));
|
|
13
13
|
};
|
|
14
14
|
return (e) => {
|
|
15
|
-
r(e, n, i, o, s, c);
|
|
16
15
|
let t;
|
|
17
|
-
return {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
return {
|
|
17
|
+
hydrate: (e) => {
|
|
18
|
+
r(e, n, i, o, s, c);
|
|
19
|
+
},
|
|
20
|
+
onChange: () => {
|
|
21
|
+
if (l <= 0) {
|
|
22
|
+
u(e);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
clearTimeout(t), t = setTimeout(() => u(e), l);
|
|
21
26
|
}
|
|
22
|
-
|
|
23
|
-
} };
|
|
27
|
+
};
|
|
24
28
|
};
|
|
25
29
|
};
|
|
26
30
|
function r(e, t, n, r, i, a) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PathOf, PathValue, StoreApi } from '../types';
|
|
2
|
+
export type ServerActionResult<TState extends object, P extends PathOf<TState>> = (value: PathValue<TState, P> | ((prev: PathValue<TState, P>) => PathValue<TState, P>)) => Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Binds a Nexus store path to a Next.js Server Action, giving you
|
|
5
|
+
* optimistic updates + automatic revalidation.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* 'use client';
|
|
9
|
+
* import { useStore } from '@plitzi/nexus';
|
|
10
|
+
* import { bindServerAction } from '@plitzi/nexus/next';
|
|
11
|
+
*
|
|
12
|
+
* function Likes({ store, updateLikes }: { store: StoreApi<State>; updateLikes: ServerAction }) {
|
|
13
|
+
* const [likes, setLikes] = useStore('likes');
|
|
14
|
+
* const syncLikes = bindServerAction(store, 'likes', updateLikes, { revalidatePath: '/dashboard' });
|
|
15
|
+
*
|
|
16
|
+
* return <button onClick={() => syncLikes(n => n + 1)}>{likes}</button>;
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ This file imports from `next/cache` dynamically. It only works within
|
|
21
|
+
* Next.js App Router — not a peer dependency, just a compatibility layer.
|
|
22
|
+
*/
|
|
23
|
+
export declare function bindServerAction<TState extends object, P extends PathOf<TState>>(store: StoreApi<TState>, path: P, action: (value: PathValue<TState, P>) => Promise<PathValue<TState, P>>, options?: {
|
|
24
|
+
revalidatePath?: string;
|
|
25
|
+
revalidateTag?: string;
|
|
26
|
+
}): ServerActionResult<TState, P>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/next/index.ts
|
|
2
|
+
function e(e, t, n, r) {
|
|
3
|
+
let i = r?.revalidatePath || r?.revalidateTag;
|
|
4
|
+
return async (a) => {
|
|
5
|
+
let o = e.getPath(t), s = typeof a == "function" ? a(o) : a;
|
|
6
|
+
e.setState(t, s);
|
|
7
|
+
try {
|
|
8
|
+
if (await n(s), i) {
|
|
9
|
+
let e = await import("next/cache");
|
|
10
|
+
r.revalidatePath && e.revalidatePath(r.revalidatePath), r.revalidateTag && e.revalidateTag(r.revalidateTag);
|
|
11
|
+
}
|
|
12
|
+
} catch (n) {
|
|
13
|
+
throw e.setState(t, o), n;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { e as bindServerAction };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare const SSR_FLAG: unique symbol;
|
|
2
|
+
type WithSsrFlag<T> = T & {
|
|
3
|
+
readonly [SSR_FLAG]: true;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Wraps server-fetched data so Nexus knows it came from SSR.
|
|
7
|
+
*
|
|
8
|
+
* Usage in a Server Component (App Router):
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { createServerSnapshot } from '@plitzi/nexus/rsc';
|
|
12
|
+
* import { StoreProvider } from '@plitzi/nexus';
|
|
13
|
+
*
|
|
14
|
+
* export default async function Page() {
|
|
15
|
+
* const data = await fetch('https://api.example.com/user');
|
|
16
|
+
* return (
|
|
17
|
+
* <StoreProvider value={createServerSnapshot(data)}>
|
|
18
|
+
* <ClientComponent />
|
|
19
|
+
* </StoreProvider>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function createServerSnapshot<T extends object>(data: T): WithSsrFlag<T>;
|
|
25
|
+
/** @internal */
|
|
26
|
+
export declare function isServerSnapshot<T>(value: T): value is WithSsrFlag<T>;
|
|
27
|
+
/** @internal Strip the SSR flag from a snapshot (returns a plain object). */
|
|
28
|
+
export declare function stripServerFlag<T extends object>(value: T): T;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/rsc/index.ts
|
|
2
|
+
var e = Symbol("@plitzi/nexus/ssr-snapshot");
|
|
3
|
+
function t(t) {
|
|
4
|
+
return e in t ? t : Object.defineProperty(t, e, {
|
|
5
|
+
value: !0,
|
|
6
|
+
enumerable: !1,
|
|
7
|
+
writable: !1,
|
|
8
|
+
configurable: !1
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function n(t) {
|
|
12
|
+
return typeof t == "object" && !!t && e in t;
|
|
13
|
+
}
|
|
14
|
+
function r(e) {
|
|
15
|
+
return n(e) ? { ...e } : e;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { t as createServerSnapshot, n as isServerSnapshot, r as stripServerFlag };
|
|
@@ -51,6 +51,7 @@ export type StoreMiddlewareHandlers<T> = {
|
|
|
51
51
|
beforeChange?: WriteInterceptor<T>;
|
|
52
52
|
onChange?: ChangeListener<T>;
|
|
53
53
|
onError?: StoreErrorHandler<T>;
|
|
54
|
+
hydrate?: (api: StoreApi<T>) => void;
|
|
54
55
|
};
|
|
55
56
|
export type StoreMiddleware<T extends object> = (api: StoreApi<T>) => StoreMiddlewareHandlers<T> | void;
|
|
56
57
|
export type Listener = (changedPath?: Path) => void;
|
|
@@ -71,6 +72,7 @@ export type StoreApi<T> = {
|
|
|
71
72
|
destroy?: () => void;
|
|
72
73
|
reconnect?: () => void;
|
|
73
74
|
subscribeInvalidate?: (listener: () => void) => () => void;
|
|
75
|
+
hydrate?: () => void;
|
|
74
76
|
};
|
|
75
77
|
export type StoreApiInternal<T> = StoreApi<T> & {
|
|
76
78
|
listeners: Subscribers<Listener>;
|
package/logo.svg
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="180" viewBox="0 0 640 180" fill="none" role="img" aria-label="@plitzi/nexus">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="640" y2="180" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#0c0c12" />
|
|
5
|
+
<stop offset="1" stop-color="#16161f" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="mark" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
+
<stop stop-color="#a78bfa" />
|
|
9
|
+
<stop offset="1" stop-color="#7c3aed" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="word" x1="150" y1="0" x2="560" y2="0" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop stop-color="#ede9fe" />
|
|
13
|
+
<stop offset="1" stop-color="#a78bfa" />
|
|
14
|
+
</linearGradient>
|
|
15
|
+
</defs>
|
|
16
|
+
|
|
17
|
+
<rect width="640" height="180" rx="20" fill="url(#bg)" />
|
|
18
|
+
<rect x="0.5" y="0.5" width="639" height="179" rx="19.5" fill="none" stroke="#2a2a38" />
|
|
19
|
+
|
|
20
|
+
<circle cx="96" cy="90" r="130" fill="#7c3aed" opacity="0.10" />
|
|
21
|
+
|
|
22
|
+
<!-- nexus mark -->
|
|
23
|
+
<g transform="translate(90 90)">
|
|
24
|
+
<rect x="-36" y="-36" width="72" height="72" rx="18" fill="url(#mark)" />
|
|
25
|
+
<g transform="scale(2.0)" stroke="#fff" stroke-linecap="round" stroke-linejoin="round">
|
|
26
|
+
<path d="M0 -8.5 7.4 -4.25 7.4 4.25 0 8.5 -7.4 4.25 -7.4 -4.25 Z" stroke-width="1.3" stroke-opacity="0.45" />
|
|
27
|
+
<g stroke-width="1.5">
|
|
28
|
+
<path d="M0 0 0 -8.5" />
|
|
29
|
+
<path d="M0 0 7.4 -4.25" />
|
|
30
|
+
<path d="M0 0 7.4 4.25" />
|
|
31
|
+
<path d="M0 0 0 8.5" />
|
|
32
|
+
<path d="M0 0 -7.4 4.25" />
|
|
33
|
+
<path d="M0 0 -7.4 -4.25" />
|
|
34
|
+
</g>
|
|
35
|
+
</g>
|
|
36
|
+
<g transform="scale(2.0)" fill="#fff">
|
|
37
|
+
<circle cx="0" cy="-8.5" r="1.7" />
|
|
38
|
+
<circle cx="7.4" cy="-4.25" r="1.7" />
|
|
39
|
+
<circle cx="7.4" cy="4.25" r="1.7" />
|
|
40
|
+
<circle cx="0" cy="8.5" r="1.7" />
|
|
41
|
+
<circle cx="-7.4" cy="4.25" r="1.7" />
|
|
42
|
+
<circle cx="-7.4" cy="-4.25" r="1.7" />
|
|
43
|
+
<circle cx="0" cy="0" r="2.6" />
|
|
44
|
+
</g>
|
|
45
|
+
</g>
|
|
46
|
+
|
|
47
|
+
<!-- wordmark -->
|
|
48
|
+
<text x="158" y="86" font-family="'Inter', system-ui, sans-serif" font-size="46" font-weight="800" letter-spacing="-1" fill="url(#word)">
|
|
49
|
+
@plitzi/nexus
|
|
50
|
+
</text>
|
|
51
|
+
<text x="160" y="120" font-family="'Inter', system-ui, sans-serif" font-size="20" font-weight="500" fill="#a1a1aa">
|
|
52
|
+
A tiny, type-safe React store
|
|
53
|
+
</text>
|
|
54
|
+
<text x="160" y="146" font-family="'JetBrains Mono', ui-monospace, monospace" font-size="15" fill="#8b5cf6">
|
|
55
|
+
path subscriptions · scoped stores · time-travel
|
|
56
|
+
</text>
|
|
57
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/nexus",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"files": [
|
|
6
|
-
"dist"
|
|
6
|
+
"dist",
|
|
7
|
+
"logo.svg"
|
|
7
8
|
],
|
|
8
9
|
"main": "dist/index.js",
|
|
9
10
|
"module": "dist/index.js",
|
|
@@ -86,14 +87,6 @@
|
|
|
86
87
|
"types": "./dist/createStore/helpers/createChainReads.d.ts",
|
|
87
88
|
"import": "./dist/createStore/helpers/createChainReads.mjs"
|
|
88
89
|
},
|
|
89
|
-
"./createStore/helpers/createGetPath": {
|
|
90
|
-
"types": "./dist/createStore/helpers/createGetPath.d.ts",
|
|
91
|
-
"import": "./dist/createStore/helpers/createGetPath.mjs"
|
|
92
|
-
},
|
|
93
|
-
"./createStore/helpers/createGetState": {
|
|
94
|
-
"types": "./dist/createStore/helpers/createGetState.d.ts",
|
|
95
|
-
"import": "./dist/createStore/helpers/createGetState.mjs"
|
|
96
|
-
},
|
|
97
90
|
"./createStore/helpers/createSetState": {
|
|
98
91
|
"types": "./dist/createStore/helpers/createSetState.d.ts",
|
|
99
92
|
"import": "./dist/createStore/helpers/createSetState.mjs"
|
|
@@ -242,6 +235,17 @@
|
|
|
242
235
|
"types": "./dist/middleware/reduxDevToolsMiddleware.d.ts",
|
|
243
236
|
"import": "./dist/middleware/reduxDevToolsMiddleware.mjs"
|
|
244
237
|
},
|
|
238
|
+
"./next": {
|
|
239
|
+
"types": "./dist/next/index.d.ts",
|
|
240
|
+
"import": "./dist/next/index.mjs"
|
|
241
|
+
},
|
|
242
|
+
"./next/index": {
|
|
243
|
+
"types": "./dist/next/index.d.ts",
|
|
244
|
+
"import": "./dist/next/index.mjs"
|
|
245
|
+
},
|
|
246
|
+
"./rsc/index": {
|
|
247
|
+
"import": "./dist/rsc/index.mjs"
|
|
248
|
+
},
|
|
245
249
|
"./types": {
|
|
246
250
|
"types": "./dist/types/index.d.ts",
|
|
247
251
|
"import": "./dist/types/index.mjs"
|