@victorylabs/params 0.1.0 → 0.3.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 (37) hide show
  1. package/dist/{chunk-NUO3GOXV.js → chunk-5UKBDZTP.js} +37 -2
  2. package/dist/chunk-5UKBDZTP.js.map +1 -0
  3. package/dist/{chunk-43PUAYQP.js → chunk-DSAHBEAQ.js} +44 -18
  4. package/dist/chunk-DSAHBEAQ.js.map +1 -0
  5. package/dist/devtools.d.cts +1 -1
  6. package/dist/devtools.d.ts +1 -1
  7. package/dist/index.cjs +79 -17
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +47 -11
  10. package/dist/index.d.ts +47 -11
  11. package/dist/index.js +5 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/forms-reverse.cjs +71 -17
  14. package/dist/integrations/forms-reverse.cjs.map +1 -1
  15. package/dist/integrations/forms-reverse.js +2 -2
  16. package/dist/integrations/forms.cjs +71 -17
  17. package/dist/integrations/forms.cjs.map +1 -1
  18. package/dist/integrations/forms.js +2 -2
  19. package/dist/{params-store-Cgbtn53j.d.cts → params-store-4Lcb1M_X.d.cts} +29 -1
  20. package/dist/{params-store-CguA9-yr.d.ts → params-store-f3pmPdw3.d.ts} +29 -1
  21. package/dist/react.cjs +127 -54
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +42 -4
  24. package/dist/react.d.ts +42 -4
  25. package/dist/react.js +38 -20
  26. package/dist/react.js.map +1 -1
  27. package/dist/storage/idb.cjs +56 -3
  28. package/dist/storage/idb.cjs.map +1 -1
  29. package/dist/storage/idb.d.cts +40 -8
  30. package/dist/storage/idb.d.ts +40 -8
  31. package/dist/storage/idb.js +56 -3
  32. package/dist/storage/idb.js.map +1 -1
  33. package/dist/storage/url.cjs.map +1 -1
  34. package/dist/storage/url.js +1 -1
  35. package/package.json +3 -2
  36. package/dist/chunk-43PUAYQP.js.map +0 -1
  37. package/dist/chunk-NUO3GOXV.js.map +0 -1
@@ -41,7 +41,14 @@ function defaultSerialize(value) {
41
41
  if (typeof value === "number") return Number.isFinite(value) ? String(value) : "";
42
42
  return JSON.stringify(value);
43
43
  }
