@victorylabs/params 0.1.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/chunk-43PUAYQP.js +573 -0
  4. package/dist/chunk-43PUAYQP.js.map +1 -0
  5. package/dist/chunk-4T4THPFW.js +100 -0
  6. package/dist/chunk-4T4THPFW.js.map +1 -0
  7. package/dist/chunk-5NSLHAHG.js +26 -0
  8. package/dist/chunk-5NSLHAHG.js.map +1 -0
  9. package/dist/chunk-NHCH2WKC.js +96 -0
  10. package/dist/chunk-NHCH2WKC.js.map +1 -0
  11. package/dist/chunk-NUO3GOXV.js +72 -0
  12. package/dist/chunk-NUO3GOXV.js.map +1 -0
  13. package/dist/devtools.cjs +41 -0
  14. package/dist/devtools.cjs.map +1 -0
  15. package/dist/devtools.d.cts +45 -0
  16. package/dist/devtools.d.ts +45 -0
  17. package/dist/devtools.js +16 -0
  18. package/dist/devtools.js.map +1 -0
  19. package/dist/index.cjs +777 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +133 -0
  22. package/dist/index.d.ts +133 -0
  23. package/dist/index.js +83 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/integrations/forms-reverse.cjs +777 -0
  26. package/dist/integrations/forms-reverse.cjs.map +1 -0
  27. package/dist/integrations/forms-reverse.d.cts +32 -0
  28. package/dist/integrations/forms-reverse.d.ts +32 -0
  29. package/dist/integrations/forms-reverse.js +73 -0
  30. package/dist/integrations/forms-reverse.js.map +1 -0
  31. package/dist/integrations/forms.cjs +771 -0
  32. package/dist/integrations/forms.cjs.map +1 -0
  33. package/dist/integrations/forms.d.cts +25 -0
  34. package/dist/integrations/forms.d.ts +25 -0
  35. package/dist/integrations/forms.js +65 -0
  36. package/dist/integrations/forms.js.map +1 -0
  37. package/dist/params-store-Cgbtn53j.d.cts +115 -0
  38. package/dist/params-store-CguA9-yr.d.ts +115 -0
  39. package/dist/react.cjs +910 -0
  40. package/dist/react.cjs.map +1 -0
  41. package/dist/react.d.cts +75 -0
  42. package/dist/react.d.ts +75 -0
  43. package/dist/react.js +202 -0
  44. package/dist/react.js.map +1 -0
  45. package/dist/snapshot.cjs +75 -0
  46. package/dist/snapshot.cjs.map +1 -0
  47. package/dist/snapshot.d.cts +42 -0
  48. package/dist/snapshot.d.ts +42 -0
  49. package/dist/snapshot.js +42 -0
  50. package/dist/snapshot.js.map +1 -0
  51. package/dist/storage/compose.cjs +196 -0
  52. package/dist/storage/compose.cjs.map +1 -0
  53. package/dist/storage/compose.d.cts +35 -0
  54. package/dist/storage/compose.d.ts +35 -0
  55. package/dist/storage/compose.js +123 -0
  56. package/dist/storage/compose.js.map +1 -0
  57. package/dist/storage/cookie.cjs +136 -0
  58. package/dist/storage/cookie.cjs.map +1 -0
  59. package/dist/storage/cookie.d.cts +57 -0
  60. package/dist/storage/cookie.d.ts +57 -0
  61. package/dist/storage/cookie.js +111 -0
  62. package/dist/storage/cookie.js.map +1 -0
  63. package/dist/storage/idb.cjs +144 -0
  64. package/dist/storage/idb.cjs.map +1 -0
  65. package/dist/storage/idb.d.cts +31 -0
  66. package/dist/storage/idb.d.ts +31 -0
  67. package/dist/storage/idb.js +119 -0
  68. package/dist/storage/idb.js.map +1 -0
  69. package/dist/storage/local.cjs +121 -0
  70. package/dist/storage/local.cjs.map +1 -0
  71. package/dist/storage/local.d.cts +23 -0
  72. package/dist/storage/local.d.ts +23 -0
  73. package/dist/storage/local.js +9 -0
  74. package/dist/storage/local.js.map +1 -0
  75. package/dist/storage/server.cjs +158 -0
  76. package/dist/storage/server.cjs.map +1 -0
  77. package/dist/storage/server.d.cts +57 -0
  78. package/dist/storage/server.d.ts +57 -0
  79. package/dist/storage/server.js +133 -0
  80. package/dist/storage/server.js.map +1 -0
  81. package/dist/storage/session.cjs +123 -0
  82. package/dist/storage/session.cjs.map +1 -0
  83. package/dist/storage/session.d.cts +14 -0
  84. package/dist/storage/session.d.ts +14 -0
  85. package/dist/storage/session.js +12 -0
  86. package/dist/storage/session.js.map +1 -0
  87. package/dist/storage/url.cjs +132 -0
  88. package/dist/storage/url.cjs.map +1 -0
  89. package/dist/storage/url.d.cts +37 -0
  90. package/dist/storage/url.d.ts +37 -0
  91. package/dist/storage/url.js +100 -0
  92. package/dist/storage/url.js.map +1 -0
  93. package/dist/storage-DBLIRR-4.d.cts +59 -0
  94. package/dist/storage-DBLIRR-4.d.ts +59 -0
  95. package/dist/types-BSWKH-jw.d.cts +68 -0
  96. package/dist/types-BUmNpSyP.d.ts +68 -0
  97. package/package.json +114 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victory Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @victorylabs/params
