@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @plitzi/nexus
2
2
 
3
+ ## 0.31.2
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.31.2
8
+
3
9
  ## 0.31.1
4
10
 
5
11
  ### Patch Changes
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.
@@ -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 { createContext as i, useContext as a, useEffect as o, useMemo as s, useRef as c } from "react";
5
- import { jsx as l } from "react/jsx-runtime";
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 u = i(void 0), d = (e) => e.cascade === !0, f = ({ store: i, id: f, path: p, value: m, inherit: h, autoSync: g = !0, middlewares: _, children: v }) => {
8
- let y = a(e), b = a(u), x = a(t), S = c(void 0), C = h === "live", w = s(() => {
9
- let e = h === "snapshot" && y ? y.getState() : {};
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
- ...m
12
+ ...g
13
13
  };
14
+ return i(t) ? a(t) : t;
14
15
  }, [
15
- h,
16
- y,
17
- m
18
- ]), T = _ ?? [], E = b ? [...b, ...T] : T, D = s(() => [...b ?? [], ...T.filter(d)], [b, _]);
19
- S.current ||= i ?? r(() => w, {
20
- id: f,
21
- parent: C ? y : void 0,
22
- middlewares: E.length > 0 ? E : void 0
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 O = s(() => f ? {
25
- id: f,
26
- store: S.current,
27
- parent: x
28
- } : x, [f, x]);
29
- c(!1), o(() => {}, [f, x]), o(() => (C && !i && S.current?.reconnect?.(), () => {
30
- C && !i && S.current?.destroy?.();
31
- }), [C, i]);
32
- let k = !!m && g;
33
- return n(p, p ? m : w, {
34
- enabled: k && !!p,
35
- store: S.current
36
- }), n(void 0, w, {
37
- enabled: k && !p,
38
- store: S.current
39
- }), /* @__PURE__ */ l(u, {
40
- value: D.length > 0 ? D : void 0,
41
- children: /* @__PURE__ */ l(t, {
42
- value: O,
43
- children: /* @__PURE__ */ l(e, {
44
- value: S.current,
45
- children: v
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 { f as default };
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
- declare function createStore<TState extends object>(initializer: Partial<TState> | ((set: SetState<TState>, get: GetState<TState>) => Partial<TState>), storeOptions?: {
2
+ export type CreateStoreOptions<TState extends object> = {
3
3
  id?: string;
4
4
  parent?: StoreApi<TState>;
5
5
  middlewares?: StoreMiddleware<TState>[];
6
- }): StoreApi<TState>;
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 v = (e, t, n) => {
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
- }, y = () => s, b = () => c ??= { ...s }, { getState: x, getPath: S, invalidate: C, setActive: w, getMergeCount: T } = e(y, b, _);
27
- _ && w(!1);
28
- let { setState: E, batch: D } = t({
29
- getOwnState: y,
30
- getOwnSnapshot: b,
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, C();
32
+ s = e, c = void 0, w();
33
33
  },
34
34
  mutateOwnKey: (e, t) => {
35
- s[e] = t, C(), c = void 0;
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: v,
42
+ reportError: y,
43
43
  invalidateDescendants: m,
44
44
  onDelegateToParent: void 0
45
- }), O = () => {
46
- C(), m();
47
- }, k, A, j = !1, M = () => u.length > 0 || f.size > 0 || d.length > 0, N = () => {
48
- k || !_ || (C(), w(!0), k = n(_, u, f, d, x, v, C), A = _.subscribeInvalidate?.(O), d.length > 0 && k.seedBaseline());
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
- _ && (!j && M() ? N() : P());
53
- }, I = (e) => (F(), () => {
54
- e(), F();
55
- }), L = (e) => I(u.add(e)), R = (e, t) => I(f.add(e, t)), z = (e) => {
56
- let t = I(d.add(e));
57
- return k?.seedBaseline(), t;
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(E, x) : a;
60
- let B = {
59
+ s = typeof a == "function" ? a(D, S) : a;
60
+ let V = {
61
61
  id: o?.id,
62
- getState: x,
63
- getPath: S,
64
- setState: E,
65
- batch: D,
66
- subscribe: L,
67
- subscribePath: R,
68
- subscribeChange: z,
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
- j = !0, P(), u.clear(), f.clear(), d.clear(), p.clear();
70
+ M = !0, F(), u.clear(), f.clear(), d.clear(), p.clear();
71
71
  },
72
72
  reconnect: () => {
73
- j = !1, F();
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(B);
79
- t?.beforeChange && h.push(t.beforeChange), t?.onChange && z(t.onChange), t?.onError && g.push(t.onError);
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 B;
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 "./StoreProvider.mjs";
17
- import { loggerMiddleware as v } from "./middleware/loggerMiddleware.mjs";
18
- import { persistMiddleware as y } from "./middleware/persistMiddleware.mjs";
19
- import { reduxDevToolsMiddleware as b } from "./middleware/reduxDevToolsMiddleware.mjs";
20
- import { cascade as x } from "./middleware/cascade.mjs";
21
- export { i as CANCEL, a as StoreContext, _ as StoreProvider, o as StoreRegistryContext, x as cascade, e as createAsync, u as createDerived, f as createEntityAdapter, l as createStore, c as createStoreHook, p as getStoreHistory, m as historyMiddleware, v as loggerMiddleware, y as persistMiddleware, b as reduxDevToolsMiddleware, r as setCodegenEnabled, t as useAsync, n as useAsyncValue, d as useDerived, g as useStoreById, h as useStoreHistory, s as useStoreSetter };
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 { onChange: () => {
18
- if (l <= 0) {
19
- u(e);
20
- return;
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
- clearTimeout(t), t = setTimeout(() => u(e), l);
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.1",
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"