@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.
- package/dist/{chunk-NUO3GOXV.js → chunk-5UKBDZTP.js} +37 -2
- package/dist/chunk-5UKBDZTP.js.map +1 -0
- package/dist/{chunk-43PUAYQP.js → chunk-DSAHBEAQ.js} +44 -18
- package/dist/chunk-DSAHBEAQ.js.map +1 -0
- package/dist/devtools.d.cts +1 -1
- package/dist/devtools.d.ts +1 -1
- package/dist/index.cjs +79 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -11
- package/dist/index.d.ts +47 -11
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/forms-reverse.cjs +71 -17
- package/dist/integrations/forms-reverse.cjs.map +1 -1
- package/dist/integrations/forms-reverse.js +2 -2
- package/dist/integrations/forms.cjs +71 -17
- package/dist/integrations/forms.cjs.map +1 -1
- package/dist/integrations/forms.js +2 -2
- package/dist/{params-store-Cgbtn53j.d.cts → params-store-4Lcb1M_X.d.cts} +29 -1
- package/dist/{params-store-CguA9-yr.d.ts → params-store-f3pmPdw3.d.ts} +29 -1
- package/dist/react.cjs +127 -54
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +42 -4
- package/dist/react.d.ts +42 -4
- package/dist/react.js +38 -20
- package/dist/react.js.map +1 -1
- package/dist/storage/idb.cjs +56 -3
- package/dist/storage/idb.cjs.map +1 -1
- package/dist/storage/idb.d.cts +40 -8
- package/dist/storage/idb.d.ts +40 -8
- package/dist/storage/idb.js +56 -3
- package/dist/storage/idb.js.map +1 -1
- package/dist/storage/url.cjs.map +1 -1
- package/dist/storage/url.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-43PUAYQP.js.map +0 -1
- 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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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"]}
|
package/dist/devtools.d.cts
CHANGED
package/dist/devtools.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|