@nexus_js/runtime 0.6.0
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/cache.d.ts +78 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +132 -0
- package/dist/cache.js.map +1 -0
- package/dist/dev-mode.d.ts +60 -0
- package/dist/dev-mode.d.ts.map +1 -0
- package/dist/dev-mode.js +89 -0
- package/dist/dev-mode.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/island.d.ts +29 -0
- package/dist/island.d.ts.map +1 -0
- package/dist/island.js +161 -0
- package/dist/island.js.map +1 -0
- package/dist/navigation.d.ts +97 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +390 -0
- package/dist/navigation.js.map +1 -0
- package/dist/optimistic.d.ts +67 -0
- package/dist/optimistic.d.ts.map +1 -0
- package/dist/optimistic.js +104 -0
- package/dist/optimistic.js.map +1 -0
- package/dist/prefetch-ai.d.ts +88 -0
- package/dist/prefetch-ai.d.ts.map +1 -0
- package/dist/prefetch-ai.js +277 -0
- package/dist/prefetch-ai.js.map +1 -0
- package/dist/runes.d.ts +61 -0
- package/dist/runes.d.ts.map +1 -0
- package/dist/runes.js +155 -0
- package/dist/runes.js.map +1 -0
- package/dist/store.d.ts +123 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +267 -0
- package/dist/store.js.map +1 -0
- package/dist/sync.d.ts +49 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +156 -0
- package/dist/sync.js.map +1 -0
- package/package.json +63 -0
package/dist/runes.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Runes — Fine-grained reactive primitives inspired by Svelte 5.
|
|
3
|
+
* Zero Virtual DOM. The runtime surgically updates only what changed.
|
|
4
|
+
*/
|
|
5
|
+
// ── Internal tracking context ─────────────────────────────────────────────
|
|
6
|
+
let currentEffect = null;
|
|
7
|
+
let idCounter = 0;
|
|
8
|
+
// ── $state ────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Creates a reactive signal. Reading `.value` inside an `$effect` or
|
|
11
|
+
* `$derived` automatically creates a dependency subscription.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const count = $state(0);
|
|
15
|
+
* count.value++; // triggers all subscribers
|
|
16
|
+
*/
|
|
17
|
+
export function $state(initial) {
|
|
18
|
+
const node = {
|
|
19
|
+
value: initial,
|
|
20
|
+
subscribers: new Set(),
|
|
21
|
+
id: idCounter++,
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
get value() {
|
|
25
|
+
if (currentEffect) {
|
|
26
|
+
node.subscribers.add(currentEffect);
|
|
27
|
+
currentEffect.deps.add(node);
|
|
28
|
+
}
|
|
29
|
+
return node.value;
|
|
30
|
+
},
|
|
31
|
+
set value(next) {
|
|
32
|
+
if (Object.is(node.value, next))
|
|
33
|
+
return;
|
|
34
|
+
node.value = next;
|
|
35
|
+
// Notify all subscribers synchronously (microtask queue in practice)
|
|
36
|
+
for (const effect of [...node.subscribers]) {
|
|
37
|
+
effect.cleanup?.();
|
|
38
|
+
effect.execute();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// ── $derived ──────────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Creates a computed value that automatically updates when its dependencies
|
|
46
|
+
* change. Computed lazily — only recalculates when accessed after a change.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const count = $state(0);
|
|
50
|
+
* const doubled = $derived(() => count.value * 2);
|
|
51
|
+
* console.log(doubled.value); // 0
|
|
52
|
+
* count.value = 5;
|
|
53
|
+
* console.log(doubled.value); // 10
|
|
54
|
+
*/
|
|
55
|
+
export function $derived(computation) {
|
|
56
|
+
const signal = $state(computation());
|
|
57
|
+
let dirty = false;
|
|
58
|
+
$effect(() => {
|
|
59
|
+
const next = computation();
|
|
60
|
+
if (!Object.is(signal.value, next)) {
|
|
61
|
+
signal.value = next;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
get value() {
|
|
66
|
+
return signal.value;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// ── $effect ───────────────────────────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Runs a side effect whenever its reactive dependencies change.
|
|
73
|
+
* Returns a cleanup function to stop tracking.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const count = $state(0);
|
|
77
|
+
* $effect(() => {
|
|
78
|
+
* document.title = `Count: ${count.value}`;
|
|
79
|
+
* });
|
|
80
|
+
*/
|
|
81
|
+
export function $effect(fn) {
|
|
82
|
+
const node = {
|
|
83
|
+
execute: () => {
|
|
84
|
+
// Clean up previous subscriptions
|
|
85
|
+
for (const dep of node.deps) {
|
|
86
|
+
dep.subscribers.delete(node);
|
|
87
|
+
}
|
|
88
|
+
node.deps.clear();
|
|
89
|
+
const prev = currentEffect;
|
|
90
|
+
currentEffect = node;
|
|
91
|
+
try {
|
|
92
|
+
node.cleanup = fn();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
currentEffect = prev;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
cleanup: undefined,
|
|
99
|
+
deps: new Set(),
|
|
100
|
+
};
|
|
101
|
+
node.execute();
|
|
102
|
+
return () => {
|
|
103
|
+
node.cleanup?.();
|
|
104
|
+
for (const dep of node.deps) {
|
|
105
|
+
dep.subscribers.delete(node);
|
|
106
|
+
}
|
|
107
|
+
node.deps.clear();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// ── $props ────────────────────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Declares component props with optional defaults.
|
|
113
|
+
* Props are reactive — parent changes flow down automatically.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* const { name, age = 18 } = $props<{ name: string; age?: number }>();
|
|
117
|
+
*/
|
|
118
|
+
export function $props(defaults = {}) {
|
|
119
|
+
// In island context, props are injected from the server manifest.
|
|
120
|
+
// This is a typed accessor with defaults.
|
|
121
|
+
return new Proxy(defaults, {
|
|
122
|
+
get(target, key) {
|
|
123
|
+
return target[key];
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// ── Batch updates ─────────────────────────────────────────────────────────
|
|
128
|
+
let batchDepth = 0;
|
|
129
|
+
const pendingEffects = new Set();
|
|
130
|
+
/**
|
|
131
|
+
* Batches multiple state updates into a single re-render pass.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* batch(() => {
|
|
135
|
+
* x.value = 1;
|
|
136
|
+
* y.value = 2; // only one re-render happens
|
|
137
|
+
* });
|
|
138
|
+
*/
|
|
139
|
+
export function batch(fn) {
|
|
140
|
+
batchDepth++;
|
|
141
|
+
try {
|
|
142
|
+
fn();
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
batchDepth--;
|
|
146
|
+
if (batchDepth === 0) {
|
|
147
|
+
for (const effect of pendingEffects) {
|
|
148
|
+
effect.cleanup?.();
|
|
149
|
+
effect.execute();
|
|
150
|
+
}
|
|
151
|
+
pendingEffects.clear();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=runes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runes.js","sourceRoot":"","sources":["../src/runes.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Global State Store — Hydration Miss = 0.
|
|
3
|
+
*
|
|
4
|
+
* THE PARADOX:
|
|
5
|
+
* Island A (cart) lives in the root layout.
|
|
6
|
+
* User adds item → cart.$state updates to [item1, item2].
|
|
7
|
+
* User navigates to /checkout.
|
|
8
|
+
* The server renders /checkout fresh — it sends cart as [].
|
|
9
|
+
* DOM morphing preserves the island element... but what about the state?
|
|
10
|
+
*
|
|
11
|
+
* THE SOLUTION:
|
|
12
|
+
* A two-layer state synchronization system:
|
|
13
|
+
*
|
|
14
|
+
* Layer 1 — Island Registry:
|
|
15
|
+
* Every island that survives navigation keeps its live $state signals.
|
|
16
|
+
* The morphing algorithm skips re-hydration for preserved islands,
|
|
17
|
+
* so their in-memory $state is never touched. ✓
|
|
18
|
+
*
|
|
19
|
+
* Layer 2 — Cross-navigation Store (this file):
|
|
20
|
+
* For islands that DON'T survive navigation (they're in the new page,
|
|
21
|
+
* not the shared layout), their last known state is checkpointed
|
|
22
|
+
* into this store before navigation. When the new page mounts,
|
|
23
|
+
* the store re-injects the state as initial props.
|
|
24
|
+
*
|
|
25
|
+
* Layer 3 — Serialized Snapshot:
|
|
26
|
+
* On navigation, the store serializes its state to sessionStorage
|
|
27
|
+
* using @nexus_js/serialize (handles Date, Map, Set, BigInt).
|
|
28
|
+
* This survives hard refreshes within the same session.
|
|
29
|
+
*
|
|
30
|
+
* USAGE:
|
|
31
|
+
* // In a cart island — auto-persists across navigation
|
|
32
|
+
* const cart = useStore('cart', {
|
|
33
|
+
* default: [],
|
|
34
|
+
* persist: 'session', // optional: also write to sessionStorage
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // cart.value is automatically restored after navigation
|
|
38
|
+
* cart.value.push(newItem);
|
|
39
|
+
*
|
|
40
|
+
* INJECTION FLOW:
|
|
41
|
+
* 1. Island registers with store via useStore('cart', ...)
|
|
42
|
+
* 2. Before navigation: store.snapshot() → serializes to sessionStorage
|
|
43
|
+
* 3. After navigation: new islands read from store.get('cart')
|
|
44
|
+
* 4. If value exists in store → use it (no hydration miss)
|
|
45
|
+
* 5. If not → use server-provided props (normal flow)
|
|
46
|
+
*/
|
|
47
|
+
export interface StoreOptions<T> {
|
|
48
|
+
default: T;
|
|
49
|
+
/**
|
|
50
|
+
* Persistence strategy:
|
|
51
|
+
* 'memory' — lives until page close (default)
|
|
52
|
+
* 'session' — persists in sessionStorage across navigations
|
|
53
|
+
* 'url' — encodes in URL hash (shareable, no sensitive data)
|
|
54
|
+
*/
|
|
55
|
+
persist?: 'memory' | 'session' | 'url';
|
|
56
|
+
/**
|
|
57
|
+
* Version token — bump this to invalidate stored state.
|
|
58
|
+
* Useful after schema changes that break deserialization.
|
|
59
|
+
*/
|
|
60
|
+
version?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface StoreEntry<T> {
|
|
63
|
+
value: T;
|
|
64
|
+
/** Updates the value and notifies subscribers */
|
|
65
|
+
set: (v: T) => void;
|
|
66
|
+
/** Resets to the initial default value */
|
|
67
|
+
reset: () => void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Creates or retrieves a store entry by key.
|
|
71
|
+
* The returned signal is reactive — binding it in an island makes that
|
|
72
|
+
* island automatically update when any component calls store.set().
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // In cart island
|
|
76
|
+
* const cart = useStore('cart', { default: [], persist: 'session' });
|
|
77
|
+
* cart.value.push({ id: 1, name: 'Widget' });
|
|
78
|
+
*
|
|
79
|
+
* // In any other island or the navigation system
|
|
80
|
+
* const cart = useStore('cart', { default: [] });
|
|
81
|
+
* console.log(cart.value); // [{ id: 1, name: 'Widget' }]
|
|
82
|
+
*/
|
|
83
|
+
export declare function useStore<T>(key: string, opts: StoreOptions<T>): StoreEntry<T>;
|
|
84
|
+
/**
|
|
85
|
+
* Snapshots the entire store to sessionStorage.
|
|
86
|
+
* Called by the navigation system BEFORE performing a navigation.
|
|
87
|
+
* This guarantees that state from any island is preserved across routes.
|
|
88
|
+
*/
|
|
89
|
+
export declare function snapshotStore(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Reads a value from the store by key.
|
|
92
|
+
* Used by the navigation system to inject state into new islands.
|
|
93
|
+
*/
|
|
94
|
+
export declare function readStore<T>(key: string): T | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Writes a value into the store.
|
|
97
|
+
* Triggers reactivity for any island bound to this key.
|
|
98
|
+
*/
|
|
99
|
+
export declare function writeStore<T>(key: string, value: T): void;
|
|
100
|
+
/**
|
|
101
|
+
* Returns a serialized snapshot of all persisted store values.
|
|
102
|
+
* Embedded by the server into the navigation response payload,
|
|
103
|
+
* so the new page can initialize islands with correct state.
|
|
104
|
+
*/
|
|
105
|
+
export declare function exportStore(): string;
|
|
106
|
+
/**
|
|
107
|
+
* Hydrates the store from a server-provided snapshot.
|
|
108
|
+
* Called on new page initialization before islands mount.
|
|
109
|
+
*/
|
|
110
|
+
export declare function importStore(serialized: string): void;
|
|
111
|
+
/**
|
|
112
|
+
* Clears all store entries. Useful in tests or on logout.
|
|
113
|
+
*/
|
|
114
|
+
export declare function clearStore(keys?: string[]): void;
|
|
115
|
+
/**
|
|
116
|
+
* Returns debug info about all active store entries.
|
|
117
|
+
*/
|
|
118
|
+
export declare function storeDebugInfo(): Record<string, {
|
|
119
|
+
value: unknown;
|
|
120
|
+
persist: string;
|
|
121
|
+
hasSignal: boolean;
|
|
122
|
+
}>;
|
|
123
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX;;;;;OAKG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC;IACT,iDAAiD;IACjD,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;IACpB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA4BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA0C7E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAWpC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAKvD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAQzD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,MAAM,CASpC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CASpD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAahD;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE;IAC/C,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC,CAUD"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Global State Store — Hydration Miss = 0.
|
|
3
|
+
*
|
|
4
|
+
* THE PARADOX:
|
|
5
|
+
* Island A (cart) lives in the root layout.
|
|
6
|
+
* User adds item → cart.$state updates to [item1, item2].
|
|
7
|
+
* User navigates to /checkout.
|
|
8
|
+
* The server renders /checkout fresh — it sends cart as [].
|
|
9
|
+
* DOM morphing preserves the island element... but what about the state?
|
|
10
|
+
*
|
|
11
|
+
* THE SOLUTION:
|
|
12
|
+
* A two-layer state synchronization system:
|
|
13
|
+
*
|
|
14
|
+
* Layer 1 — Island Registry:
|
|
15
|
+
* Every island that survives navigation keeps its live $state signals.
|
|
16
|
+
* The morphing algorithm skips re-hydration for preserved islands,
|
|
17
|
+
* so their in-memory $state is never touched. ✓
|
|
18
|
+
*
|
|
19
|
+
* Layer 2 — Cross-navigation Store (this file):
|
|
20
|
+
* For islands that DON'T survive navigation (they're in the new page,
|
|
21
|
+
* not the shared layout), their last known state is checkpointed
|
|
22
|
+
* into this store before navigation. When the new page mounts,
|
|
23
|
+
* the store re-injects the state as initial props.
|
|
24
|
+
*
|
|
25
|
+
* Layer 3 — Serialized Snapshot:
|
|
26
|
+
* On navigation, the store serializes its state to sessionStorage
|
|
27
|
+
* using @nexus_js/serialize (handles Date, Map, Set, BigInt).
|
|
28
|
+
* This survives hard refreshes within the same session.
|
|
29
|
+
*
|
|
30
|
+
* USAGE:
|
|
31
|
+
* // In a cart island — auto-persists across navigation
|
|
32
|
+
* const cart = useStore('cart', {
|
|
33
|
+
* default: [],
|
|
34
|
+
* persist: 'session', // optional: also write to sessionStorage
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // cart.value is automatically restored after navigation
|
|
38
|
+
* cart.value.push(newItem);
|
|
39
|
+
*
|
|
40
|
+
* INJECTION FLOW:
|
|
41
|
+
* 1. Island registers with store via useStore('cart', ...)
|
|
42
|
+
* 2. Before navigation: store.snapshot() → serializes to sessionStorage
|
|
43
|
+
* 3. After navigation: new islands read from store.get('cart')
|
|
44
|
+
* 4. If value exists in store → use it (no hydration miss)
|
|
45
|
+
* 5. If not → use server-provided props (normal flow)
|
|
46
|
+
*/
|
|
47
|
+
import { $state, $effect } from './runes.js';
|
|
48
|
+
import { serialize, deserialize } from '@nexus_js/serialize';
|
|
49
|
+
const STORE_SESSION_KEY = '__nx_store__';
|
|
50
|
+
const STORE_VERSION_KEY = '__nx_store_version__';
|
|
51
|
+
const CURRENT_VERSION = 1;
|
|
52
|
+
// ── In-memory signal registry ─────────────────────────────────────────────────
|
|
53
|
+
const signals = new Map();
|
|
54
|
+
const defaults = new Map();
|
|
55
|
+
const persistModes = new Map();
|
|
56
|
+
// ── Initialize from sessionStorage on module load ─────────────────────────────
|
|
57
|
+
let _sessionData = {};
|
|
58
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
59
|
+
try {
|
|
60
|
+
const version = sessionStorage.getItem(STORE_VERSION_KEY);
|
|
61
|
+
if (version && parseInt(version, 10) === CURRENT_VERSION) {
|
|
62
|
+
const raw = sessionStorage.getItem(STORE_SESSION_KEY) ?? '{}';
|
|
63
|
+
_sessionData = deserialize(raw);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
_sessionData = {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Creates or retrieves a store entry by key.
|
|
73
|
+
* The returned signal is reactive — binding it in an island makes that
|
|
74
|
+
* island automatically update when any component calls store.set().
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // In cart island
|
|
78
|
+
* const cart = useStore('cart', { default: [], persist: 'session' });
|
|
79
|
+
* cart.value.push({ id: 1, name: 'Widget' });
|
|
80
|
+
*
|
|
81
|
+
* // In any other island or the navigation system
|
|
82
|
+
* const cart = useStore('cart', { default: [] });
|
|
83
|
+
* console.log(cart.value); // [{ id: 1, name: 'Widget' }]
|
|
84
|
+
*/
|
|
85
|
+
export function useStore(key, opts) {
|
|
86
|
+
const persist = opts.persist ?? 'memory';
|
|
87
|
+
persistModes.set(key, persist);
|
|
88
|
+
defaults.set(key, opts.default);
|
|
89
|
+
// Determine initial value (priority: session → url → default)
|
|
90
|
+
let initial = opts.default;
|
|
91
|
+
if (persist === 'session' && key in _sessionData) {
|
|
92
|
+
initial = _sessionData[key];
|
|
93
|
+
}
|
|
94
|
+
else if (persist === 'url') {
|
|
95
|
+
const urlVal = readFromURL(key);
|
|
96
|
+
if (urlVal !== null)
|
|
97
|
+
initial = urlVal;
|
|
98
|
+
}
|
|
99
|
+
// Reuse existing signal if already registered (cross-island sharing)
|
|
100
|
+
if (!signals.has(key)) {
|
|
101
|
+
signals.set(key, $state(initial));
|
|
102
|
+
}
|
|
103
|
+
const signal = signals.get(key);
|
|
104
|
+
// Sync to sessionStorage on every change
|
|
105
|
+
if (persist === 'session') {
|
|
106
|
+
$effect(() => {
|
|
107
|
+
const val = signal.value;
|
|
108
|
+
_sessionData[key] = val;
|
|
109
|
+
flushToSession();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (persist === 'url') {
|
|
113
|
+
$effect(() => {
|
|
114
|
+
writeToURL(key, signal.value);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
get value() { return signal.value; },
|
|
119
|
+
set(v) { signal.value = v; },
|
|
120
|
+
reset() { signal.value = opts.default; },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Snapshots the entire store to sessionStorage.
|
|
125
|
+
* Called by the navigation system BEFORE performing a navigation.
|
|
126
|
+
* This guarantees that state from any island is preserved across routes.
|
|
127
|
+
*/
|
|
128
|
+
export function snapshotStore() {
|
|
129
|
+
if (typeof sessionStorage === 'undefined')
|
|
130
|
+
return;
|
|
131
|
+
for (const [key, signal] of signals.entries()) {
|
|
132
|
+
const mode = persistModes.get(key);
|
|
133
|
+
if (mode === 'session' || mode === 'url') {
|
|
134
|
+
_sessionData[key] = signal.value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
flushToSession();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Reads a value from the store by key.
|
|
141
|
+
* Used by the navigation system to inject state into new islands.
|
|
142
|
+
*/
|
|
143
|
+
export function readStore(key) {
|
|
144
|
+
const signal = signals.get(key);
|
|
145
|
+
if (signal)
|
|
146
|
+
return signal.value;
|
|
147
|
+
if (key in _sessionData)
|
|
148
|
+
return _sessionData[key];
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Writes a value into the store.
|
|
153
|
+
* Triggers reactivity for any island bound to this key.
|
|
154
|
+
*/
|
|
155
|
+
export function writeStore(key, value) {
|
|
156
|
+
const existing = signals.get(key);
|
|
157
|
+
if (existing) {
|
|
158
|
+
existing.value = value;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
signals.set(key, $state(value));
|
|
162
|
+
}
|
|
163
|
+
_sessionData[key] = value;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Returns a serialized snapshot of all persisted store values.
|
|
167
|
+
* Embedded by the server into the navigation response payload,
|
|
168
|
+
* so the new page can initialize islands with correct state.
|
|
169
|
+
*/
|
|
170
|
+
export function exportStore() {
|
|
171
|
+
const exportable = {};
|
|
172
|
+
for (const [key, signal] of signals.entries()) {
|
|
173
|
+
const mode = persistModes.get(key);
|
|
174
|
+
if (mode && mode !== 'memory') {
|
|
175
|
+
exportable[key] = signal.value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return serialize(exportable);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Hydrates the store from a server-provided snapshot.
|
|
182
|
+
* Called on new page initialization before islands mount.
|
|
183
|
+
*/
|
|
184
|
+
export function importStore(serialized) {
|
|
185
|
+
try {
|
|
186
|
+
const data = deserialize(serialized);
|
|
187
|
+
for (const [key, value] of Object.entries(data)) {
|
|
188
|
+
writeStore(key, value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.warn('[Nexus Store] Failed to import snapshot:', err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Clears all store entries. Useful in tests or on logout.
|
|
197
|
+
*/
|
|
198
|
+
export function clearStore(keys) {
|
|
199
|
+
if (keys) {
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
signals.delete(key);
|
|
202
|
+
delete _sessionData[key];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
signals.clear();
|
|
207
|
+
_sessionData = {};
|
|
208
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
209
|
+
sessionStorage.removeItem(STORE_SESSION_KEY);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Returns debug info about all active store entries.
|
|
215
|
+
*/
|
|
216
|
+
export function storeDebugInfo() {
|
|
217
|
+
const info = {};
|
|
218
|
+
for (const [key] of signals.entries()) {
|
|
219
|
+
info[key] = {
|
|
220
|
+
value: signals.get(key)?.value,
|
|
221
|
+
persist: persistModes.get(key) ?? 'memory',
|
|
222
|
+
hasSignal: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return info;
|
|
226
|
+
}
|
|
227
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
228
|
+
let _flushTimer = null;
|
|
229
|
+
function flushToSession() {
|
|
230
|
+
if (typeof sessionStorage === 'undefined')
|
|
231
|
+
return;
|
|
232
|
+
if (_flushTimer)
|
|
233
|
+
clearTimeout(_flushTimer);
|
|
234
|
+
_flushTimer = setTimeout(() => {
|
|
235
|
+
try {
|
|
236
|
+
sessionStorage.setItem(STORE_VERSION_KEY, String(CURRENT_VERSION));
|
|
237
|
+
sessionStorage.setItem(STORE_SESSION_KEY, serialize(_sessionData));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// sessionStorage quota exceeded — clear old data
|
|
241
|
+
sessionStorage.clear();
|
|
242
|
+
}
|
|
243
|
+
}, 50);
|
|
244
|
+
}
|
|
245
|
+
function readFromURL(key) {
|
|
246
|
+
if (typeof location === 'undefined')
|
|
247
|
+
return null;
|
|
248
|
+
try {
|
|
249
|
+
const hash = new URLSearchParams(location.hash.slice(1));
|
|
250
|
+
const raw = hash.get(`nx_${key}`);
|
|
251
|
+
return raw ? deserialize(decodeURIComponent(raw)) : null;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function writeToURL(key, value) {
|
|
258
|
+
if (typeof location === 'undefined')
|
|
259
|
+
return;
|
|
260
|
+
try {
|
|
261
|
+
const hash = new URLSearchParams(location.hash.slice(1));
|
|
262
|
+
hash.set(`nx_${key}`, encodeURIComponent(serialize(value)));
|
|
263
|
+
history.replaceState(null, '', `${location.pathname}${location.search}#${hash.toString()}`);
|
|
264
|
+
}
|
|
265
|
+
catch { }
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus $sync Rune — "The Teleportation Rune"
|
|
3
|
+
*
|
|
4
|
+
* Automatically synchronizes client state with:
|
|
5
|
+
* - Browser cookies
|
|
6
|
+
* - SessionStorage / LocalStorage
|
|
7
|
+
* - Server-side DB (via Server Action endpoint)
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* // Sync with a cookie (persists across sessions)
|
|
11
|
+
* const theme = $sync('theme', { default: 'dark', persist: 'cookie' });
|
|
12
|
+
* theme.value = 'light'; // auto-saves to cookie + syncs to server
|
|
13
|
+
*
|
|
14
|
+
* // Sync with DB (calls /_nexus/sync/:key automatically)
|
|
15
|
+
* const cart = $sync('cart', { default: [], persist: 'db' });
|
|
16
|
+
* cart.value = [...cart.value, newItem]; // writes to DB in background
|
|
17
|
+
*
|
|
18
|
+
* // Optimistic DB sync
|
|
19
|
+
* const likes = $sync('post:42:likes', { default: 0, persist: 'db', optimistic: true });
|
|
20
|
+
* likes.value++; // UI updates immediately, DB writes in background
|
|
21
|
+
*/
|
|
22
|
+
export type SyncPersistence = 'cookie' | 'session' | 'local' | 'db';
|
|
23
|
+
export interface SyncOptions<T> {
|
|
24
|
+
default?: T;
|
|
25
|
+
persist?: SyncPersistence;
|
|
26
|
+
optimistic?: boolean;
|
|
27
|
+
/** Debounce writes in ms (default: 300) */
|
|
28
|
+
debounce?: number;
|
|
29
|
+
/** Cookie options (when persist: 'cookie') */
|
|
30
|
+
cookie?: {
|
|
31
|
+
maxAge?: number;
|
|
32
|
+
path?: string;
|
|
33
|
+
secure?: boolean;
|
|
34
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface SyncedSignal<T> {
|
|
38
|
+
value: T;
|
|
39
|
+
pending: boolean;
|
|
40
|
+
error: string | null;
|
|
41
|
+
/** Force sync with server/storage */
|
|
42
|
+
refresh: () => Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates a synced reactive signal.
|
|
46
|
+
* Reads initial value from storage/server, writes back on every change.
|
|
47
|
+
*/
|
|
48
|
+
export declare function $sync<T>(key: string, opts?: SyncOptions<T>): SyncedSignal<T>;
|
|
49
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpE,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,OAAO,CAAC,EAAE,CAAC,CAAC;IACZ,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;KACtC,CAAC;CACH;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qCAAqC;IACrC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAID;;;GAGG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,WAAW,CAAC,CAAC,CAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAwDhF"}
|