dalila 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/context/auto-scope.d.ts +167 -0
  2. package/dist/context/auto-scope.js +381 -0
  3. package/dist/context/context.d.ts +111 -0
  4. package/dist/context/context.js +283 -0
  5. package/dist/context/index.d.ts +2 -0
  6. package/dist/context/index.js +2 -0
  7. package/dist/context/raw.d.ts +2 -0
  8. package/dist/context/raw.js +2 -0
  9. package/dist/core/dev.d.ts +7 -0
  10. package/dist/core/dev.js +14 -0
  11. package/dist/core/for.d.ts +42 -0
  12. package/dist/core/for.js +311 -0
  13. package/dist/core/index.d.ts +14 -0
  14. package/dist/core/index.js +14 -0
  15. package/dist/core/key.d.ts +33 -0
  16. package/dist/core/key.js +83 -0
  17. package/dist/core/match.d.ts +22 -0
  18. package/dist/core/match.js +175 -0
  19. package/dist/core/mutation.d.ts +55 -0
  20. package/dist/core/mutation.js +128 -0
  21. package/dist/core/persist.d.ts +63 -0
  22. package/dist/core/persist.js +371 -0
  23. package/dist/core/query.d.ts +72 -0
  24. package/dist/core/query.js +184 -0
  25. package/dist/core/resource.d.ts +299 -0
  26. package/dist/core/resource.js +924 -0
  27. package/dist/core/scheduler.d.ts +111 -0
  28. package/dist/core/scheduler.js +243 -0
  29. package/dist/core/scope.d.ts +74 -0
  30. package/dist/core/scope.js +171 -0
  31. package/dist/core/signal.d.ts +88 -0
  32. package/dist/core/signal.js +451 -0
  33. package/dist/core/store.d.ts +130 -0
  34. package/dist/core/store.js +234 -0
  35. package/dist/core/virtual.d.ts +26 -0
  36. package/dist/core/virtual.js +277 -0
  37. package/dist/core/watch-testing.d.ts +13 -0
  38. package/dist/core/watch-testing.js +16 -0
  39. package/dist/core/watch.d.ts +81 -0
  40. package/dist/core/watch.js +353 -0
  41. package/dist/core/when.d.ts +23 -0
  42. package/dist/core/when.js +124 -0
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.js +4 -0
  45. package/dist/internal/watch-testing.d.ts +1 -0
  46. package/dist/internal/watch-testing.js +8 -0
  47. package/dist/router/index.d.ts +1 -0
  48. package/dist/router/index.js +1 -0
  49. package/dist/router/route.d.ts +23 -0
  50. package/dist/router/route.js +48 -0
  51. package/dist/router/router.d.ts +23 -0
  52. package/dist/router/router.js +169 -0
  53. package/dist/runtime/bind.d.ts +65 -0
  54. package/dist/runtime/bind.js +616 -0
  55. package/dist/runtime/index.d.ts +10 -0
  56. package/dist/runtime/index.js +9 -0
  57. package/dist/simple.d.ts +11 -0
  58. package/dist/simple.js +11 -0
  59. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ export * from "./scope.js";