2
+
3
+ Centralized view-state config (filters, sort, pagination, app config) with pluggable storage. Memory by default; URL/localStorage/sessionStorage opt-in. Single React hook DX, schema-validated reads, cross-component sharing.
4
+
5
+ A focused alternative to `nuqs` / hand-rolled `useSearchParams + useEffect` chains, with:
6
+
7
+ - **Single hook** — `useParams(def)` returns a controller; everything else flows from `p.`.
8
+ - **Pluggable storage** — memory (default), URL, localStorage, sessionStorage, or your own backend implementing `ParamsStorage<T>`.
9
+ - **Schema-validated reads** — Zod / Standard Schema / plain spec; corrupted external data silently falls back to defaults.
10
+ - **Cross-component sharing** — multiple `useParams(def)` calls in different components share the same store automatically (definition identity).
11
+ - **Per-field debounce + input shadow** — typing feels instant; URL writes are throttled.
12
+ - **Compile-time path safety** — `p.value('emial')` is a TypeScript error.
13
+ - **Deterministic `p.toQuery()`** — alphabetical, suitable for React Query cache keys.
14
+ - **Forms bridge** — `paramsToFormSync(def)` lets `@victorylabs/forms` borrow a params definition (one source of truth for filters + a save-view form).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pnpm add @victorylabs/params zod
20
+ ```
21
+
22
+ `react`, `zod`, and `@victorylabs/forms` are optional peer dependencies. Install only what you use.
23
+
24
+ ## Quick start
25
+
26
+ ```tsx
27
+ import { defineParams } from '@victorylabs/params'
28
+ import { useParams } from '@victorylabs/params/react'
29
+ import { urlStorage } from '@victorylabs/params/storage/url'
30
+ import { z } from 'zod'
31
+
32
+ const filters = defineParams(
33
+ {
34
+ query: z.string().default(''),
35
+ page: z.coerce.number().int().min(1).default(1),
36
+ sort: z.enum(['asc', 'desc']).default('asc'),
37
+ },
38
+ {
39
+ storage: urlStorage(),
40
+ fields: {
41
+ query: { debounce: 300 },
42
+ page: { omitWhenDefault: true },
43
+ },
44
+ },
45
+ )
46
+
47
+ export function ProductsPage() {
48
+ const p = useParams(filters)
49
+
50
+ return (
51
+ <>
52
+ <input {...p.input('query')} placeholder="Search…" />
53
+ <select value={p.values.sort} onChange={(e) => p.set('sort', e.target.value as 'asc' | 'desc')}>
54
+ <option value="asc">A → Z</option>
55
+ <option value="desc">Z → A</option>
56
+ </select>
57
+ <ResultList query={p.deferred('query')} sort={p.value('sort')} page={p.value('page')} />
58
+ <a href={p.href({ page: p.values.page + 1 })}>Next</a>
59
+ </>
60
+ )
61
+ }
62
+ ```
63
+
64
+ ## Documentation
65
+
66
+ Full reference at [the docs site](https://victorylabs.dev/libs/params).
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,573 @@
1
+ import {
2
+ deepEqual,
3
+ deepGet,
4
+ deepSet,
5
+ splitPath,
6
+ warn
7
+ } from "./chunk-4T4THPFW.js";
8
+ import {
9
+ takePreHydrationValues
10
+ } from "./chunk-5NSLHAHG.js";
11
+ import {
12
+ defaultSerialize,
13
+ extractEnumValues,
14
+ getDefault,
15
+ isPlainSpec,
16
+ isStandardSchema,
17
+ parseField
18
+ } from "./chunk-NUO3GOXV.js";
19
+
20
+ // ../utils/src/cascade.ts
21
+ var DEPTH_CAP = 10;
22
+ function resolveCascade(initialChanges, ctx) {
23
+ const changes = { ...initialChanges };
24
+ const warnings = [];
25
+ const working = { ...ctx.currentValues, ...initialChanges };
26
+ let frontier = new Set(Object.keys(initialChanges));
27
+ const visited = new Set(frontier);
28
+ let depth = 0;
29
+ while (frontier.size > 0) {
30
+ if (depth >= DEPTH_CAP) {
31
+ warnings.push(
32
+ `cascade depth cap (${DEPTH_CAP}) reached; stopping. Frontier: [${[...frontier].join(", ")}]. Likely a config error \u2014 check for accidental long chains.`
33
+ );
34
+ break;
35
+ }
36
+ const nextFrontier = /* @__PURE__ */ new Set();
37
+ for (const targetPath of ctx.allPaths) {
38
+ const config = ctx.fieldConfigs[targetPath];
39
+ if (!config?.onDepChange) continue;
40
+ if (frontier.has(targetPath)) continue;
41
+ const deps = resolveDeps(config, ctx.allPaths, targetPath);
42
+ const changedDeps = deps.filter((d) => frontier.has(d));
43
+ if (changedDeps.length === 0) continue;
44
+ if (visited.has(targetPath)) {
45
+ warnings.push(
46
+ `cycle detected: '${targetPath}' would cascade twice (triggered by [${changedDeps.join(", ")}]). Skipping re-entry.`
47
+ );
48
+ continue;
49
+ }
50
+ const newValue = applyDepChange(config.onDepChange, deps, working, ctx.defaults, targetPath);
51
+ if (Object.is(newValue, working[targetPath])) continue;
52
+ changes[targetPath] = newValue;
53
+ working[targetPath] = newValue;
54
+ visited.add(targetPath);
55
+ nextFrontier.add(targetPath);
56
+ }
57
+ frontier = nextFrontier;
58
+ depth++;
59
+ }
60
+ return { changes, warnings };
61
+ }
62
+ function resolveDeps(config, allPaths, selfPath) {
63
+ if (config.dependsOn === "*") {
64
+ const exclude = new Set(config.excludeDeps ?? []);
65
+ exclude.add(selfPath);
66
+ return allPaths.filter((p) => !exclude.has(p));
67
+ }
68
+ if (Array.isArray(config.dependsOn)) {
69
+ return config.dependsOn.filter((p) => p !== selfPath);
70
+ }
71
+ return [];
72
+ }
73
+ function applyDepChange(onDepChange, deps, working, defaults, targetPath) {
74
+ if (onDepChange === "reset") return defaults[targetPath];
75
+ if (onDepChange === "clear") return void 0;
76
+ const depsRecord = {};
77
+ for (const d of deps) {
78
+ depsRecord[d] = working[d];
79
+ }
80
+ return onDepChange(depsRecord, working[targetPath]);
81
+ }
82
+
83
+ // ../utils/src/path-trie.ts
84
+ var makeNode = () => ({ listeners: /* @__PURE__ */ new Set(), children: /* @__PURE__ */ new Map() });
85
+ var PathTrie = class {
86
+ root = makeNode();
87
+ subscribe(path, listener) {
88
+ const node = this.ensure(path);
89
+ node.listeners.add(listener);
90
+ return () => {
91
+ node.listeners.delete(listener);
92
+ };
93
+ }
94
+ notify(path) {
95
+ const segments = splitPath(path);
96
+ let current = this.root;
97
+ fire(current);
98
+ for (const seg of segments) {
99
+ const next = current.children.get(seg);
100
+ if (!next) return;
101
+ current = next;
102
+ fire(current);
103
+ }
104
+ fireSubtree(current);
105
+ }
106
+ /** Test/diagnostic helper — total subscriber count below a given path. */
107
+ size(path = "") {
108
+ const node = this.find(path);
109
+ if (!node) return 0;
110
+ return countSubtree(node);
111
+ }
112
+ ensure(path) {
113
+ let node = this.root;
114
+ for (const seg of splitPath(path)) {
115
+ let child = node.children.get(seg);
116
+ if (!child) {
117
+ child = makeNode();
118
+ node.children.set(seg, child);
119
+ }
120
+ node = child;
121
+ }
122
+ return node;
123
+ }
124
+ find(path) {
125
+ let node = this.root;
126
+ for (const seg of splitPath(path)) {
127
+ node = node?.children.get(seg);
128
+ if (!node) return void 0;
129
+ }
130
+ return node;
131
+ }
132
+ };
133
+ function fire(node) {
134
+ for (const listener of node.listeners) listener();
135
+ }
136
+ function fireSubtree(node) {
137
+ for (const child of node.children.values()) {
138
+ fire(child);
139
+ fireSubtree(child);
140
+ }
141
+ }
142
+ function countSubtree(node) {
143
+ let total = node.listeners.size;
144
+ for (const child of node.children.values()) {
145
+ total += countSubtree(child);
146
+ }
147
+ return total;
148
+ }
149
+
150
+ // src/params-store.ts
151
+ var isClient = typeof window !== "undefined";
152
+ var ParamsStore = class {
153
+ spec;
154
+ storage;
155
+ fieldConfigs;
156
+ values;
157
+ defaults;
158
+ trie = new PathTrie();
159
+ storageErrorMap = /* @__PURE__ */ new Map();
160
+ lastWritten;
161
+ storageUnsubscribe;
162
+ toQueryCache;
163
+ disposed = false;
164
+ constructor(def) {
165
+ this.spec = def.spec;
166
+ this.storage = def.storage;
167
+ this.fieldConfigs = def.fields;
168
+ this.defaults = this.computeDefaults();
169
+ this.values = { ...this.defaults };
170
+ const seeded = takePreHydrationValues(def.name);
171
+ if (seeded !== void 0) {
172
+ this.values = { ...this.defaults, ...seeded };
173
+ } else if (this.storage.clientOnly && !isClient) {
174
+ } else {
175
+ this.hydrateFromStorage();
176
+ }
177
+ if (this.storage.subscribe) {
178
+ this.storageUnsubscribe = this.storage.subscribe((raw) => this.onExternalChange(raw));
179
+ }
180
+ }
181
+ // ─── Reads (synchronous, framework-agnostic) ──────────────────────────
182
+ getValues() {
183
+ return this.values;
184
+ }
185
+ getValue(path) {
186
+ return deepGet(this.values, path);
187
+ }
188
+ /** Storage parse failures discovered on hydrate or external change. */
189
+ get storageErrors() {
190
+ const out = {};
191
+ for (const [path, reason] of this.storageErrorMap) out[path] = reason;
192
+ return out;
193
+ }
194
+ /** Per-field config (for the React adapter to read debounce settings, etc.). */
195
+ getFieldConfig(path) {
196
+ return this.fieldConfigs[path];
197
+ }
198
+ set(pathOrPartial, valueOrOptions, maybeOptions) {
199
+ if (this.disposed) return;
200
+ let updates;
201
+ let options;
202
+ if (typeof pathOrPartial === "string") {
203
+ updates = { [pathOrPartial]: valueOrOptions };
204
+ options = maybeOptions;
205
+ } else {
206
+ updates = pathOrPartial;
207
+ options = valueOrOptions;
208
+ }
209
+ const initialChanges = {};
210
+ for (const [path, v] of Object.entries(updates)) {
211
+ const old = deepGet(this.values, path);
212
+ if (deepEqual(old, v)) continue;
213
+ initialChanges[path] = v;
214
+ }
215
+ if (Object.keys(initialChanges).length === 0) return;
216
+ const cascade = resolveCascade(initialChanges, {
217
+ fieldConfigs: this.fieldConfigs,
218
+ defaults: this.defaults,
219
+ currentValues: this.values,
220
+ allPaths: Object.keys(this.spec)
221
+ });
222
+ for (const w of cascade.warnings) warn(w);
223
+ const changed = [];
224
+ let next = this.values;
225
+ for (const [path, v] of Object.entries(cascade.changes)) {
226
+ const old = deepGet(next, path);
227
+ if (deepEqual(old, v)) continue;
228
+ next = deepSet(next, path, v);
229
+ changed.push(path);
230
+ }
231
+ if (changed.length === 0) return;
232
+ this.values = next;
233
+ this.invalidateToQueryCache();
234
+ for (const path of changed) this.trie.notify(path);
235
+ this.persistToStorage(changed, options);
236
+ }
237
+ /** Boolean-flip helper. */
238
+ toggle(path, options) {
239
+ const current = this.getValue(path);
240
+ this.set(path, !current, options);
241
+ }
242
+ /** Push a value onto an array field. */
243
+ append(path, value, options) {
244
+ const current = this.getValue(path);
245
+ if (!Array.isArray(current)) return;
246
+ this.set(path, [...current, value], options);
247
+ }
248
+ /** Remove the first array element matching `value` by deepEqual. */
249
+ remove(path, value, options) {
250
+ const current = this.getValue(path);
251
+ if (!Array.isArray(current)) return;
252
+ const idx = current.findIndex((item) => deepEqual(item, value));
253
+ if (idx === -1) return;
254
+ this.set(path, [...current.slice(0, idx), ...current.slice(idx + 1)], options);
255
+ }
256
+ /** Remove the array element at the given index. */
257
+ removeAt(path, index, options) {
258
+ const current = this.getValue(path);
259
+ if (!Array.isArray(current)) return;
260
+ if (index < 0 || index >= current.length) return;
261
+ this.set(path, [...current.slice(0, index), ...current.slice(index + 1)], options);
262
+ }
263
+ cycle(path, valuesOrOptions, maybeOptions) {
264
+ let resolved;
265
+ let setOpts;
266
+ if (Array.isArray(valuesOrOptions)) {
267
+ resolved = valuesOrOptions;
268
+ setOpts = maybeOptions;
269
+ } else {
270
+ resolved = void 0;
271
+ setOpts = valuesOrOptions;
272
+ }
273
+ if (resolved === void 0) {
274
+ const fieldSpec = this.spec[path];
275
+ const derived = fieldSpec !== void 0 ? extractEnumValues(fieldSpec) : void 0;
276
+ if (!derived || derived.length === 0) {
277
+ throw new Error(
278
+ `Cannot derive enum values for params field '${path}'; pass values explicitly to cycle().`
279
+ );
280
+ }
281
+ resolved = derived;
282
+ }
283
+ if (resolved.length === 0) return;
284
+ const current = this.getValue(path);
285
+ const idx = resolved.findIndex((o) => deepEqual(o, current));
286
+ const next = idx === -1 ? resolved[0] : resolved[(idx + 1) % resolved.length];
287
+ this.set(path, next, setOpts);
288
+ }
289
+ /** Reset a single field to its default. */
290
+ clear(path, options) {
291
+ const def = deepGet(this.defaults, path);
292
+ this.set(path, def, options);
293
+ }
294
+ /** Reset all fields to defaults; optional partial overrides + SetOptions. */
295
+ reset(values, options) {
296
+ if (this.disposed) return;
297
+ const next = values ? { ...this.defaults, ...values } : { ...this.defaults };
298
+ const changed = Object.keys({ ...this.values, ...next });
299
+ this.values = next;
300
+ this.invalidateToQueryCache();
301
+ for (const path of changed) this.trie.notify(path);
302
+ const writeOptions = options?.history !== void 0 ? { history: options.history } : void 0;
303
+ void this.storage.clear?.([], writeOptions);
304
+ this.lastWritten = void 0;
305
+ }
306
+ // ─── Subscriptions ────────────────────────────────────────────────────
307
+ subscribe(path, listener) {
308
+ return this.trie.subscribe(path, listener);
309
+ }
310
+ // ─── URL helpers ──────────────────────────────────────────────────────
311
+ toQuery(overrides) {
312
+ const effective = overrides ? { ...this.values, ...overrides } : this.values;
313
+ if (!overrides && this.toQueryCache && this.toQueryCache.values === this.values) {
314
+ return this.toQueryCache.query;
315
+ }
316
+ const params = new URLSearchParams();
317
+ const keys = Object.keys(effective).sort();
318
+ for (const key of keys) {
319
+ const value = effective[key];
320
+ if (value === void 0 || value === null) continue;
321
+ const config = this.fieldConfigs[key];
322
+ const def = deepGet(this.defaults, key);
323
+ if (config?.omitWhenDefault && deepEqual(value, def)) continue;
324
+ params.set(key, defaultSerialize(value));
325
+ }
326
+ const str = params.toString();
327
+ const out = str ? `?${str}` : "";
328
+ if (!overrides) this.toQueryCache = { values: this.values, query: out };
329
+ return out;
330
+ }
331
+ href(overrides) {
332
+ if (!overrides) {
333
+ const query = this.toQuery();
334
+ const pathname2 = isClient ? window.location.pathname : "";
335
+ const cached = this.toQueryCache;
336
+ if (cached && cached.values === this.values && cached.href !== void 0) {
337
+ const expected = pathname2 + query;
338
+ if (cached.href === expected) return cached.href;
339
+ }
340
+ const out = pathname2 + query;
341
+ if (cached && cached.values === this.values) cached.href = out;
342
+ return out;
343
+ }
344
+ const pathname = isClient ? window.location.pathname : "";
345
+ return pathname + this.toQuery(overrides);
346
+ }
347
+ // ─── Disposal ─────────────────────────────────────────────────────────
348
+ dispose() {
349
+ if (this.disposed) return;
350
+ this.disposed = true;
351
+ this.storageUnsubscribe?.();
352
+ this.storageUnsubscribe = void 0;
353
+ }
354
+ // ─── Devtools / debugging escape hatch (v0.5) ─────────────────────────
355
+ /**
356
+ * Devtools / debugging escape hatch. Returns a frozen, defensive-copied
357
+ * snapshot of all internal state. Pure read — no side effects, no
358
+ * subscriptions registered.
359
+ *
360
+ * Uses `structuredClone` for the values + defaults deep-copy. Because
361
+ * params values round-trip through storage backends, they're already
362
+ * restricted to JSON-safe shapes — so `__introspect()` is safer here than
363
+ * on `FormStore`. Still: marked unstable via the `__` prefix; field set
364
+ * may grow over releases.
365
+ */
366
+ __introspect() {
367
+ const specTypes = {};
368
+ for (const [path, spec] of Object.entries(this.spec)) {
369
+ if (isPlainSpec(spec)) {
370
+ specTypes[path] = "plain";
371
+ continue;
372
+ }
373
+ if (spec?._def) {
374
+ specTypes[path] = "zod";
375
+ continue;
376
+ }
377
+ if (isStandardSchema(spec)) {
378
+ specTypes[path] = "standard-schema";
379
+ continue;
380
+ }
381
+ specTypes[path] = "plain";
382
+ }
383
+ let valuesClone;
384
+ let defaultsClone;
385
+ let lastWrittenClone;
386
+ try {
387
+ valuesClone = structuredClone(this.values);
388
+ defaultsClone = structuredClone(this.defaults);
389
+ lastWrittenClone = this.lastWritten ? structuredClone(this.lastWritten) : void 0;
390
+ } catch (err) {
391
+ throw new Error(
392
+ `ParamsStore.__introspect() failed: values contain non-cloneable data (functions, Symbols, DOM nodes are not allowed in params state). Original error: ${err instanceof Error ? err.message : String(err)}`
393
+ );
394
+ }
395
+ return Object.freeze({
396
+ values: valuesClone,
397
+ defaults: defaultsClone,
398
+ fieldsConfigured: Object.freeze({ ...this.fieldConfigs }),
399
+ fieldsBySpecType: Object.freeze(specTypes),
400
+ storageErrors: Object.freeze({ ...this.storageErrors }),
401
+ storage: Object.freeze({
402
+ name: this.storage.name,
403
+ clientOnly: this.storage.clientOnly === true,
404
+ hasReadAsync: this.storage.readAsync !== void 0,
405
+ hasSubscribe: this.storage.subscribe !== void 0,
406
+ hasClear: this.storage.clear !== void 0
407
+ }),
408
+ lastWritten: lastWrittenClone
409
+ });
410
+ }
411
+ // ─── Internals ────────────────────────────────────────────────────────
412
+ computeDefaults() {
413
+ const out = {};
414
+ for (const [key, fieldSpec] of Object.entries(this.spec)) {
415
+ const def = getDefault(fieldSpec);
416
+ if (def !== void 0) out[key] = def;
417
+ }
418
+ return out;
419
+ }
420
+ hydrateFromStorage() {
421
+ let raw;
422
+ try {
423
+ raw = this.storage.read();
424
+ } catch (err) {
425
+ this.storageErrorMap.set("__read__", err instanceof Error ? err.message : String(err));
426
+ return;
427
+ }
428
+ if (!raw) return;
429
+ for (const [key, rawValue] of Object.entries(raw)) {
430
+ const fieldSpec = this.spec[key];
431
+ if (!fieldSpec) continue;
432
+ const result = parseField(fieldSpec, rawValue);
433
+ if (result.ok && result.value !== void 0) {
434
+ ;
435
+ this.values[key] = result.value;
436
+ } else if (result.reason) {
437
+ this.storageErrorMap.set(key, result.reason);
438
+ }
439
+ }
440
+ }
441
+ persistToStorage(changed, options) {
442
+ const filtered = {};
443
+ for (const path of changed) {
444
+ const value = this.values[path];
445
+ const config = this.fieldConfigs[path];
446
+ const def = deepGet(this.defaults, path);
447
+ if (config?.omitWhenDefault && deepEqual(value, def)) {
448
+ filtered[path] = void 0;
449
+ } else {
450
+ filtered[path] = value;
451
+ }
452
+ }
453
+ const writeOptions = options?.history !== void 0 ? { history: options.history } : void 0;
454
+ this.lastWritten = { ...this.values };
455
+ try {
456
+ const result = this.storage.write(filtered, changed, writeOptions);
457
+ if (result && typeof result.then === "function") {
458
+ ;
459
+ result.catch(() => void 0);
460
+ }
461
+ } catch {
462
+ }
463
+ }
464
+ onExternalChange(raw) {
465
+ if (this.disposed) return;
466
+ if (this.lastWritten && deepEqual(raw, this.lastWritten)) return;
467
+ const changed = [];
468
+ let next = this.values;
469
+ for (const [key, rawValue] of Object.entries(raw)) {
470
+ const fieldSpec = this.spec[key];
471
+ if (!fieldSpec) continue;
472
+ const result = parseField(fieldSpec, rawValue);
473
+ const newValue = result.ok ? result.value : deepGet(this.defaults, key);
474
+ const oldValue = deepGet(next, key);
475
+ if (deepEqual(oldValue, newValue)) continue;
476
+ next = deepSet(next, key, newValue);
477
+ changed.push(key);
478
+ if (!result.ok && result.reason) {
479
+ this.storageErrorMap.set(key, result.reason);
480
+ } else {
481
+ this.storageErrorMap.delete(key);
482
+ }
483
+ }
484
+ if (changed.length === 0) return;
485
+ this.values = next;
486
+ this.invalidateToQueryCache();
487
+ for (const path of changed) this.trie.notify(path);
488
+ }
489
+ invalidateToQueryCache() {
490
+ this.toQueryCache = void 0;
491
+ }
492
+ };
493
+
494
+ // src/store-cache.ts
495
+ var cache = /* @__PURE__ */ new WeakMap();
496
+ var seenNames = /* @__PURE__ */ new Map();
497
+ function acquire(def) {
498
+ warnOnDuplicateName(def);
499
+ let entry = cache.get(def);
500
+ if (!entry) {
501
+ entry = {
502
+ store: new ParamsStore(def),
503
+ refCount: 0,
504
+ disposalQueued: false
505
+ };
506
+ cache.set(def, entry);
507
+ } else {
508
+ entry.disposalQueued = false;
509
+ }
510
+ entry.refCount++;
511
+ return entry.store;
512
+ }
513
+ function release(def) {
514
+ const entry = cache.get(def);
515
+ if (!entry) return;
516
+ entry.refCount--;
517
+ if (entry.refCount > 0) return;
518
+ entry.disposalQueued = true;
519
+ queueMicrotask(() => {
520
+ if (!entry.disposalQueued) return;
521
+ entry.disposalQueued = false;
522
+ disposeAndClear(def, entry);
523
+ });
524
+ }
525
+ function disposeAndClear(def, entry) {
526
+ entry.store.dispose();
527
+ cache.delete(def);
528
+ if (def.name !== void 0 && seenNames.get(def.name) === def) {
529
+ seenNames.delete(def.name);
530
+ }
531
+ }
532
+ function getCachedStore(def) {
533
+ let entry = cache.get(def);
534
+ if (!entry) {
535
+ entry = {
536
+ store: new ParamsStore(def),
537
+ refCount: 0,
538
+ disposalQueued: false
539
+ };
540
+ cache.set(def, entry);
541
+ }
542
+ return entry.store;
543
+ }
544
+ function releaseCachedStore(def) {
545
+ const entry = cache.get(def);
546
+ if (!entry) {
547
+ if (def.name !== void 0 && seenNames.get(def.name) === def) {
548
+ seenNames.delete(def.name);
549
+ }
550
+ return;
551
+ }
552
+ disposeAndClear(def, entry);
553
+ }
554
+ function warnOnDuplicateName(def) {
555
+ if (def.name === void 0) return;
556
+ const existing = seenNames.get(def.name);
557
+ if (existing !== void 0 && existing !== def) {
558
+ warn(
559
+ `Duplicate definition name '${def.name}' detected. Names must be unique across definitions for v0.2 SSR snapshot keys.`
560
+ );
561
+ return;
562
+ }
563
+ seenNames.set(def.name, def);
564
+ }
565
+
566
+ export {
567
+ ParamsStore,
568
+ acquire,
569
+ release,
570
+ getCachedStore,
571
+ releaseCachedStore
572
+ };
573
+ //# sourceMappingURL=chunk-43PUAYQP.js.map