44
- function extractEnumValues(spec) {
44
+ var customExtractors = /* @__PURE__ */ new Set();
45
+ function registerEnumExtractor(extractor) {
46
+ customExtractors.add(extractor);
47
+ return () => {
48
+ customExtractors.delete(extractor);
49
+ };
50
+ }
51
+ var extractZodEnum = (spec) => {
45
52
  let current = spec;
46
53
  for (let i = 0; i < 8 && current !== null && typeof current === "object"; i++) {
47
54
  const def = current._def;
@@ -59,6 +66,33 @@ function extractEnumValues(spec) {
59
66
  return void 0;
60
67
  }
61
68
  return void 0;
69
+ };
70
+ var extractValibotEnum = (spec) => {
71
+ let current = spec;
72
+ for (let i = 0; i < 8 && current !== null && typeof current === "object"; i++) {
73
+ if (current.kind !== "schema" || typeof current.type !== "string") return void 0;
74
+ if (current.type === "picklist" || current.type === "enum") {
75
+ return Array.isArray(current.options) ? current.options : void 0;
76
+ }
77
+ if (current.wrapped) {
78
+ current = current.wrapped;
79
+ continue;
80
+ }
81
+ return void 0;
82
+ }
83
+ return void 0;
84
+ };
85
+ var builtInExtractors = [extractZodEnum, extractValibotEnum];
86
+ function extractEnumValues(spec) {
87
+ for (const extractor of customExtractors) {
88
+ const result = extractor(spec);
89
+ if (result !== void 0) return result;
90
+ }
91
+ for (const extractor of builtInExtractors) {
92
+ const result = extractor(spec);
93
+ if (result !== void 0) return result;
94
+ }
95
+ return void 0;
62
96
  }
63
97
 
64
98
  export {
@@ -67,6 +101,7 @@ export {
67
101
  getDefault,
68
102
  parseField,
69
103
  defaultSerialize,
104
+ registerEnumExtractor,
70
105
  extractEnumValues
71
106
  };
72
- //# sourceMappingURL=chunk-NUO3GOXV.js.map
107
+ //# sourceMappingURL=chunk-5UKBDZTP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec'\n\nimport type { FieldSpec, PlainFieldSpec } from './types'\n\nexport function isStandardSchema(spec: unknown): spec is StandardSchemaV1 {\n return (\n typeof spec === 'object' &&\n spec !== null &&\n '~standard' in spec &&\n typeof (spec as { '~standard'?: unknown })['~standard'] === 'object'\n )\n}\n\nexport function isPlainSpec<T>(spec: FieldSpec<T>): spec is PlainFieldSpec<T> {\n return !isStandardSchema(spec) && typeof spec === 'object' && spec !== null && 'default' in spec\n}\n\n/**\n * Resolve a field's default value.\n *\n * - Plain spec: `spec.default` (always present — required by the type).\n * - Standard Schema (Zod, Valibot, ArkType, …): parse `undefined` and use the\n * schema's `.default()` if it produced a value. Schemas without a default\n * return `undefined`.\n */\nexport function getDefault<T>(spec: FieldSpec<T>): T | undefined {\n if (isStandardSchema(spec)) {\n const result = spec['~standard'].validate(undefined)\n if (result instanceof Promise) return undefined\n if ('value' in result && !result.issues) return result.value as T\n return undefined\n }\n return (spec as PlainFieldSpec<T>).default\n}\n\n/**\n * Parse a raw storage value through the field's spec. Returns the typed value\n * on success, or `undefined` on parse failure (engine falls back to default).\n *\n * The engine records the failure reason in `ParamsStore.storageErrors` for\n * debugging; consumer code doesn't see it.\n */\nexport interface ParseResult<T> {\n ok: boolean\n value?: T\n reason?: string\n}\n\nexport function parseField<T>(spec: FieldSpec<T>, raw: unknown): ParseResult<T> {\n if (isStandardSchema(spec)) {\n const result = spec['~standard'].validate(raw)\n if (result instanceof Promise) {\n return { ok: false, reason: 'async-schema-not-supported-in-v0.1' }\n }\n if ('value' in result && !result.issues) return { ok: true, value: result.value as T }\n return { ok: false, reason: result.issues?.[0]?.message ?? 'parse-failed' }\n }\n\n const plainSpec = spec as PlainFieldSpec<T>\n if (typeof raw === 'string' && plainSpec.parse) {\n try {\n return { ok: true, value: plainSpec.parse(raw) }\n } catch (err) {\n return { ok: false, reason: err instanceof Error ? err.message : String(err) }\n }\n }\n // No custom parse: accept raw value as-is if it's a usable runtime value\n // (string, number, boolean, array, object, etc.). The schema-less plain spec\n // is mostly used for non-string-coerced flows (memory storage with raw values).\n if (raw === undefined) return { ok: false, reason: 'undefined-no-parse' }\n return { ok: true, value: raw as T }\n}\n\n/**\n * Serialize a typed value to its storage string representation. Used by URL\n * storage (every value must be a string) and other string-keyed backends.\n *\n * Plain spec uses its `serialize` if provided, else `String(value)`. Standard\n * schemas don't define an inverse — we fall back to `String(value)` (or\n * `JSON.stringify` for objects) and the consumer can override via per-field\n * `serialize` in the storage backend's options.\n */\nexport function defaultSerialize(value: unknown): string {\n if (value === undefined || value === null) return ''\n if (typeof value === 'string') return value\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n return JSON.stringify(value)\n}\n\n/**\n * Probe a schema spec and return its enum members, or `undefined` when the\n * spec doesn't expose any. Used by {@link extractEnumValues} to discover the\n * cycle values for `params.cycle(path)` calls without an explicit list.\n *\n * Built-ins ship for Zod and Valibot. Other Standard Schema libs (ArkType,\n * Effect Schema) lack a uniform introspection API — register a custom\n * extractor via {@link registerEnumExtractor} for those.\n */\nexport type EnumExtractor = (spec: unknown) => readonly unknown[] | undefined\n\nconst customExtractors = new Set<EnumExtractor>()\n\n/**\n * Register a custom enum extractor. Useful when:\n * - Your Standard Schema lib isn't covered by the built-ins (ArkType,\n * Effect Schema, …).\n * - You're using a proprietary or homegrown schema system.\n * - You need to override the built-in detection for a specific shape.\n *\n * Custom extractors run BEFORE the built-ins, so they win on overlap. Returns\n * an unregister function — capture it for clean teardown in tests or HMR.\n *\n * @example\n * ```ts\n * import { registerEnumExtractor } from '@victorylabs/params'\n * import { type } from 'arktype'\n *\n * const off = registerEnumExtractor((spec) => {\n * if (spec instanceof type.Type && spec.json?.length) {\n * return spec.json\n * .filter((node) => 'unit' in node)\n * .map((node) => node.unit)\n * }\n * return undefined\n * })\n * ```\n */\nexport function registerEnumExtractor(extractor: EnumExtractor): () => void {\n customExtractors.add(extractor)\n return () => {\n customExtractors.delete(extractor)\n }\n}\n\n/**\n * Walk Zod wrapper types (ZodDefault, ZodOptional, ZodNullable, …) to find\n * an underlying enum. `.default(...)` / `.optional()` / `.nullable()` produce\n * a wrapper whose `_def.innerType` points to the wrapped schema. Cap the walk\n * at a small depth — pathological self-referential schemas shouldn't loop.\n */\nconst extractZodEnum: EnumExtractor = (spec) => {\n // biome-ignore lint/suspicious/noExplicitAny: Zod internal `_def` API\n let current: any = spec\n for (let i = 0; i < 8 && current !== null && typeof current === 'object'; i++) {\n const def = current._def\n if (!def) return undefined\n\n // z.enum(['a', 'b']): values is already a clean string array.\n if (Array.isArray(def.values)) return def.values\n\n // z.nativeEnum(MyEnum): values is the enum object. TS numeric enums are\n // BIDIRECTIONAL — Object.values returns numbers + reverse-mapped strings:\n // enum Color { Red = 0 } → Object.values(Color) = [0, 'Red'].\n // Detect numeric and filter; pure string enums are unidirectional and\n // return only strings.\n if (typeof def.values === 'object' && def.values !== null) {\n const all = Object.values(def.values)\n const hasNumber = all.some((v) => typeof v === 'number')\n return hasNumber ? all.filter((v) => typeof v === 'number') : all\n }\n\n if (def.innerType) {\n current = def.innerType\n continue\n }\n return undefined\n }\n return undefined\n}\n\n/**\n * Walk Valibot wrapper schemas (`optional`, `nullable`, `nullish`) via the\n * `wrapped` property, then read the enum members from `picklist` / `enum`\n * schemas.\n *\n * - `v.picklist(['a','b'])` → `options: ['a','b']`.\n * - `v.enum_(MyEnum)` (or `v.enum(...)` in v1+) → `options: [...]` already\n * filtered for numeric-enum bidirectional reverse mapping by Valibot.\n *\n * Confirmed against valibot@1.3.1.\n */\nconst extractValibotEnum: EnumExtractor = (spec) => {\n // biome-ignore lint/suspicious/noExplicitAny: Valibot internal schema shape\n let current: any = spec\n for (let i = 0; i < 8 && current !== null && typeof current === 'object'; i++) {\n if (current.kind !== 'schema' || typeof current.type !== 'string') return undefined\n\n if (current.type === 'picklist' || current.type === 'enum') {\n return Array.isArray(current.options) ? current.options : undefined\n }\n\n // Wrapper schemas — `optional`, `nullable`, `nullish`, `undefinedable`.\n // Each carries a `wrapped` property pointing to the inner schema.\n if (current.wrapped) {\n current = current.wrapped\n continue\n }\n return undefined\n }\n return undefined\n}\n\nconst builtInExtractors: readonly EnumExtractor[] = [extractZodEnum, extractValibotEnum]\n\n/**\n * Extract the enum values from a field spec, if any. Returns the array of\n * enum members or `undefined` if the spec doesn't expose them.\n *\n * Built-in support:\n * - Zod `z.enum(['a', 'b'])` and `z.nativeEnum(MyEnum)`, including wrapped\n * variants (`.default()`, `.optional()`, `.nullable()`).\n * - Valibot `v.picklist(['a', 'b'])` and `v.enum_(MyEnum)`, including wrapped\n * variants (`v.optional(...)`, `v.nullable(...)`, `v.nullish(...)`).\n *\n * For other Standard Schema libs (ArkType, Effect Schema) or custom shapes,\n * register a probe via {@link registerEnumExtractor}. Custom extractors run\n * before the built-ins. When nothing matches, `cycle()` throws and asks the\n * caller to pass options explicitly.\n */\nexport function extractEnumValues(spec: unknown): readonly unknown[] | undefined {\n for (const extractor of customExtractors) {\n const result = extractor(spec)\n if (result !== undefined) return result\n }\n for (const extractor of builtInExtractors) {\n const result = extractor(spec)\n if (result !== undefined) return result\n }\n return undefined\n}\n"],"mappings":"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","names":[]}
@@ -15,7 +15,7 @@ import {
15
15
  isPlainSpec,
16
16
  isStandardSchema,
17
17
  parseField
18
- } from "./chunk-NUO3GOXV.js";
18
+ } from "./chunk-5UKBDZTP.js";
19
19
 
20
20
  // ../utils/src/cascade.ts
21
21
  var DEPTH_CAP = 10;
@@ -195,17 +195,43 @@ var ParamsStore = class {
195
195
  getFieldConfig(path) {
196
196
  return this.fieldConfigs[path];
197
197
  }
198
- set(pathOrPartial, valueOrOptions, maybeOptions) {
198
+ // ─── Writes ───────────────────────────────────────────────────────────
199
+ /**
200
+ * Apply a partial update — write multiple fields at once.
201
+ *
202
+ * `set(partial)` is the canonical write form. The path-and-value form
203
+ * (`set(path, value)` historically) is now `setField(path, value)` —
204
+ * see {@link setField}. The split keeps `set`'s single-signature
205
+ * inference clean for the common `set({ [key]: value })` pattern, which
206
+ * previously required `as Partial<T>` casts because TypeScript couldn't
207
+ * resolve the overload from a generic-keyed object literal.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * p.set({ page: 1, sort: 'updated' })
212
+ * p.set({ [key]: value }) // works without a cast now
213
+ * ```
214
+ */
215
+ set(partial, options) {
199
216
  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
- }
217
+ this.applyUpdates(partial, options);
218
+ }
219
+ /**
220
+ * Write a single field by path. Equivalent to `set({ [path]: value })`
221
+ * but skips the object-literal allocation and avoids the inference issue
222
+ * that motivated splitting the API.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * p.setField('query', 'react')
227
+ * p.setField('filters.tags', ['ts', 'react'])
228
+ * ```
229
+ */
230
+ setField(path, value, options) {
231
+ if (this.disposed) return;
232
+ this.applyUpdates({ [path]: value }, options);
233
+ }
234
+ applyUpdates(updates, options) {
209
235
  const initialChanges = {};
210
236
  for (const [path, v] of Object.entries(updates)) {
211
237
  const old = deepGet(this.values, path);
@@ -237,13 +263,13 @@ var ParamsStore = class {
237
263
  /** Boolean-flip helper. */
238
264
  toggle(path, options) {
239
265
  const current = this.getValue(path);
240
- this.set(path, !current, options);
266
+ this.setField(path, !current, options);
241
267
  }
242
268
  /** Push a value onto an array field. */
243
269
  append(path, value, options) {
244
270
  const current = this.getValue(path);
245
271
  if (!Array.isArray(current)) return;
246
- this.set(path, [...current, value], options);
272
+ this.setField(path, [...current, value], options);
247
273
  }
248
274
  /** Remove the first array element matching `value` by deepEqual. */
249
275
  remove(path, value, options) {
@@ -251,14 +277,14 @@ var ParamsStore = class {
251
277
  if (!Array.isArray(current)) return;
252
278
  const idx = current.findIndex((item) => deepEqual(item, value));
253
279
  if (idx === -1) return;
254
- this.set(path, [...current.slice(0, idx), ...current.slice(idx + 1)], options);
280
+ this.setField(path, [...current.slice(0, idx), ...current.slice(idx + 1)], options);
255
281
  }
256
282
  /** Remove the array element at the given index. */
257
283
  removeAt(path, index, options) {
258
284
  const current = this.getValue(path);
259
285
  if (!Array.isArray(current)) return;
260
286
  if (index < 0 || index >= current.length) return;
261
- this.set(path, [...current.slice(0, index), ...current.slice(index + 1)], options);
287
+ this.setField(path, [...current.slice(0, index), ...current.slice(index + 1)], options);
262
288
  }
263
289
  cycle(path, valuesOrOptions, maybeOptions) {
264
290
  let resolved;
@@ -284,12 +310,12 @@ var ParamsStore = class {
284
310
  const current = this.getValue(path);
285
311
  const idx = resolved.findIndex((o) => deepEqual(o, current));
286
312
  const next = idx === -1 ? resolved[0] : resolved[(idx + 1) % resolved.length];
287
- this.set(path, next, setOpts);
313
+ this.setField(path, next, setOpts);
288
314
  }
289
315
  /** Reset a single field to its default. */
290
316
  clear(path, options) {
291
317
  const def = deepGet(this.defaults, path);
292
- this.set(path, def, options);
318
+ this.setField(path, def, options);
293
319
  }
294
320
  /** Reset all fields to defaults; optional partial overrides + SetOptions. */
295
321
  reset(values, options) {
@@ -570,4 +596,4 @@ export {
570
596
  getCachedStore,
571
597
  releaseCachedStore
572
598
  };
573
- //# sourceMappingURL=chunk-43PUAYQP.js.map
599
+ //# sourceMappingURL=chunk-DSAHBEAQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../utils/src/cascade.ts","../../utils/src/path-trie.ts","../src/params-store.ts","../src/store-cache.ts"],"sourcesContent":["/**\n * Cascade resolver for `dependsOn` / `excludeDeps` / `onDepChange` field configs.\n * Used by both `@victorylabs/forms` and `@victorylabs/params` v0.2.\n *\n * Pure function — no side effects, no I/O. Consumers wire the result into their\n * own commit phase (params: PathTrie notify; forms: notify + revalidate + dirty/error reset).\n *\n * Algorithm: BFS over the dependency graph.\n * - Each round computes the next layer of cascaded changes.\n * - Visited paths are tracked to detect cycles (warn + skip re-entry).\n * - Depth cap stops runaway chains at 10 levels.\n *\n * The `'*'` wildcard for `dependsOn` is resolved at cascade time (not config\n * time), so adding fields to a definition doesn't invalidate `'*'` consumers.\n */\n\nconst DEPTH_CAP = 10\n\nexport interface CascadeFieldConfig {\n /**\n * Paths whose changes trigger this field's `onDepChange`. `'*'` means\n * \"every other field\"; combine with `excludeDeps` to carve out exceptions.\n */\n dependsOn?: string[] | '*'\n\n /**\n * Paths to exclude from the `'*'` wildcard. Only meaningful when\n * `dependsOn === '*'`. Implicitly always includes the field itself\n * (a field never depends on itself).\n */\n excludeDeps?: string[]\n\n /**\n * What to do when a dep changes. `'reset'` restores the field's default;\n * `'clear'` sets it to `undefined`; a function returns the new value.\n * The function form receives ALL dep values (changed + unchanged) keyed\n * by path, plus the field's own current value.\n */\n onDepChange?: 'reset' | 'clear' | ((deps: Record<string, unknown>, current: unknown) => unknown)\n}\n\nexport interface CascadeContext {\n /** Per-field config keyed by path. Fields without cascade config are inert. */\n fieldConfigs: Record<string, CascadeFieldConfig>\n\n /** Default values keyed by path — resolves `onDepChange === 'reset'`. */\n defaults: Record<string, unknown>\n\n /** Current values keyed by path — resolves the function-form `onDepChange`. */\n currentValues: Record<string, unknown>\n\n /** Every path in the definition — resolves `'*'` wildcard. */\n allPaths: string[]\n}\n\nexport interface CascadeResult {\n /** Initial changes plus all cascaded changes, keyed by path. */\n changes: Record<string, unknown>\n\n /** Cycle-detection / depth-cap warnings (one entry per anomaly). */\n warnings: string[]\n}\n\n/**\n * Resolve a cascade. Given a set of initial changes, compute every dependent\n * field that should also change.\n */\nexport function resolveCascade(\n initialChanges: Record<string, unknown>,\n ctx: CascadeContext,\n): CascadeResult {\n const changes: Record<string, unknown> = { ...initialChanges }\n const warnings: string[] = []\n\n // Working values during BFS — start from current, override with initial changes.\n // Each round's cascaded changes accumulate here so dependent fields see them.\n const working: Record<string, unknown> = { ...ctx.currentValues, ...initialChanges }\n\n // Seed: paths changed in this round become the next round's triggers.\n let frontier = new Set(Object.keys(initialChanges))\n // Cycle guard: if a path is re-targeted within the same cascade, warn + skip.\n const visited = new Set<string>(frontier)\n\n let depth = 0\n while (frontier.size > 0) {\n if (depth >= DEPTH_CAP) {\n warnings.push(\n `cascade depth cap (${DEPTH_CAP}) reached; stopping. Frontier: [${[...frontier].join(', ')}]. Likely a config error — check for accidental long chains.`,\n )\n break\n }\n\n const nextFrontier = new Set<string>()\n\n for (const targetPath of ctx.allPaths) {\n const config = ctx.fieldConfigs[targetPath]\n if (!config?.onDepChange) continue\n if (frontier.has(targetPath)) continue // a path doesn't cascade to itself\n\n const deps = resolveDeps(config, ctx.allPaths, targetPath)\n // Did any of this field's deps change in the current frontier?\n const changedDeps = deps.filter((d) => frontier.has(d))\n if (changedDeps.length === 0) continue\n\n if (visited.has(targetPath)) {\n warnings.push(\n `cycle detected: '${targetPath}' would cascade twice (triggered by [${changedDeps.join(', ')}]). Skipping re-entry.`,\n )\n continue\n }\n\n // Compute the new value via onDepChange.\n const newValue = applyDepChange(config.onDepChange, deps, working, ctx.defaults, targetPath)\n\n // Skip no-op cascades: if the resolved value equals the field's current\n // working value, don't propagate further (avoids spurious re-renders).\n if (Object.is(newValue, working[targetPath])) continue\n\n changes[targetPath] = newValue\n working[targetPath] = newValue\n visited.add(targetPath)\n nextFrontier.add(targetPath)\n }\n\n frontier = nextFrontier\n depth++\n }\n\n return { changes, warnings }\n}\n\n/**\n * Resolve a field's effective dependency list. `'*'` expands to every OTHER\n * path minus `excludeDeps` and the field itself.\n */\nfunction resolveDeps(config: CascadeFieldConfig, allPaths: string[], selfPath: string): string[] {\n if (config.dependsOn === '*') {\n const exclude = new Set(config.excludeDeps ?? [])\n exclude.add(selfPath) // a field doesn't depend on itself\n return allPaths.filter((p) => !exclude.has(p))\n }\n if (Array.isArray(config.dependsOn)) {\n // Filter out self-references defensively (config error otherwise).\n return config.dependsOn.filter((p) => p !== selfPath)\n }\n return []\n}\n\n/**\n * Compute the new value for a cascaded field via its `onDepChange` config.\n */\nfunction applyDepChange(\n onDepChange: NonNullable<CascadeFieldConfig['onDepChange']>,\n deps: string[],\n working: Record<string, unknown>,\n defaults: Record<string, unknown>,\n targetPath: string,\n): unknown {\n if (onDepChange === 'reset') return defaults[targetPath]\n if (onDepChange === 'clear') return undefined\n\n // Function form: build the deps record from the working values\n // (so cascaded changes within the same set() call are visible).\n const depsRecord: Record<string, unknown> = {}\n for (const d of deps) {\n depsRecord[d] = working[d]\n }\n return onDepChange(depsRecord, working[targetPath])\n}\n","import { splitPath } from './deep'\n\ninterface Node {\n listeners: Set<() => void>\n children: Map<string, Node>\n}\n\nconst makeNode = (): Node => ({ listeners: new Set(), children: new Map() })\n\n/**\n * Path-scoped subscription bus.\n *\n * Notification semantics for `notify(path)`:\n * - Subscribers AT `path` fire (exact match).\n * - Subscribers at every ANCESTOR of `path` fire (a parent observing the\n * aggregated subtree should re-render when any descendant changes).\n * - Subscribers at every DESCENDANT of `path` fire (a subtree replacement\n * at `path` invalidates any leaf below it).\n * - Sibling-branch subscribers do NOT fire.\n *\n * notify('') notifies the root + every subscriber in the trie.\n */\nexport class PathTrie {\n private readonly root: Node = makeNode()\n\n subscribe(path: string, listener: () => void): () => void {\n const node = this.ensure(path)\n node.listeners.add(listener)\n return () => {\n node.listeners.delete(listener)\n }\n }\n\n notify(path: string): void {\n const segments = splitPath(path)\n\n let current: Node = this.root\n fire(current)\n\n for (const seg of segments) {\n const next = current.children.get(seg)\n if (!next) return\n current = next\n fire(current)\n }\n\n fireSubtree(current)\n }\n\n /** Test/diagnostic helper — total subscriber count below a given path. */\n size(path = ''): number {\n const node = this.find(path)\n if (!node) return 0\n return countSubtree(node)\n }\n\n private ensure(path: string): Node {\n let node = this.root\n for (const seg of splitPath(path)) {\n let child = node.children.get(seg)\n if (!child) {\n child = makeNode()\n node.children.set(seg, child)\n }\n node = child\n }\n return node\n }\n\n private find(path: string): Node | undefined {\n let node: Node | undefined = this.root\n for (const seg of splitPath(path)) {\n node = node?.children.get(seg)\n if (!node) return undefined\n }\n return node\n }\n}\n\nfunction fire(node: Node): void {\n for (const listener of node.listeners) listener()\n}\n\nfunction fireSubtree(node: Node): void {\n for (const child of node.children.values()) {\n fire(child)\n fireSubtree(child)\n }\n}\n\nfunction countSubtree(node: Node): number {\n let total = node.listeners.size\n for (const child of node.children.values()) {\n total += countSubtree(child)\n }\n return total\n}\n","import { deepEqual, deepGet, deepSet, PathTrie, resolveCascade } from '@victorylabs/utils'\n\nimport { warn } from './dev'\nimport { takePreHydrationValues } from './name-registry'\nimport {\n defaultSerialize,\n extractEnumValues,\n getDefault,\n isPlainSpec,\n isStandardSchema,\n parseField,\n} from './schema'\nimport type { ParamsStorage, WriteOptions } from './storage'\nimport type { FieldConfig, FieldSpec, ParamsDefinition } from './types'\n\n/**\n * Per-call write options accepted by `set()` and `clear()`. Currently shapes\n * the URL `history` strategy override; backends ignore fields they don't\n * recognize.\n */\nexport interface SetOptions {\n /** History API override for URL-like backends. `'push'` beats `'replace'` across batched writes. */\n readonly history?: 'push' | 'replace'\n}\n\n/**\n * Frozen, defensive-copied snapshot returned by `ParamsStore.__introspect()`.\n *\n * Surface is **unstable** — fields may be added freely across releases; do\n * not depend on the type identity. Use for devtools panels, debug logging,\n * or test assertions that don't fit through the public observable API.\n */\nexport interface ParamsStoreIntrospection<T = Record<string, unknown>> {\n readonly values: Readonly<Partial<T>>\n readonly defaults: Readonly<Partial<T>>\n readonly fieldsConfigured: Readonly<Record<string, FieldConfig>>\n readonly fieldsBySpecType: Readonly<Record<string, 'zod' | 'standard-schema' | 'plain'>>\n readonly storageErrors: Readonly<Record<string, string>>\n readonly storage: {\n readonly name: string\n readonly clientOnly: boolean\n readonly hasReadAsync: boolean\n readonly hasSubscribe: boolean\n readonly hasClear: boolean\n }\n readonly lastWritten: Readonly<Partial<T>> | undefined\n}\n\nconst isClient = typeof window !== 'undefined'\n\n/**\n * Framework-agnostic store. Owns:\n * - values + storage round-tripping\n * - schema validation on read (silent fallback to defaults)\n * - PathTrie-based subscriptions\n * - all state-mutation helpers (set, toggle, append, …)\n * - loop prevention against external storage echoes\n * - memoized toQuery\n *\n * The React adapter (`@victorylabs/params/react`) wraps this with\n * useSyncExternalStore-style hooks; non-React consumers use the methods\n * directly via `getParamsStore(def)`.\n */\nexport class ParamsStore<T = Record<string, unknown>> {\n private readonly spec: Readonly<Record<string, FieldSpec>>\n private readonly storage: ParamsStorage<T>\n private readonly fieldConfigs: Readonly<Record<string, FieldConfig>>\n\n private values: Partial<T>\n private readonly defaults: Partial<T>\n private readonly trie = new PathTrie()\n private readonly storageErrorMap = new Map<string, string>()\n\n private lastWritten: Partial<T> | undefined\n private storageUnsubscribe: (() => void) | undefined\n private toQueryCache: { values: Partial<T>; query: string; href?: string } | undefined\n private disposed = false\n\n constructor(def: ParamsDefinition<T>) {\n this.spec = def.spec\n this.storage = def.storage\n this.fieldConfigs = def.fields\n\n this.defaults = this.computeDefaults()\n this.values = { ...this.defaults }\n\n // SSR pre-hydration: if hydrateParams() seeded values for this def's name,\n // those win over the storage backend's read() — the snapshot represents\n // the authoritative server-rendered state.\n const seeded = takePreHydrationValues<T>(def.name)\n if (seeded !== undefined) {\n this.values = { ...this.defaults, ...seeded }\n } else if (this.storage.clientOnly && !isClient) {\n // Server: skip read; values stay at defaults\n } else {\n this.hydrateFromStorage()\n }\n\n if (this.storage.subscribe) {\n this.storageUnsubscribe = this.storage.subscribe((raw) => this.onExternalChange(raw))\n }\n }\n\n // ─── Reads (synchronous, framework-agnostic) ──────────────────────────\n\n getValues(): Readonly<Partial<T>> {\n return this.values\n }\n\n getValue<P extends string>(path: P): unknown {\n return deepGet(this.values, path)\n }\n\n /** Storage parse failures discovered on hydrate or external change. */\n get storageErrors(): Readonly<Record<string, string>> {\n const out: Record<string, string> = {}\n for (const [path, reason] of this.storageErrorMap) out[path] = reason\n return out\n }\n\n /** Per-field config (for the React adapter to read debounce settings, etc.). */\n getFieldConfig(path: string): FieldConfig | undefined {\n return this.fieldConfigs[path]\n }\n\n // ─── Writes ───────────────────────────────────────────────────────────\n\n /**\n * Apply a partial update — write multiple fields at once.\n *\n * `set(partial)` is the canonical write form. The path-and-value form\n * (`set(path, value)` historically) is now `setField(path, value)` —\n * see {@link setField}. The split keeps `set`'s single-signature\n * inference clean for the common `set({ [key]: value })` pattern, which\n * previously required `as Partial<T>` casts because TypeScript couldn't\n * resolve the overload from a generic-keyed object literal.\n *\n * @example\n * ```ts\n * p.set({ page: 1, sort: 'updated' })\n * p.set({ [key]: value }) // works without a cast now\n * ```\n */\n set(partial: Partial<T>, options?: SetOptions): void {\n if (this.disposed) return\n this.applyUpdates(partial as Record<string, unknown>, options)\n }\n\n /**\n * Write a single field by path. Equivalent to `set({ [path]: value })`\n * but skips the object-literal allocation and avoids the inference issue\n * that motivated splitting the API.\n *\n * @example\n * ```ts\n * p.setField('query', 'react')\n * p.setField('filters.tags', ['ts', 'react'])\n * ```\n */\n setField(path: string, value: unknown, options?: SetOptions): void {\n if (this.disposed) return\n this.applyUpdates({ [path]: value }, options)\n }\n\n private applyUpdates(updates: Record<string, unknown>, options?: SetOptions): void {\n // First pass: filter out no-op updates (value already deep-equals current).\n const initialChanges: Record<string, unknown> = {}\n for (const [path, v] of Object.entries(updates)) {\n const old = deepGet(this.values, path)\n if (deepEqual(old, v)) continue\n initialChanges[path] = v\n }\n if (Object.keys(initialChanges).length === 0) return\n\n // Resolve cascading dependencies (no-op when no field has dependsOn).\n const cascade = resolveCascade(initialChanges, {\n fieldConfigs: this.fieldConfigs,\n defaults: this.defaults as Record<string, unknown>,\n currentValues: this.values as Record<string, unknown>,\n allPaths: Object.keys(this.spec),\n })\n\n for (const w of cascade.warnings) warn(w)\n\n // Second pass: apply initial + cascaded changes atomically.\n const changed: string[] = []\n let next = this.values\n for (const [path, v] of Object.entries(cascade.changes)) {\n const old = deepGet(next, path)\n if (deepEqual(old, v)) continue\n next = deepSet(next, path, v) as Partial<T>\n changed.push(path)\n }\n if (changed.length === 0) return\n\n this.values = next\n this.invalidateToQueryCache()\n\n // Notify subscribers (React 18 batches these into one render per consumer)\n for (const path of changed) this.trie.notify(path)\n\n // Persist to storage with omitWhenDefault filtering. Per-call options\n // (history strategy) flow straight through to storage.write.\n this.persistToStorage(changed, options)\n }\n\n /** Boolean-flip helper. */\n toggle(path: string, options?: SetOptions): void {\n const current = this.getValue(path)\n this.setField(path, !current, options)\n }\n\n /** Push a value onto an array field. */\n append(path: string, value: unknown, options?: SetOptions): void {\n const current = this.getValue(path)\n if (!Array.isArray(current)) return\n this.setField(path, [...current, value], options)\n }\n\n /** Remove the first array element matching `value` by deepEqual. */\n remove(path: string, value: unknown, options?: SetOptions): void {\n const current = this.getValue(path)\n if (!Array.isArray(current)) return\n const idx = current.findIndex((item) => deepEqual(item, value))\n if (idx === -1) return\n this.setField(path, [...current.slice(0, idx), ...current.slice(idx + 1)], options)\n }\n\n /** Remove the array element at the given index. */\n removeAt(path: string, index: number, options?: SetOptions): void {\n const current = this.getValue(path)\n if (!Array.isArray(current)) return\n if (index < 0 || index >= current.length) return\n this.setField(path, [...current.slice(0, index), ...current.slice(index + 1)], options)\n }\n\n /**\n * Cycle the value through the given values (e.g., 'asc' → 'desc' → 'asc').\n * If the current value isn't in the values, jumps to the first one.\n *\n * v0.4: when called without an explicit values array, derives the rotation\n * from the field's Zod `z.enum` / `z.nativeEnum` schema. Throws if the schema\n * doesn't expose enum metadata.\n *\n * v0.5: optional `SetOptions` arg (history strategy) on every overload.\n */\n cycle(path: string, options?: SetOptions): void\n cycle(path: string, values: ReadonlyArray<unknown>, options?: SetOptions): void\n cycle(\n path: string,\n valuesOrOptions?: ReadonlyArray<unknown> | SetOptions,\n maybeOptions?: SetOptions,\n ): void {\n // Disambiguate the second argument: a ReadonlyArray means explicit values;\n // a non-array object means SetOptions; undefined means \"derive from schema\".\n let resolved: ReadonlyArray<unknown> | undefined\n let setOpts: SetOptions | undefined\n if (Array.isArray(valuesOrOptions)) {\n resolved = valuesOrOptions\n setOpts = maybeOptions\n } else {\n resolved = undefined\n setOpts = valuesOrOptions as SetOptions | undefined\n }\n\n if (resolved === undefined) {\n const fieldSpec = this.spec[path]\n const derived = fieldSpec !== undefined ? extractEnumValues(fieldSpec) : undefined\n if (!derived || derived.length === 0) {\n throw new Error(\n `Cannot derive enum values for params field '${path}'; pass values explicitly to cycle().`,\n )\n }\n resolved = derived\n }\n if (resolved.length === 0) return\n const current = this.getValue(path)\n const idx = resolved.findIndex((o) => deepEqual(o, current))\n const next = idx === -1 ? resolved[0] : resolved[(idx + 1) % resolved.length]\n this.setField(path, next, setOpts)\n }\n\n /** Reset a single field to its default. */\n clear(path: string, options?: SetOptions): void {\n const def = deepGet(this.defaults, path)\n this.setField(path, def, options)\n }\n\n /** Reset all fields to defaults; optional partial overrides + SetOptions. */\n reset(values?: Partial<T>, options?: SetOptions): void {\n if (this.disposed) return\n const next = values ? { ...this.defaults, ...values } : { ...this.defaults }\n const changed = Object.keys({ ...this.values, ...next })\n this.values = next\n this.invalidateToQueryCache()\n for (const path of changed) this.trie.notify(path)\n const writeOptions: WriteOptions | undefined =\n options?.history !== undefined ? { history: options.history } : undefined\n void this.storage.clear?.([], writeOptions)\n this.lastWritten = undefined\n }\n\n // ─── Subscriptions ────────────────────────────────────────────────────\n\n subscribe(path: string, listener: () => void): () => void {\n return this.trie.subscribe(path, listener)\n }\n\n // ─── URL helpers ──────────────────────────────────────────────────────\n\n toQuery(overrides?: Partial<T>): string {\n const effective = overrides\n ? { ...(this.values as Record<string, unknown>), ...(overrides as Record<string, unknown>) }\n : (this.values as Record<string, unknown>)\n\n if (!overrides && this.toQueryCache && this.toQueryCache.values === this.values) {\n return this.toQueryCache.query\n }\n\n const params = new URLSearchParams()\n const keys = Object.keys(effective).sort()\n for (const key of keys) {\n const value = effective[key]\n if (value === undefined || value === null) continue\n const config = this.fieldConfigs[key]\n const def = deepGet(this.defaults, key)\n if (config?.omitWhenDefault && deepEqual(value, def)) continue\n params.set(key, defaultSerialize(value))\n }\n\n const str = params.toString()\n const out = str ? `?${str}` : ''\n if (!overrides) this.toQueryCache = { values: this.values, query: out }\n return out\n }\n\n href(overrides?: Partial<T>): string {\n // Memoize the no-overrides case alongside toQuery's cache. Pathname is\n // read live (could change via routing within a page), so the cache key\n // includes (values, pathname). With overrides, no caching — fresh string.\n if (!overrides) {\n const query = this.toQuery()\n // toQuery() refreshed the cache for `this.values`; safe to extend it.\n const pathname = isClient ? window.location.pathname : ''\n const cached = this.toQueryCache\n if (cached && cached.values === this.values && cached.href !== undefined) {\n // Confirm pathname hasn't changed — if it has, recompute.\n const expected = pathname + query\n if (cached.href === expected) return cached.href\n }\n const out = pathname + query\n if (cached && cached.values === this.values) cached.href = out\n return out\n }\n const pathname = isClient ? window.location.pathname : ''\n return pathname + this.toQuery(overrides)\n }\n\n // ─── Disposal ─────────────────────────────────────────────────────────\n\n dispose(): void {\n if (this.disposed) return\n this.disposed = true\n this.storageUnsubscribe?.()\n this.storageUnsubscribe = undefined\n }\n\n // ─── Devtools / debugging escape hatch (v0.5) ─────────────────────────\n\n /**\n * Devtools / debugging escape hatch. Returns a frozen, defensive-copied\n * snapshot of all internal state. Pure read — no side effects, no\n * subscriptions registered.\n *\n * Uses `structuredClone` for the values + defaults deep-copy. Because\n * params values round-trip through storage backends, they're already\n * restricted to JSON-safe shapes — so `__introspect()` is safer here than\n * on `FormStore`. Still: marked unstable via the `__` prefix; field set\n * may grow over releases.\n */\n __introspect(): ParamsStoreIntrospection<T> {\n const specTypes: Record<string, 'zod' | 'standard-schema' | 'plain'> = {}\n for (const [path, spec] of Object.entries(this.spec)) {\n if (isPlainSpec(spec)) {\n specTypes[path] = 'plain'\n continue\n }\n // biome-ignore lint/suspicious/noExplicitAny: Zod internal `_def` API\n if ((spec as any)?._def) {\n specTypes[path] = 'zod'\n continue\n }\n if (isStandardSchema(spec)) {\n specTypes[path] = 'standard-schema'\n continue\n }\n specTypes[path] = 'plain'\n }\n\n let valuesClone: Partial<T>\n let defaultsClone: Partial<T>\n let lastWrittenClone: Partial<T> | undefined\n try {\n valuesClone = structuredClone(this.values)\n defaultsClone = structuredClone(this.defaults)\n lastWrittenClone = this.lastWritten ? structuredClone(this.lastWritten) : undefined\n } catch (err) {\n throw new Error(\n 'ParamsStore.__introspect() failed: values contain non-cloneable data ' +\n '(functions, Symbols, DOM nodes are not allowed in params state). ' +\n `Original error: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n\n return Object.freeze({\n values: valuesClone,\n defaults: defaultsClone,\n fieldsConfigured: Object.freeze({ ...this.fieldConfigs }),\n fieldsBySpecType: Object.freeze(specTypes),\n storageErrors: Object.freeze({ ...this.storageErrors }),\n storage: Object.freeze({\n name: this.storage.name,\n clientOnly: this.storage.clientOnly === true,\n hasReadAsync: this.storage.readAsync !== undefined,\n hasSubscribe: this.storage.subscribe !== undefined,\n hasClear: this.storage.clear !== undefined,\n }),\n lastWritten: lastWrittenClone,\n })\n }\n\n // ─── Internals ────────────────────────────────────────────────────────\n\n private computeDefaults(): Partial<T> {\n const out: Record<string, unknown> = {}\n for (const [key, fieldSpec] of Object.entries(this.spec)) {\n const def = getDefault(fieldSpec)\n if (def !== undefined) out[key] = def\n }\n return out as Partial<T>\n }\n\n private hydrateFromStorage(): void {\n let raw: Partial<T> | undefined\n try {\n raw = this.storage.read()\n } catch (err) {\n // Read failure → keep defaults, record reason\n this.storageErrorMap.set('__read__', err instanceof Error ? err.message : String(err))\n return\n }\n if (!raw) return\n\n for (const [key, rawValue] of Object.entries(raw)) {\n const fieldSpec = this.spec[key]\n if (!fieldSpec) continue // unknown field — ignore\n const result = parseField(fieldSpec, rawValue)\n if (result.ok && result.value !== undefined) {\n ;(this.values as Record<string, unknown>)[key] = result.value\n } else if (result.reason) {\n this.storageErrorMap.set(key, result.reason)\n }\n }\n }\n\n private persistToStorage(changed: string[], options?: SetOptions): void {\n const filtered: Record<string, unknown> = {}\n for (const path of changed) {\n const value = (this.values as Record<string, unknown>)[path]\n const config = this.fieldConfigs[path]\n const def = deepGet(this.defaults, path)\n if (config?.omitWhenDefault && deepEqual(value, def)) {\n // Mark for clear instead of write\n filtered[path] = undefined\n } else {\n filtered[path] = value\n }\n }\n\n // Forward per-call options (history strategy) to the backend. Backends\n // ignore fields they don't recognize.\n const writeOptions: WriteOptions | undefined =\n options?.history !== undefined ? { history: options.history } : undefined\n\n this.lastWritten = { ...this.values }\n try {\n // Storage write is `void | Promise<void>`. Catch sync throws here; for\n // async rejections, attach a `.catch` so the promise doesn't surface\n // as an unhandled rejection. Either way: silent fallback to in-memory\n // state — storage drift accepted (the standard storage error contract).\n const result = this.storage.write(filtered as Partial<T>, changed, writeOptions)\n if (result && typeof (result as Promise<void>).then === 'function') {\n ;(result as Promise<void>).catch(() => undefined)\n }\n } catch {\n // Sync write failure — same silent contract.\n }\n }\n\n private onExternalChange(raw: Partial<T>): void {\n if (this.disposed) return\n // Loop prevention: drop echoes that match what we just wrote\n if (this.lastWritten && deepEqual(raw, this.lastWritten)) return\n\n const changed: string[] = []\n let next = this.values\n for (const [key, rawValue] of Object.entries(raw)) {\n const fieldSpec = this.spec[key]\n if (!fieldSpec) continue\n const result = parseField(fieldSpec, rawValue)\n const newValue = result.ok ? result.value : deepGet(this.defaults, key)\n const oldValue = deepGet(next, key)\n if (deepEqual(oldValue, newValue)) continue\n next = deepSet(next, key, newValue) as Partial<T>\n changed.push(key)\n if (!result.ok && result.reason) {\n this.storageErrorMap.set(key, result.reason)\n } else {\n this.storageErrorMap.delete(key)\n }\n }\n if (changed.length === 0) return\n this.values = next\n this.invalidateToQueryCache()\n for (const path of changed) this.trie.notify(path)\n }\n\n private invalidateToQueryCache(): void {\n this.toQueryCache = undefined\n }\n}\n","import { warn } from './dev'\nimport { ParamsStore } from './params-store'\nimport type { ParamsDefinition } from './types'\n\ninterface CacheEntry<T> {\n store: ParamsStore<T>\n refCount: number\n pendingDispose?: ReturnType<typeof queueMicrotask> | null\n // Microtask scheduled disposal flag — `true` = a release-triggered disposal\n // is queued; `acquire()` cancels it by clearing this and incrementing\n // refCount.\n disposalQueued: boolean\n}\n\nconst cache = new WeakMap<ParamsDefinition, CacheEntry<unknown>>()\nconst seenNames = new Map<string, ParamsDefinition>()\n\n/**\n * Acquire (or reuse) the cached store for a definition. Increments the\n * reference count; the store is disposed only when the last subscriber\n * releases it (and the microtask-deferred disposal isn't preempted by\n * another acquire — Strict Mode safety).\n */\nexport function acquire<T>(def: ParamsDefinition<T>): ParamsStore<T> {\n warnOnDuplicateName(def)\n let entry = cache.get(def) as CacheEntry<T> | undefined\n if (!entry) {\n entry = {\n store: new ParamsStore<T>(def),\n refCount: 0,\n disposalQueued: false,\n }\n cache.set(def, entry as CacheEntry<unknown>)\n } else {\n // Cancel any pending disposal — Strict Mode and rapid mount/unmount/remount\n entry.disposalQueued = false\n }\n entry.refCount++\n return entry.store\n}\n\n/**\n * Release a previously-acquired store. When the last subscriber releases,\n * disposal is **scheduled in a microtask** — if a fresh `acquire()` arrives\n * before the microtask runs (Strict Mode dev double-invoke), the disposal\n * is cancelled and the store survives.\n */\nexport function release<T>(def: ParamsDefinition<T>): void {\n const entry = cache.get(def) as CacheEntry<T> | undefined\n if (!entry) return\n entry.refCount--\n if (entry.refCount > 0) return\n\n entry.disposalQueued = true\n queueMicrotask(() => {\n if (!entry.disposalQueued) return // cancelled by acquire()\n entry.disposalQueued = false\n disposeAndClear(def, entry)\n })\n}\n\nfunction disposeAndClear<T>(def: ParamsDefinition<T>, entry: CacheEntry<T>): void {\n entry.store.dispose()\n cache.delete(def)\n if (def.name !== undefined && seenNames.get(def.name) === def) {\n seenNames.delete(def.name)\n }\n}\n\n/**\n * Public, no-React imperative read of the cached store. The store lives\n * for the program lifetime once acquired (no ref counting). Use\n * `releaseParamsStore(def)` for explicit cleanup in long-running non-React\n * contexts (daemons, electron apps, server processes).\n *\n * Not safe for shared-process SSR contexts — see plan §SSR.\n */\nexport function getCachedStore<T>(def: ParamsDefinition<T>): ParamsStore<T> {\n let entry = cache.get(def) as CacheEntry<T> | undefined\n if (!entry) {\n entry = {\n store: new ParamsStore<T>(def),\n refCount: 0,\n disposalQueued: false,\n }\n cache.set(def, entry as CacheEntry<unknown>)\n }\n return entry.store\n}\n\n/** Explicit cleanup for non-React long-running apps. */\nexport function releaseCachedStore<T>(def: ParamsDefinition<T>): void {\n const entry = cache.get(def) as CacheEntry<T> | undefined\n if (!entry) {\n // Cache entry already gone (e.g., microtask-deferred React release ran\n // first), but the name registration may still be live. Clear it so the\n // same name can be reused without a spurious duplicate-name warning.\n if (def.name !== undefined && seenNames.get(def.name) === def) {\n seenNames.delete(def.name)\n }\n return\n }\n disposeAndClear(def, entry)\n}\n\nfunction warnOnDuplicateName(def: ParamsDefinition): void {\n if (def.name === undefined) return\n const existing = seenNames.get(def.name)\n if (existing !== undefined && existing !== def) {\n warn(\n `Duplicate definition name '${def.name}' detected. Names must be unique across definitions for v0.2 SSR snapshot keys.`,\n )\n return\n }\n seenNames.set(def.name, def)\n}\n"],"mappings":"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","names":["pathname"]}
@@ -1,4 +1,4 @@
1
- import { P as ParamsStore } from './params-store-Cgbtn53j.cjs';
1
+ import { P as ParamsStore } from './params-store-4Lcb1M_X.cjs';
2
2
  import './types-BSWKH-jw.cjs';
3
3
  import '@standard-schema/spec';
4
4
  import './storage-DBLIRR-4.cjs';
@@ -1,4 +1,4 @@
1
- import { P as ParamsStore } from './params-store-CguA9-yr.js';
1
+ import { P as ParamsStore } from './params-store-f3pmPdw3.js';
2
2
  import './types-BUmNpSyP.js';
3
3
  import '@standard-schema/spec';
4
4
  import './storage-DBLIRR-4.js';
package/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ __export(src_exports, {
29
29
  isStandardSchema: () => isStandardSchema,
30
30
  memoryStorage: () => memoryStorage,
31
31
  parseField: () => parseField,
32
+ registerEnumExtractor: () => registerEnumExtractor,
32
33
  releaseParamsStore: () => releaseParamsStore
33
34
  });
34
35
  module.exports = __toCommonJS(src_exports);
@@ -357,7 +358,14 @@ function defaultSerialize(value) {
357
358
  if (typeof value === "number") return Number.isFinite(value) ? String(value) : "";
358
359
  return JSON.stringify(value);
359
360
  }
360
- function extractEnumValues(spec) {
361
+ var customExtractors = /* @__PURE__ */ new Set();
362
+ function registerEnumExtractor(extractor) {
363
+ customExtractors.add(extractor);
364
+ return () => {
365
+ customExtractors.delete(extractor);
366
+ };
367
+ }
368
+ var extractZodEnum = (spec) => {
361
369
  let current = spec;
362
370
  for (let i = 0; i < 8 && current !== null && typeof current === "object"; i++) {
363
371
  const def = current._def;
@@ -375,6 +383,33 @@ function extractEnumValues(spec) {
375
383
  return void 0;
376
384
  }
377
385
  return void 0;
386
+ };
387
+ var extractValibotEnum = (spec) => {
388
+ let current = spec;
389
+ for (let i = 0; i < 8 && current !== null && typeof current === "object"; i++) {
390
+ if (current.kind !== "schema" || typeof current.type !== "string") return void 0;
391
+ if (current.type === "picklist" || current.type === "enum") {
392
+ return Array.isArray(current.options) ? current.options : void 0;
393
+ }
394
+ if (current.wrapped) {
395
+ current = current.wrapped;
396
+ continue;
397
+ }
398
+ return void 0;
399
+ }
400
+ return void 0;
401
+ };
402
+ var builtInExtractors = [extractZodEnum, extractValibotEnum];
403
+ function extractEnumValues(spec) {
404
+ for (const extractor of customExtractors) {
405
+ const result = extractor(spec);
406
+ if (result !== void 0) return result;
407
+ }
408
+ for (const extractor of builtInExtractors) {
409
+ const result = extractor(spec);
410
+ if (result !== void 0) return result;
411
+ }
412
+ return void 0;
378
413
  }
379
414
 
380
415
  // src/params-store.ts
@@ -425,17 +460,43 @@ var ParamsStore = class {
425
460
  getFieldConfig(path) {
426
461
  return this.fieldConfigs[path];
427
462
  }
428
- set(pathOrPartial, valueOrOptions, maybeOptions) {
463
+ // ─── Writes ───────────────────────────────────────────────────────────
464
+ /**
465
+ * Apply a partial update — write multiple fields at once.
466
+ *
467
+ * `set(partial)` is the canonical write form. The path-and-value form
468
+ * (`set(path, value)` historically) is now `setField(path, value)` —
469
+ * see {@link setField}. The split keeps `set`'s single-signature
470
+ * inference clean for the common `set({ [key]: value })` pattern, which
471
+ * previously required `as Partial<T>` casts because TypeScript couldn't
472
+ * resolve the overload from a generic-keyed object literal.
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * p.set({ page: 1, sort: 'updated' })
477
+ * p.set({ [key]: value }) // works without a cast now
478
+ * ```
479
+ */
480
+ set(partial, options) {
429
481
  if (this.disposed) return;
430
- let updates;
431
- let options;
432
- if (typeof pathOrPartial === "string") {
433
- updates = { [pathOrPartial]: valueOrOptions };
434
- options = maybeOptions;
435
- } else {
436
- updates = pathOrPartial;
437
- options = valueOrOptions;
438
- }
482
+ this.applyUpdates(partial, options);
483
+ }
484
+ /**
485
+ * Write a single field by path. Equivalent to `set({ [path]: value })`
486
+ * but skips the object-literal allocation and avoids the inference issue
487
+ * that motivated splitting the API.
488
+ *
489
+ * @example
490
+ * ```ts
491
+ * p.setField('query', 'react')
492
+ * p.setField('filters.tags', ['ts', 'react'])
493
+ * ```
494
+ */
495
+ setField(path, value, options) {
496
+ if (this.disposed) return;
497
+ this.applyUpdates({ [path]: value }, options);
498
+ }
499
+ applyUpdates(updates, options) {
439
500
  const initialChanges = {};
440
501
  for (const [path, v] of Object.entries(updates)) {
441
502
  const old = deepGet(this.values, path);
@@ -467,13 +528,13 @@ var ParamsStore = class {
467
528
  /** Boolean-flip helper. */
468
529
  toggle(path, options) {
469
530
  const current = this.getValue(path);
470
- this.set(path, !current, options);
531
+ this.setField(path, !current, options);
471
532
  }
472
533
  /** Push a value onto an array field. */
473
534
  append(path, value, options) {
474
535
  const current = this.getValue(path);
475
536
  if (!Array.isArray(current)) return;
476
- this.set(path, [...current, value], options);
537
+ this.setField(path, [...current, value], options);
477
538
  }
478
539
  /** Remove the first array element matching `value` by deepEqual. */
479
540
  remove(path, value, options) {
@@ -481,14 +542,14 @@ var ParamsStore = class {
481
542
  if (!Array.isArray(current)) return;
482
543
  const idx = current.findIndex((item) => deepEqual(item, value));
483
544
  if (idx === -1) return;
484
- this.set(path, [...current.slice(0, idx), ...current.slice(idx + 1)], options);
545
+ this.setField(path, [...current.slice(0, idx), ...current.slice(idx + 1)], options);
485
546
  }
486
547
  /** Remove the array element at the given index. */
487
548
  removeAt(path, index, options) {
488
549
  const current = this.getValue(path);
489
550
  if (!Array.isArray(current)) return;
490
551
  if (index < 0 || index >= current.length) return;
491
- this.set(path, [...current.slice(0, index), ...current.slice(index + 1)], options);
552
+ this.setField(path, [...current.slice(0, index), ...current.slice(index + 1)], options);
492
553
  }
493
554
  cycle(path, valuesOrOptions, maybeOptions) {
494
555
  let resolved;
@@ -514,12 +575,12 @@ var ParamsStore = class {
514
575
  const current = this.getValue(path);
515
576
  const idx = resolved.findIndex((o) => deepEqual(o, current));
516
577
  const next = idx === -1 ? resolved[0] : resolved[(idx + 1) % resolved.length];
517
- this.set(path, next, setOpts);
578
+ this.setField(path, next, setOpts);
518
579
  }
519
580
  /** Reset a single field to its default. */
520
581
  clear(path, options) {
521
582
  const def = deepGet(this.defaults, path);
522
- this.set(path, def, options);
583
+ this.setField(path, def, options);
523
584
  }
524
585
  /** Reset all fields to defaults; optional partial overrides + SetOptions. */
525
586
  reset(values, options) {
@@ -772,6 +833,7 @@ function releaseParamsStore(def) {
772
833
  isStandardSchema,
773
834
  memoryStorage,
774
835
  parseField,
836
+ registerEnumExtractor,
775
837
  releaseParamsStore
776
838
  });
777
839
  //# sourceMappingURL=index.cjs.map