2
+ export * from "./signal.js";
3
+ export * from "./watch.js";
4
+ export * from "./when.js";
5
+ export * from "./match.js";
6
+ export * from "./for.js";
7
+ export * from "./virtual.js";
8
+ export * from "./dev.js";
9
+ export * from "./key.js";
10
+ export * from "./resource.js";
11
+ export * from "./query.js";
12
+ export * from "./mutation.js";
13
+ export { batch, measure, mutate, configureScheduler, getSchedulerConfig } from "./scheduler.js";
14
+ export { persist, createJSONStorage, clearPersisted, createPreloadScript, createThemeScript } from "./persist.js";
@@ -0,0 +1,33 @@
1
+ export type QueryKeyPart = string | number | boolean | null | undefined | symbol;
2
+ export type QueryKey = readonly QueryKeyPart[] | string;
3
+ /**
4
+ * Enable or disable dev mode warnings for key encoding.
5
+ */
6
+ export declare function setKeyDevMode(enabled: boolean): void;
7
+ /**
8
+ * Helper to build an array key with good inference and stable readonly typing.
9
+ *
10
+ * Example:
11
+ * key("user", userId())
12
+ *
13
+ * Note: Objects are not valid key parts. Use primitive values only.
14
+ */
15
+ export declare function key<const T extends readonly QueryKeyPart[]>(...parts: T): T;
16
+ /**
17
+ * Encoded string form of a query key.
18
+ * This is what the cache uses internally as the Map key.
19
+ */
20
+ export type EncodedKey = string;
21
+ /**
22
+ * Encode a query key to a stable string (ES2020-safe).
23
+ *
24
+ * We avoid JSON.stringify because:
25
+ * - it can be unstable for some values
26
+ * - it is slower
27
+ * - it doesn't encode NaN/-0 consistently
28
+ */
29
+ export declare function encodeKey(k: QueryKey): EncodedKey;
30
+ /**
31
+ * Runtime guard (handy for debugging / devtools).
32
+ */
33
+ export declare function isQueryKey(v: unknown): v is QueryKey;
@@ -0,0 +1,83 @@
1
+ /** Dev mode flag for warnings. */
2
+ let devMode = true;
3
+ /**
4
+ * Enable or disable dev mode warnings for key encoding.
5
+ */
6
+ export function setKeyDevMode(enabled) {
7
+ devMode = enabled;
8
+ }
9
+ /**
10
+ * Helper to build an array key with good inference and stable readonly typing.
11
+ *
12
+ * Example:
13
+ * key("user", userId())
14
+ *
15
+ * Note: Objects are not valid key parts. Use primitive values only.
16
+ */
17
+ export function key(...parts) {
18
+ if (devMode) {
19
+ for (let i = 0; i < parts.length; i++) {
20
+ const part = parts[i];
21
+ if (part !== null && typeof part === 'object') {
22
+ console.warn(`[Dalila] key() received an object at index ${i}. ` +
23
+ `Objects are not stable key parts and may cause cache misses. ` +
24
+ `Use primitive values (string, number, boolean) instead.`);
25
+ }
26
+ }
27
+ }
28
+ return parts;
29
+ }
30
+ /**
31
+ * Encode a query key to a stable string (ES2020-safe).
32
+ *
33
+ * We avoid JSON.stringify because:
34
+ * - it can be unstable for some values
35
+ * - it is slower
36
+ * - it doesn't encode NaN/-0 consistently
37
+ */
38
+ export function encodeKey(k) {
39
+ if (typeof k === "string")
40
+ return "k|str|" + escapeKeyString(k);
41
+ return "k|arr|" + k.map(encodeKeyPart).join(";");
42
+ }
43
+ /**
44
+ * Runtime guard (handy for debugging / devtools).
45
+ */
46
+ export function isQueryKey(v) {
47
+ return typeof v === "string" || Array.isArray(v);
48
+ }
49
+ function escapeKeyString(s) {
50
+ // No replaceAll: keep ES2020 typing compatibility.
51
+ // Escape characters used by our encoding format: \ ; | :
52
+ return s
53
+ .split("\\")
54
+ .join("\\\\")
55
+ .split(";")
56
+ .join("\\;")
57
+ .split("|")
58
+ .join("\\|")
59
+ .split(":")
60
+ .join("\\:");
61
+ }
62
+ function encodeKeyPart(v) {
63
+ switch (typeof v) {
64
+ case "string":
65
+ return "str:" + escapeKeyString(v);
66
+ case "number":
67
+ if (Object.is(v, -0))
68
+ return "num:-0";
69
+ if (Number.isNaN(v))
70
+ return "num:NaN";
71
+ return "num:" + String(v);
72
+ case "boolean":
73
+ return "bool:" + (v ? "1" : "0");
74
+ case "undefined":
75
+ return "undef";
76
+ case "symbol":
77
+ // Symbol descriptions are not guaranteed unique, but this is stable for a single runtime.
78
+ // For truly stable keys, prefer strings/numbers/booleans.
79
+ return "sym:" + escapeKeyString(String(v));
80
+ default:
81
+ return "null";
82
+ }
83
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Multi-branch conditional DOM primitive with per-case lifetime.
3
+ *
4
+ * `match()` renders exactly one case at a time, chosen by `value()`.
5
+ * It returns a stable DOM "slot" delimited by two comment markers:
6
+ *
7
+ * <!-- match:start --> ...active case... <!-- match:end -->
8
+ *
9
+ * Design goals:
10
+ * - DOM-first: direct Node insertion/removal (no VDOM).
11
+ * - No "flash": initial case is mounted synchronously.
12
+ * - Correct reactivity: the reactive driver tracks only `value()`.
13
+ * - Isolation: each case runs inside its own `Scope`, so effects/listeners/timers
14
+ * created by a case are disposed when that case unmounts.
15
+ * - Safety: swaps are scheduled via microtask to avoid accidental dependency
16
+ * tracking from inside the case render function.
17
+ *
18
+ * Cases:
19
+ * - `cases[v]` runs when `value()` equals `v`.
20
+ * - `cases["_"]` is an optional fallback when there is no exact match.
21
+ */
22
+ export declare function match<T extends string | number | symbol>(value: () => T, cases: Record<T | "_", () => Node | Node[]>): DocumentFragment;
@@ -0,0 +1,175 @@
1
+ import { effect } from "./signal.js";
2
+ import { createScope, getCurrentScope, isScopeDisposed, withScope } from "./scope.js";
3
+ import { scheduleMicrotask } from "./scheduler.js";
4
+ /**
5
+ * Multi-branch conditional DOM primitive with per-case lifetime.
6
+ *
7
+ * `match()` renders exactly one case at a time, chosen by `value()`.
8
+ * It returns a stable DOM "slot" delimited by two comment markers:
9
+ *
10
+ * <!-- match:start --> ...active case... <!-- match:end -->
11
+ *
12
+ * Design goals:
13
+ * - DOM-first: direct Node insertion/removal (no VDOM).
14
+ * - No "flash": initial case is mounted synchronously.
15
+ * - Correct reactivity: the reactive driver tracks only `value()`.
16
+ * - Isolation: each case runs inside its own `Scope`, so effects/listeners/timers
17
+ * created by a case are disposed when that case unmounts.
18
+ * - Safety: swaps are scheduled via microtask to avoid accidental dependency
19
+ * tracking from inside the case render function.
20
+ *
21
+ * Cases:
22
+ * - `cases[v]` runs when `value()` equals `v`.
23
+ * - `cases["_"]` is an optional fallback when there is no exact match.
24
+ */
25
+ export function match(value, cases) {
26
+ /** Stable range markers that define the insertion point. */
27
+ const start = document.createComment("match:start");
28
+ const end = document.createComment("match:end");
29
+ /** Nodes currently mounted between `start` and `end`. */
30
+ let currentNodes = [];
31
+ /** Scope owning the currently mounted case (disposed on swap). */
32
+ let caseScope = null;
33
+ /** Last resolved case key (exact match or "_"). */
34
+ let lastKey = undefined;
35
+ /** Microtask coalescing: allow only one pending swap per tick. */
36
+ let swapScheduled = false;
37
+ let pendingKey = undefined;
38
+ /** Parent scope captured at creation time (if any). */
39
+ const parentScope = getCurrentScope();
40
+ const resolveParentScope = () => {
41
+ if (!parentScope)
42
+ return null;
43
+ return isScopeDisposed(parentScope) ? null : parentScope;
44
+ };
45
+ /**
46
+ * Guard to prevent "orphan" microtasks from touching DOM after this match()
47
+ * is disposed by a parent scope.
48
+ */
49
+ let disposed = false;
50
+ /** Own-property check (works with symbols and ignores prototype chain). */
51
+ const has = (k) => Object.prototype.hasOwnProperty.call(cases, k);
52
+ /**
53
+ * Unmount current case:
54
+ * - Dispose scope to run cleanups (effects/listeners/timers).
55
+ * - Remove DOM nodes currently mounted in the slot.
56
+ */
57
+ const clear = () => {
58
+ if (caseScope) {
59
+ caseScope.dispose();
60
+ caseScope = null;
61
+ }
62
+ for (const n of currentNodes) {
63
+ if (n.parentNode)
64
+ n.parentNode.removeChild(n);
65
+ }
66
+ currentNodes = [];
67
+ };
68
+ /**
69
+ * Mount nodes right before the end marker and bind them to `scope`.
70
+ * Keeping the markers stable makes swaps predictable and cheap.
71
+ */
72
+ const mount = (nodes, scope) => {
73
+ currentNodes = nodes;
74
+ caseScope = scope;
75
+ end.before(...nodes);
76
+ };
77
+ /**
78
+ * Resolve a runtime value to an actual render key:
79
+ * - exact match if present,
80
+ * - otherwise "_" fallback if present,
81
+ * - otherwise throws (explicit + debuggable failure).
82
+ */
83
+ const resolveKey = (v) => {
84
+ const key = (has(v) ? v : "_");
85
+ if (!has(key)) {
86
+ throw new Error(`No case found for value: ${String(v)} (and no "_" fallback)`);
87
+ }
88
+ return key;
89
+ };
90
+ /**
91
+ * Swap the mounted DOM to the given key.
92
+ * This function is intentionally called outside reactive tracking.
93
+ *
94
+ * Important: the case render function runs inside a fresh `Scope`,
95
+ * so anything created by that case is automatically cleaned up on swap.
96
+ */
97
+ const swap = (key) => {
98
+ clear();
99
+ const nextScope = createScope(resolveParentScope());
100
+ try {
101
+ const fn = cases[key];
102
+ if (!fn) {
103
+ // Should be unreachable if resolveKey is used, but keep it defensive.
104
+ nextScope.dispose();
105
+ throw new Error(`No case found for key: ${String(key)}`);
106
+ }
107
+ const result = withScope(nextScope, () => fn());
108
+ if (!result) {
109
+ // Allow "empty" cases: nothing is mounted, scope is discarded.
110
+ nextScope.dispose();
111
+ return;
112
+ }
113
+ const nodes = Array.isArray(result) ? result : [result];
114
+ mount(nodes, nextScope);
115
+ }
116
+ catch (err) {
117
+ // Never leak the newly created scope on errors.
118
+ nextScope.dispose();
119
+ throw err;
120
+ }
121
+ };
122
+ /** The returned fragment contains only stable markers; content lives between them. */
123
+ const frag = document.createDocumentFragment();
124
+ frag.append(start, end);
125
+ /**
126
+ * Initial mount is synchronous to avoid content flash.
127
+ *
128
+ * CRITICAL: markers must already be in the fragment before `swap()`,
129
+ * because `swap()` inserts using `end.before(...)`.
130
+ */
131
+ const initialKey = resolveKey(value());
132
+ lastKey = initialKey;
133
+ swap(initialKey);
134
+ /**
135
+ * Reactive driver:
136
+ * - Tracks only `value()`.
137
+ * - If the selected key doesn't change, we do nothing (no remount).
138
+ * - If it changes, we schedule a microtask swap (coalesced).
139
+ *
140
+ * Why microtask?
141
+ * - It prevents any reads inside a case render function from becoming
142
+ * dependencies of this outer effect.
143
+ */
144
+ effect(() => {
145
+ const key = resolveKey(value());
146
+ if (lastKey === key)
147
+ return;
148
+ lastKey = key;
149
+ pendingKey = key;
150
+ if (swapScheduled)
151
+ return;
152
+ swapScheduled = true;
153
+ scheduleMicrotask(() => {
154
+ swapScheduled = false;
155
+ if (disposed)
156
+ return;
157
+ const next = pendingKey;
158
+ pendingKey = undefined;
159
+ swap(next);
160
+ });
161
+ });
162
+ /**
163
+ * If this match() is created inside a parent scope, we auto-cleanup:
164
+ * - prevent pending microtasks from doing work,
165
+ * - dispose current case scope,
166
+ * - remove mounted nodes.
167
+ */
168
+ if (parentScope) {
169
+ parentScope.onCleanup(() => {
170
+ disposed = true;
171
+ clear();
172
+ });
173
+ }
174
+ return frag;
175
+ }
@@ -0,0 +1,55 @@
1
+ import { type QueryKey } from "./key.js";
2
+ export interface MutationConfig<TInput, TResult> {
3
+ mutate: (signal: AbortSignal, input: TInput) => Promise<TResult>;
4
+ /**
5
+ * Optional invalidation (runs on success).
6
+ * - Tags revalidate all cached resources that registered those tags.
7
+ * - Keys revalidate a specific cached resource by key.
8
+ */
9
+ invalidateTags?: readonly string[];
10
+ invalidateKeys?: readonly QueryKey[];
11
+ onSuccess?: (result: TResult, input: TInput) => void;
12
+ onError?: (error: Error, input: TInput) => void;
13
+ onSettled?: (input: TInput) => void;
14
+ }
15
+ export interface MutationState<TInput, TResult> {
16
+ data: () => TResult | null;
17
+ loading: () => boolean;
18
+ error: () => Error | null;
19
+ /**
20
+ * Runs the mutation.
21
+ * - Dedupe: if already loading and not forced, it awaits the current run.
22
+ * - Force: aborts the current run and starts a new request.
23
+ */
24
+ run: (input: TInput, opts?: {
25
+ force?: boolean;
26
+ }) => Promise<TResult | null>;
27
+ /**
28
+ * Resets local mutation state.
29
+ * Does not affect the query cache.
30
+ */
31
+ reset: () => void;
32
+ }
33
+ /**
34
+ * Mutation primitive (scope-safe).
35
+ *
36
+ * Design goals:
37
+ * - DOM-first friendly: mutations are just async actions with reactive state.
38
+ * - Scope-safe: abort on scope disposal (best-effort cleanup).
39
+ * - Dedupe-by-default: concurrent `run()` calls share the same in-flight promise.
40
+ * - Force re-run: abort the current request and start a new one.
41
+ * - React Query-like behavior: keep the last successful `data()` until overwritten or reset.
42
+ *
43
+ * Semantics:
44
+ * - Each run uses its own AbortController.
45
+ * - If a run is aborted:
46
+ * - it returns null,
47
+ * - it MUST NOT call onSuccess/onError/onSettled,
48
+ * - and it MUST NOT overwrite state from a newer run.
49
+ *
50
+ * Invalidation:
51
+ * - Runs only after a successful, non-aborted mutation.
52
+ * - invalidateTags: revalidates all cached resources registered for those tags.
53
+ * - invalidateKeys: revalidates specific cached resources by encoded key.
54
+ */
55
+ export declare function createMutation<TInput, TResult>(cfg: MutationConfig<TInput, TResult>): MutationState<TInput, TResult>;
@@ -0,0 +1,128 @@
1
+ import { signal } from "./signal.js";
2
+ import { getCurrentScope } from "./scope.js";
3
+ import { encodeKey } from "./key.js";
4
+ import { invalidateResourceCache, invalidateResourceTags } from "./resource.js";
5
+ /**
6
+ * Mutation primitive (scope-safe).
7
+ *
8
+ * Design goals:
9
+ * - DOM-first friendly: mutations are just async actions with reactive state.
10
+ * - Scope-safe: abort on scope disposal (best-effort cleanup).
11
+ * - Dedupe-by-default: concurrent `run()` calls share the same in-flight promise.
12
+ * - Force re-run: abort the current request and start a new one.
13
+ * - React Query-like behavior: keep the last successful `data()` until overwritten or reset.
14
+ *
15
+ * Semantics:
16
+ * - Each run uses its own AbortController.
17
+ * - If a run is aborted:
18
+ * - it returns null,
19
+ * - it MUST NOT call onSuccess/onError/onSettled,
20
+ * - and it MUST NOT overwrite state from a newer run.
21
+ *
22
+ * Invalidation:
23
+ * - Runs only after a successful, non-aborted mutation.
24
+ * - invalidateTags: revalidates all cached resources registered for those tags.
25
+ * - invalidateKeys: revalidates specific cached resources by encoded key.
26
+ */
27
+ export function createMutation(cfg) {
28
+ const data = signal(null);
29
+ const loading = signal(false);
30
+ const error = signal(null);
31
+ /** In-flight promise for dedupe (represents the latest started run). */
32
+ let inFlight = null;
33
+ /** AbortController for the latest started run (used for force + scope cleanup). */
34
+ let controller = null;
35
+ /**
36
+ * If created inside a scope, abort the active run when the scope is disposed.
37
+ * This prevents orphan network work and avoids updating dead UI.
38
+ */
39
+ const scope = getCurrentScope();
40
+ if (scope) {
41
+ scope.onCleanup(() => {
42
+ controller?.abort();
43
+ controller = null;
44
+ inFlight = null;
45
+ });
46
+ }
47
+ async function run(input, opts = {}) {
48
+ /**
49
+ * Dedupe:
50
+ * - If a run is already loading and we're not forcing, await the current promise.
51
+ * - Snapshot `inFlight` to avoid races if a forced run starts mid-await.
52
+ */
53
+ if (loading() && !opts.force) {
54
+ const p0 = inFlight;
55
+ return (await (p0 ?? Promise.resolve(null)));
56
+ }
57
+ /**
58
+ * Start a new run:
59
+ * - Abort previous run (if any).
60
+ * - Create a fresh controller/signal for this run.
61
+ * - Capture controller identity so older runs cannot clobber newer state.
62
+ */
63
+ controller?.abort();
64
+ controller = new AbortController();
65
+ const sig = controller.signal;
66
+ const localController = controller;
67
+ loading.set(true);
68
+ error.set(null);
69
+ inFlight = (async () => {
70
+ try {
71
+ const result = await cfg.mutate(sig, input);
72
+ // Aborted runs never commit state or call callbacks.
73
+ if (sig.aborted)
74
+ return null;
75
+ data.set(result);
76
+ cfg.onSuccess?.(result, input);
77
+ // Invalidate only after a successful, non-aborted mutation.
78
+ if (cfg.invalidateTags && cfg.invalidateTags.length > 0) {
79
+ invalidateResourceTags(cfg.invalidateTags, { revalidate: true, force: true });
80
+ }
81
+ if (cfg.invalidateKeys && cfg.invalidateKeys.length > 0) {
82
+ for (const k of cfg.invalidateKeys) {
83
+ invalidateResourceCache(encodeKey(k), { revalidate: true, force: true });
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ catch (e) {
89
+ // Aborted runs are treated as null (no error state, no callbacks).
90
+ if (sig.aborted)
91
+ return null;
92
+ const err = e instanceof Error ? e : new Error(String(e));
93
+ error.set(err);
94
+ cfg.onError?.(err, input);
95
+ return null;
96
+ }
97
+ finally {
98
+ /**
99
+ * Only the latest run is allowed to update `loading`.
100
+ *
101
+ * Why?
102
+ * - If run A is aborted because run B starts, A's finally will still execute.
103
+ * - Without this guard, A could flip loading(false) while B is still running.
104
+ */
105
+ const stillCurrent = controller === localController;
106
+ if (stillCurrent)
107
+ loading.set(false);
108
+ // Keep onSettled consistent with onSuccess/onError: never run it for aborted runs.
109
+ if (!sig.aborted)
110
+ cfg.onSettled?.(input);
111
+ }
112
+ })();
113
+ return await inFlight;
114
+ }
115
+ /**
116
+ * Resets local state and aborts any active run.
117
+ * Does not touch the resource/query cache.
118
+ */
119
+ function reset() {
120
+ controller?.abort();
121
+ controller = null;
122
+ inFlight = null;
123
+ loading.set(false);
124
+ error.set(null);
125
+ data.set(null);
126
+ }
127
+ return { data, loading, error, run, reset };
128
+ }
@@ -0,0 +1,63 @@
1
+ import { type Signal } from './signal.js';
2
+ /**
3
+ * Storage interface (compatible with localStorage, sessionStorage, etc.)
4
+ */
5
+ export interface StateStorage {
6
+ getItem(key: string): string | null | Promise<string | null>;
7
+ setItem(key: string, value: string): void | Promise<void>;
8
+ removeItem?(key: string): void | Promise<void>;
9
+ }
10
+ /**
11
+ * Serialization interface
12
+ */
13
+ export interface Serializer<T> {
14
+ serialize(value: T): string;
15
+ deserialize(value: string): T;
16
+ }
17
+ /**
18
+ * Persist options
19
+ */
20
+ export interface PersistOptions<T> {
21
+ name: string;
22
+ storage?: StateStorage;
23
+ serializer?: Serializer<T>;
24
+ version?: number;
25
+ migrate?: (persistedState: unknown, version: number) => T;
26
+ merge?: 'replace' | 'shallow';
27
+ onRehydrate?: (state: T) => void;
28
+ onError?: (error: Error) => void;
29
+ /**
30
+ * Dev-server only hint (not used by runtime yet).
31
+ * Kept for forward-compat with preload injection.
32
+ */
33
+ preload?: boolean;
34
+ }
35
+ /**
36
+ * Create a persisted signal that automatically syncs with storage.
37
+ */
38
+ export declare function persist<T>(baseSignal: Signal<T>, options: PersistOptions<T>): Signal<T>;
39
+ /**
40
+ * Helper to create JSON storage wrapper
41
+ */
42
+ export declare function createJSONStorage(getStorage: () => StateStorage): StateStorage;
43
+ /**
44
+ * Clear persisted data for a given key
45
+ */
46
+ export declare function clearPersisted(name: string, storage?: StateStorage): void;
47
+ /**
48
+ * Options for preload script generation
49
+ */
50
+ export interface PreloadScriptOptions {
51
+ storageKey: string;
52
+ defaultValue: string;
53
+ target?: 'documentElement' | 'body';
54
+ attribute?: string;
55
+ storageType?: 'localStorage' | 'sessionStorage';
56
+ }
57
+ /**
58
+ * Generate a minimal inline script to prevent FOUC.
59
+ *
60
+ * NOTE: Assumes the value in storage was JSON serialized (default serializer).
61
+ */
62
+ export declare function createPreloadScript(options: PreloadScriptOptions): string;
63
+ export declare function createThemeScript(storageKey: string, defaultTheme?: string): string;