@victorylabs/params 0.4.0 → 0.5.1

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.
@@ -35,6 +35,35 @@ function defaultSerialize(value) {
35
35
 
36
36
  // src/storage/url/index.ts
37
37
  var isClient = typeof window !== "undefined";
38
+ var LOCATION_CHANGE_EVENT = "urlStorage:locationchange";
39
+ var patchRefCount = 0;
40
+ var originalPushState = null;
41
+ var originalReplaceState = null;
42
+ function patchHistory() {
43
+ if (!isClient) return;
44
+ patchRefCount++;
45
+ if (patchRefCount > 1) return;
46
+ originalPushState = window.history.pushState;
47
+ originalReplaceState = window.history.replaceState;
48
+ window.history.pushState = function patchedPushState(...args) {
49
+ originalPushState.apply(this, args);
50
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
51
+ };
52
+ window.history.replaceState = function patchedReplaceState(...args) {
53
+ originalReplaceState.apply(this, args);
54
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
55
+ };
56
+ }
57
+ function unpatchHistory() {
58
+ if (!isClient) return;
59
+ if (patchRefCount === 0) return;
60
+ patchRefCount--;
61
+ if (patchRefCount > 0) return;
62
+ if (originalPushState) window.history.pushState = originalPushState;
63
+ if (originalReplaceState) window.history.replaceState = originalReplaceState;
64
+ originalPushState = null;
65
+ originalReplaceState = null;
66
+ }
38
67
  function urlStorage(options = {}) {
39
68
  const adapterStrategy = options.strategy ?? "replace";
40
69
  const paramMap = options.paramMap ?? {};
@@ -109,12 +138,30 @@ function urlStorage(options = {}) {
109
138
  },
110
139
  subscribe: (callback) => {
111
140
  if (!isClient) return () => void 0;
141
+ let previousKeys = new Set(Object.keys(readImpl() ?? {}));
112
142
  const handler = () => {
113
- const values = readImpl();
114
- if (values) callback(values);
143
+ const values = readImpl() ?? {};
144
+ const currentKeys = new Set(Object.keys(values));
145
+ const merged = { ...values };
146
+ let hasRemoved = false;
147
+ for (const k of previousKeys) {
148
+ if (!currentKeys.has(k)) {
149
+ merged[k] = void 0;
150
+ hasRemoved = true;
151
+ }
152
+ }
153
+ previousKeys = currentKeys;
154
+ if (currentKeys.size === 0 && !hasRemoved) return;
155
+ callback(merged);
115
156
  };
116
157
  window.addEventListener("popstate", handler);
117
- return () => window.removeEventListener("popstate", handler);
158
+ window.addEventListener(LOCATION_CHANGE_EVENT, handler);
159
+ patchHistory();
160
+ return () => {
161
+ window.removeEventListener("popstate", handler);
162
+ window.removeEventListener(LOCATION_CHANGE_EVENT, handler);
163
+ unpatchHistory();
164
+ };
118
165
  }
119
166
  };
120
167
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/storage/url/index.ts","../../src/schema.ts"],"sourcesContent":["import { defaultSerialize } from '../../schema'\nimport type { ParamsStorage, WriteOptions } from '../../storage'\n\nexport interface UrlStorageOptions {\n /**\n * History API strategy. `'replace'` keeps the back-stack clean (recommended\n * for filters/search). `'push'` creates a new history entry for every write.\n * Default: `'replace'`.\n */\n readonly strategy?: 'replace' | 'push'\n\n /** form path → URL search-param key. Defaults to identity. */\n readonly paramMap?: Record<string, string>\n\n /** Per-path string serializer. Default: `defaultSerialize` (same as schema). */\n readonly serialize?: Record<string, (value: unknown) => string>\n\n /** Per-path string-to-value deserializer. Default: identity (raw string). */\n readonly deserialize?: Record<string, (str: string) => unknown>\n}\n\nconst isClient = typeof window !== 'undefined'\n\n/**\n * URL search-param storage backend (native History API).\n *\n * - Hydrates from `window.location.search` on store creation.\n * - Reflects writes back to the URL via `replaceState` (default) or `pushState`.\n * - Subscribes to `popstate` so back/forward navigation flows into the store.\n *\n * `omitWhenDefault` is honored at the engine level (the store sends `undefined`\n * for paths at default; the backend deletes them from the URL).\n *\n * Limitations (v0.1):\n * - History API only — `hashchange` not supported.\n * - Cross-tab URL sync not supported (History API is per-tab).\n *\n * v0.4: per-call strategy override via `WriteOptions.history` — store callers\n * can pass `params.set(path, value, { history: 'push' })` to override the\n * adapter-level default for that single write.\n */\nexport function urlStorage<T = Record<string, unknown>>(\n options: UrlStorageOptions = {},\n): ParamsStorage<T> {\n const adapterStrategy = options.strategy ?? 'replace'\n const paramMap = options.paramMap ?? {}\n const reverseMap = buildReverseMap(paramMap)\n const serialize = options.serialize ?? {}\n const deserialize = options.deserialize ?? {}\n\n const readImpl = (): Partial<T> | undefined => {\n if (!isClient) return undefined\n try {\n const params = new URLSearchParams(window.location.search)\n const partial: Record<string, unknown> = {}\n let hasAny = false\n for (const [paramKey, raw] of params.entries()) {\n const formPath = reverseMap[paramKey] ?? paramKey\n const deserializer = deserialize[formPath]\n try {\n partial[formPath] = deserializer ? deserializer(raw) : raw\n hasAny = true\n } catch {\n // Per-path deserialize failure → skip; engine falls back to default\n }\n }\n return hasAny ? (partial as Partial<T>) : undefined\n } catch {\n return undefined\n }\n }\n\n const updateUrl = (params: URLSearchParams, opts?: WriteOptions): void => {\n if (!isClient) return\n const search = params.toString()\n const url = search ? `${window.location.pathname}?${search}` : window.location.pathname\n const strategy = opts?.history ?? adapterStrategy\n try {\n if (strategy === 'push') {\n window.history.pushState(null, '', url)\n } else {\n window.history.replaceState(null, '', url)\n }\n } catch {\n // Some browsers cap History API writes (rare) — silent fallback.\n }\n }\n\n return {\n name: 'urlStorage',\n clientOnly: true,\n read: readImpl,\n write: (values, _changed, opts) => {\n if (!isClient) return\n const params = new URLSearchParams(window.location.search)\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n const paramKey = paramMap[path] ?? path\n if (value === undefined || value === null) {\n params.delete(paramKey)\n continue\n }\n const serializer = serialize[path]\n const str = serializer ? serializer(value) : defaultSerialize(value)\n params.set(paramKey, str)\n }\n updateUrl(params, opts)\n },\n clear: (paths, opts) => {\n if (!isClient) return\n if (paths.length === 0) {\n updateUrl(new URLSearchParams(), opts)\n return\n }\n const params = new URLSearchParams(window.location.search)\n for (const path of paths) {\n const paramKey = paramMap[path] ?? path\n params.delete(paramKey)\n }\n updateUrl(params, opts)\n },\n subscribe: (callback) => {\n if (!isClient) return () => undefined\n const handler = () => {\n const values = readImpl()\n if (values) callback(values)\n }\n window.addEventListener('popstate', handler)\n return () => window.removeEventListener('popstate', handler)\n },\n }\n}\n\nfunction buildReverseMap(map: Record<string, string>): Record<string, string> {\n const reverse: Record<string, string> = {}\n for (const [formPath, paramKey] of Object.entries(map)) {\n reverse[paramKey] = formPath\n }\n return reverse\n}\n","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":[]}
1
+ {"version":3,"sources":["../../src/storage/url/index.ts","../../src/schema.ts"],"sourcesContent":["import { defaultSerialize } from '../../schema'\nimport type { ParamsStorage, WriteOptions } from '../../storage'\n\nexport interface UrlStorageOptions {\n /**\n * History API strategy. `'replace'` keeps the back-stack clean (recommended\n * for filters/search). `'push'` creates a new history entry for every write.\n * Default: `'replace'`.\n */\n readonly strategy?: 'replace' | 'push'\n\n /** form path → URL search-param key. Defaults to identity. */\n readonly paramMap?: Record<string, string>\n\n /** Per-path string serializer. Default: `defaultSerialize` (same as schema). */\n readonly serialize?: Record<string, (value: unknown) => string>\n\n /** Per-path string-to-value deserializer. Default: identity (raw string). */\n readonly deserialize?: Record<string, (str: string) => unknown>\n}\n\nconst isClient = typeof window !== 'undefined'\n\n// History.pushState / replaceState don't fire popstate, so any framework that\n// navigates programmatically (kit/router, react-router, etc.) silently\n// desyncs urlStorage from the URL. We monkey-patch both methods on first\n// subscribe to emit a synthetic event, and restore them when the last\n// subscriber tears down — same trick the `history` library uses.\nconst LOCATION_CHANGE_EVENT = 'urlStorage:locationchange'\n\nlet patchRefCount = 0\nlet originalPushState: typeof window.history.pushState | null = null\nlet originalReplaceState: typeof window.history.replaceState | null = null\n\nfunction patchHistory(): void {\n if (!isClient) return\n patchRefCount++\n if (patchRefCount > 1) return\n\n originalPushState = window.history.pushState\n originalReplaceState = window.history.replaceState\n\n window.history.pushState = function patchedPushState(\n this: History,\n ...args: Parameters<typeof window.history.pushState>\n ): void {\n originalPushState!.apply(this, args)\n window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT))\n }\n window.history.replaceState = function patchedReplaceState(\n this: History,\n ...args: Parameters<typeof window.history.replaceState>\n ): void {\n originalReplaceState!.apply(this, args)\n window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT))\n }\n}\n\nfunction unpatchHistory(): void {\n if (!isClient) return\n if (patchRefCount === 0) return\n patchRefCount--\n if (patchRefCount > 0) return\n\n if (originalPushState) window.history.pushState = originalPushState\n if (originalReplaceState) window.history.replaceState = originalReplaceState\n originalPushState = null\n originalReplaceState = null\n}\n\n/**\n * URL search-param storage backend (native History API).\n *\n * - Hydrates from `window.location.search` on store creation.\n * - Reflects writes back to the URL via `replaceState` (default) or `pushState`.\n * - Subscribes to `popstate` (back/forward) AND to programmatic\n * `history.pushState`/`replaceState` calls (via a refcounted monkey-patch),\n * so any route framework that navigates with the History API stays in sync.\n * - When a key disappears from the URL (full clear or partial removal), the\n * subscribe payload signals it as `undefined` so the engine resets the\n * field to its schema default.\n *\n * `omitWhenDefault` is honored at the engine level (the store sends `undefined`\n * for paths at default; the backend deletes them from the URL).\n *\n * Limitations (v0.1):\n * - History API only — `hashchange` not supported.\n * - Cross-tab URL sync not supported (History API is per-tab).\n *\n * v0.4: per-call strategy override via `WriteOptions.history` — store callers\n * can pass `params.set(path, value, { history: 'push' })` to override the\n * adapter-level default for that single write.\n */\nexport function urlStorage<T = Record<string, unknown>>(\n options: UrlStorageOptions = {},\n): ParamsStorage<T> {\n const adapterStrategy = options.strategy ?? 'replace'\n const paramMap = options.paramMap ?? {}\n const reverseMap = buildReverseMap(paramMap)\n const serialize = options.serialize ?? {}\n const deserialize = options.deserialize ?? {}\n\n const readImpl = (): Partial<T> | undefined => {\n if (!isClient) return undefined\n try {\n const params = new URLSearchParams(window.location.search)\n const partial: Record<string, unknown> = {}\n let hasAny = false\n for (const [paramKey, raw] of params.entries()) {\n const formPath = reverseMap[paramKey] ?? paramKey\n const deserializer = deserialize[formPath]\n try {\n partial[formPath] = deserializer ? deserializer(raw) : raw\n hasAny = true\n } catch {\n // Per-path deserialize failure → skip; engine falls back to default\n }\n }\n return hasAny ? (partial as Partial<T>) : undefined\n } catch {\n return undefined\n }\n }\n\n const updateUrl = (params: URLSearchParams, opts?: WriteOptions): void => {\n if (!isClient) return\n const search = params.toString()\n const url = search ? `${window.location.pathname}?${search}` : window.location.pathname\n const strategy = opts?.history ?? adapterStrategy\n try {\n if (strategy === 'push') {\n window.history.pushState(null, '', url)\n } else {\n window.history.replaceState(null, '', url)\n }\n } catch {\n // Some browsers cap History API writes (rare) — silent fallback.\n }\n }\n\n return {\n name: 'urlStorage',\n clientOnly: true,\n read: readImpl,\n write: (values, _changed, opts) => {\n if (!isClient) return\n const params = new URLSearchParams(window.location.search)\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n const paramKey = paramMap[path] ?? path\n if (value === undefined || value === null) {\n params.delete(paramKey)\n continue\n }\n const serializer = serialize[path]\n const str = serializer ? serializer(value) : defaultSerialize(value)\n params.set(paramKey, str)\n }\n updateUrl(params, opts)\n },\n clear: (paths, opts) => {\n if (!isClient) return\n if (paths.length === 0) {\n updateUrl(new URLSearchParams(), opts)\n return\n }\n const params = new URLSearchParams(window.location.search)\n for (const path of paths) {\n const paramKey = paramMap[path] ?? path\n params.delete(paramKey)\n }\n updateUrl(params, opts)\n },\n subscribe: (callback) => {\n if (!isClient) return () => undefined\n // Track keys present on the most recent emit so we can signal removals\n // explicitly. The engine's onExternalChange only iterates raw's own\n // keys — without explicit `undefined` for disappearing keys, navigating\n // to a URL that drops a param leaves the in-memory value stale.\n let previousKeys = new Set<string>(Object.keys(readImpl() ?? {}))\n const handler = () => {\n const values = (readImpl() ?? {}) as Partial<T>\n const currentKeys = new Set(Object.keys(values))\n const merged: Record<string, unknown> = { ...(values as Record<string, unknown>) }\n let hasRemoved = false\n for (const k of previousKeys) {\n if (!currentKeys.has(k)) {\n merged[k] = undefined\n hasRemoved = true\n }\n }\n previousKeys = currentKeys\n if (currentKeys.size === 0 && !hasRemoved) return\n callback(merged as Partial<T>)\n }\n window.addEventListener('popstate', handler)\n window.addEventListener(LOCATION_CHANGE_EVENT, handler)\n patchHistory()\n return () => {\n window.removeEventListener('popstate', handler)\n window.removeEventListener(LOCATION_CHANGE_EVENT, handler)\n unpatchHistory()\n }\n },\n }\n}\n\nfunction buildReverseMap(map: Record<string, string>): Record<string, string> {\n const reverse: Record<string, string> = {}\n for (const [formPath, paramKey] of Object.entries(map)) {\n reverse[paramKey] = formPath\n }\n return reverse\n}\n","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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkFO,SAAS,iBAAiB,OAAwB;AACvD,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,UAAW,QAAO,QAAQ,SAAS;AACxD,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI;AAC/E,SAAO,KAAK,UAAU,KAAK;AAC7B;;;ADnEA,IAAM,WAAW,OAAO,WAAW;AAOnC,IAAM,wBAAwB;AAE9B,IAAI,gBAAgB;AACpB,IAAI,oBAA4D;AAChE,IAAI,uBAAkE;AAEtE,SAAS,eAAqB;AAC5B,MAAI,CAAC,SAAU;AACf;AACA,MAAI,gBAAgB,EAAG;AAEvB,sBAAoB,OAAO,QAAQ;AACnC,yBAAuB,OAAO,QAAQ;AAEtC,SAAO,QAAQ,YAAY,SAAS,oBAE/B,MACG;AACN,sBAAmB,MAAM,MAAM,IAAI;AACnC,WAAO,cAAc,IAAI,MAAM,qBAAqB,CAAC;AAAA,EACvD;AACA,SAAO,QAAQ,eAAe,SAAS,uBAElC,MACG;AACN,yBAAsB,MAAM,MAAM,IAAI;AACtC,WAAO,cAAc,IAAI,MAAM,qBAAqB,CAAC;AAAA,EACvD;AACF;AAEA,SAAS,iBAAuB;AAC9B,MAAI,CAAC,SAAU;AACf,MAAI,kBAAkB,EAAG;AACzB;AACA,MAAI,gBAAgB,EAAG;AAEvB,MAAI,kBAAmB,QAAO,QAAQ,YAAY;AAClD,MAAI,qBAAsB,QAAO,QAAQ,eAAe;AACxD,sBAAoB;AACpB,yBAAuB;AACzB;AAyBO,SAAS,WACd,UAA6B,CAAC,GACZ;AAClB,QAAM,kBAAkB,QAAQ,YAAY;AAC5C,QAAM,WAAW,QAAQ,YAAY,CAAC;AACtC,QAAM,aAAa,gBAAgB,QAAQ;AAC3C,QAAM,YAAY,QAAQ,aAAa,CAAC;AACxC,QAAM,cAAc,QAAQ,eAAe,CAAC;AAE5C,QAAM,WAAW,MAA8B;AAC7C,QAAI,CAAC,SAAU,QAAO;AACtB,QAAI;AACF,YAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,YAAM,UAAmC,CAAC;AAC1C,UAAI,SAAS;AACb,iBAAW,CAAC,UAAU,GAAG,KAAK,OAAO,QAAQ,GAAG;AAC9C,cAAM,WAAW,WAAW,QAAQ,KAAK;AACzC,cAAM,eAAe,YAAY,QAAQ;AACzC,YAAI;AACF,kBAAQ,QAAQ,IAAI,eAAe,aAAa,GAAG,IAAI;AACvD,mBAAS;AAAA,QACX,QAAQ;AAAA,QAER;AAAA,MACF;AACA,aAAO,SAAU,UAAyB;AAAA,IAC5C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,QAAyB,SAA8B;AACxE,QAAI,CAAC,SAAU;AACf,UAAM,SAAS,OAAO,SAAS;AAC/B,UAAM,MAAM,SAAS,GAAG,OAAO,SAAS,QAAQ,IAAI,MAAM,KAAK,OAAO,SAAS;AAC/E,UAAM,WAAW,MAAM,WAAW;AAClC,QAAI;AACF,UAAI,aAAa,QAAQ;AACvB,eAAO,QAAQ,UAAU,MAAM,IAAI,GAAG;AAAA,MACxC,OAAO;AACL,eAAO,QAAQ,aAAa,MAAM,IAAI,GAAG;AAAA,MAC3C;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,OAAO,CAAC,QAAQ,UAAU,SAAS;AACjC,UAAI,CAAC,SAAU;AACf,YAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAiC,GAAG;AAC7E,cAAM,WAAW,SAAS,IAAI,KAAK;AACnC,YAAI,UAAU,UAAa,UAAU,MAAM;AACzC,iBAAO,OAAO,QAAQ;AACtB;AAAA,QACF;AACA,cAAM,aAAa,UAAU,IAAI;AACjC,cAAM,MAAM,aAAa,WAAW,KAAK,IAAI,iBAAiB,KAAK;AACnE,eAAO,IAAI,UAAU,GAAG;AAAA,MAC1B;AACA,gBAAU,QAAQ,IAAI;AAAA,IACxB;AAAA,IACA,OAAO,CAAC,OAAO,SAAS;AACtB,UAAI,CAAC,SAAU;AACf,UAAI,MAAM,WAAW,GAAG;AACtB,kBAAU,IAAI,gBAAgB,GAAG,IAAI;AACrC;AAAA,MACF;AACA,YAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,iBAAW,QAAQ,OAAO;AACxB,cAAM,WAAW,SAAS,IAAI,KAAK;AACnC,eAAO,OAAO,QAAQ;AAAA,MACxB;AACA,gBAAU,QAAQ,IAAI;AAAA,IACxB;AAAA,IACA,WAAW,CAAC,aAAa;AACvB,UAAI,CAAC,SAAU,QAAO,MAAM;AAK5B,UAAI,eAAe,IAAI,IAAY,OAAO,KAAK,SAAS,KAAK,CAAC,CAAC,CAAC;AAChE,YAAM,UAAU,MAAM;AACpB,cAAM,SAAU,SAAS,KAAK,CAAC;AAC/B,cAAM,cAAc,IAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AAC/C,cAAM,SAAkC,EAAE,GAAI,OAAmC;AACjF,YAAI,aAAa;AACjB,mBAAW,KAAK,cAAc;AAC5B,cAAI,CAAC,YAAY,IAAI,CAAC,GAAG;AACvB,mBAAO,CAAC,IAAI;AACZ,yBAAa;AAAA,UACf;AAAA,QACF;AACA,uBAAe;AACf,YAAI,YAAY,SAAS,KAAK,CAAC,WAAY;AAC3C,iBAAS,MAAoB;AAAA,MAC/B;AACA,aAAO,iBAAiB,YAAY,OAAO;AAC3C,aAAO,iBAAiB,uBAAuB,OAAO;AACtD,mBAAa;AACb,aAAO,MAAM;AACX,eAAO,oBAAoB,YAAY,OAAO;AAC9C,eAAO,oBAAoB,uBAAuB,OAAO;AACzD,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,KAAqD;AAC5E,QAAM,UAAkC,CAAC;AACzC,aAAW,CAAC,UAAU,QAAQ,KAAK,OAAO,QAAQ,GAAG,GAAG;AACtD,YAAQ,QAAQ,IAAI;AAAA,EACtB;AACA,SAAO;AACT;","names":[]}
@@ -19,7 +19,12 @@ interface UrlStorageOptions {
19
19
  *
20
20
  * - Hydrates from `window.location.search` on store creation.
21
21
  * - Reflects writes back to the URL via `replaceState` (default) or `pushState`.
22
- * - Subscribes to `popstate` so back/forward navigation flows into the store.
22
+ * - Subscribes to `popstate` (back/forward) AND to programmatic
23
+ * `history.pushState`/`replaceState` calls (via a refcounted monkey-patch),
24
+ * so any route framework that navigates with the History API stays in sync.
25
+ * - When a key disappears from the URL (full clear or partial removal), the
26
+ * subscribe payload signals it as `undefined` so the engine resets the
27
+ * field to its schema default.
23
28
  *
24
29
  * `omitWhenDefault` is honored at the engine level (the store sends `undefined`
25
30
  * for paths at default; the backend deletes them from the URL).
@@ -19,7 +19,12 @@ interface UrlStorageOptions {
19
19
  *
20
20
  * - Hydrates from `window.location.search` on store creation.
21
21
  * - Reflects writes back to the URL via `replaceState` (default) or `pushState`.
22
- * - Subscribes to `popstate` so back/forward navigation flows into the store.
22
+ * - Subscribes to `popstate` (back/forward) AND to programmatic
23
+ * `history.pushState`/`replaceState` calls (via a refcounted monkey-patch),
24
+ * so any route framework that navigates with the History API stays in sync.
25
+ * - When a key disappears from the URL (full clear or partial removal), the
26
+ * subscribe payload signals it as `undefined` so the engine resets the
27
+ * field to its schema default.
23
28
  *
24
29
  * `omitWhenDefault` is honored at the engine level (the store sends `undefined`
25
30
  * for paths at default; the backend deletes them from the URL).
@@ -4,6 +4,35 @@ import {
4
4
 
5
5
  // src/storage/url/index.ts
6
6
  var isClient = typeof window !== "undefined";
7
+ var LOCATION_CHANGE_EVENT = "urlStorage:locationchange";
8
+ var patchRefCount = 0;
9
+ var originalPushState = null;
10
+ var originalReplaceState = null;
11
+ function patchHistory() {
12
+ if (!isClient) return;
13
+ patchRefCount++;
14
+ if (patchRefCount > 1) return;
15
+ originalPushState = window.history.pushState;
16
+ originalReplaceState = window.history.replaceState;
17
+ window.history.pushState = function patchedPushState(...args) {
18
+ originalPushState.apply(this, args);
19
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
20
+ };
21
+ window.history.replaceState = function patchedReplaceState(...args) {
22
+ originalReplaceState.apply(this, args);
23
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
24
+ };
25
+ }
26
+ function unpatchHistory() {
27
+ if (!isClient) return;
28
+ if (patchRefCount === 0) return;
29
+ patchRefCount--;
30
+ if (patchRefCount > 0) return;
31
+ if (originalPushState) window.history.pushState = originalPushState;
32
+ if (originalReplaceState) window.history.replaceState = originalReplaceState;
33
+ originalPushState = null;
34
+ originalReplaceState = null;
35
+ }
7
36
  function urlStorage(options = {}) {
8
37
  const adapterStrategy = options.strategy ?? "replace";
9
38
  const paramMap = options.paramMap ?? {};
@@ -78,12 +107,30 @@ function urlStorage(options = {}) {
78
107
  },
79
108
  subscribe: (callback) => {
80
109
  if (!isClient) return () => void 0;
110
+ let previousKeys = new Set(Object.keys(readImpl() ?? {}));
81
111
  const handler = () => {
82
- const values = readImpl();
83
- if (values) callback(values);
112
+ const values = readImpl() ?? {};
113
+ const currentKeys = new Set(Object.keys(values));
114
+ const merged = { ...values };
115
+ let hasRemoved = false;
116
+ for (const k of previousKeys) {
117
+ if (!currentKeys.has(k)) {
118
+ merged[k] = void 0;
119
+ hasRemoved = true;
120
+ }
121
+ }
122
+ previousKeys = currentKeys;
123
+ if (currentKeys.size === 0 && !hasRemoved) return;
124
+ callback(merged);
84
125
  };
85
126
  window.addEventListener("popstate", handler);
86
- return () => window.removeEventListener("popstate", handler);
127
+ window.addEventListener(LOCATION_CHANGE_EVENT, handler);
128
+ patchHistory();
129
+ return () => {
130
+ window.removeEventListener("popstate", handler);
131
+ window.removeEventListener(LOCATION_CHANGE_EVENT, handler);
132
+ unpatchHistory();
133
+ };
87
134
  }
88
135
  };
89
136
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/storage/url/index.ts"],"sourcesContent":["import { defaultSerialize } from '../../schema'\nimport type { ParamsStorage, WriteOptions } from '../../storage'\n\nexport interface UrlStorageOptions {\n /**\n * History API strategy. `'replace'` keeps the back-stack clean (recommended\n * for filters/search). `'push'` creates a new history entry for every write.\n * Default: `'replace'`.\n */\n readonly strategy?: 'replace' | 'push'\n\n /** form path → URL search-param key. Defaults to identity. */\n readonly paramMap?: Record<string, string>\n\n /** Per-path string serializer. Default: `defaultSerialize` (same as schema). */\n readonly serialize?: Record<string, (value: unknown) => string>\n\n /** Per-path string-to-value deserializer. Default: identity (raw string). */\n readonly deserialize?: Record<string, (str: string) => unknown>\n}\n\nconst isClient = typeof window !== 'undefined'\n\n/**\n * URL search-param storage backend (native History API).\n *\n * - Hydrates from `window.location.search` on store creation.\n * - Reflects writes back to the URL via `replaceState` (default) or `pushState`.\n * - Subscribes to `popstate` so back/forward navigation flows into the store.\n *\n * `omitWhenDefault` is honored at the engine level (the store sends `undefined`\n * for paths at default; the backend deletes them from the URL).\n *\n * Limitations (v0.1):\n * - History API only — `hashchange` not supported.\n * - Cross-tab URL sync not supported (History API is per-tab).\n *\n * v0.4: per-call strategy override via `WriteOptions.history` — store callers\n * can pass `params.set(path, value, { history: 'push' })` to override the\n * adapter-level default for that single write.\n */\nexport function urlStorage<T = Record<string, unknown>>(\n options: UrlStorageOptions = {},\n): ParamsStorage<T> {\n const adapterStrategy = options.strategy ?? 'replace'\n const paramMap = options.paramMap ?? {}\n const reverseMap = buildReverseMap(paramMap)\n const serialize = options.serialize ?? {}\n const deserialize = options.deserialize ?? {}\n\n const readImpl = (): Partial<T> | undefined => {\n if (!isClient) return undefined\n try {\n const params = new URLSearchParams(window.location.search)\n const partial: Record<string, unknown> = {}\n let hasAny = false\n for (const [paramKey, raw] of params.entries()) {\n const formPath = reverseMap[paramKey] ?? paramKey\n const deserializer = deserialize[formPath]\n try {\n partial[formPath] = deserializer ? deserializer(raw) : raw\n hasAny = true\n } catch {\n // Per-path deserialize failure → skip; engine falls back to default\n }\n }\n return hasAny ? (partial as Partial<T>) : undefined\n } catch {\n return undefined\n }\n }\n\n const updateUrl = (params: URLSearchParams, opts?: WriteOptions): void => {\n if (!isClient) return\n const search = params.toString()\n const url = search ? `${window.location.pathname}?${search}` : window.location.pathname\n const strategy = opts?.history ?? adapterStrategy\n try {\n if (strategy === 'push') {\n window.history.pushState(null, '', url)\n } else {\n window.history.replaceState(null, '', url)\n }\n } catch {\n // Some browsers cap History API writes (rare) — silent fallback.\n }\n }\n\n return {\n name: 'urlStorage',\n clientOnly: true,\n read: readImpl,\n write: (values, _changed, opts) => {\n if (!isClient) return\n const params = new URLSearchParams(window.location.search)\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n const paramKey = paramMap[path] ?? path\n if (value === undefined || value === null) {\n params.delete(paramKey)\n continue\n }\n const serializer = serialize[path]\n const str = serializer ? serializer(value) : defaultSerialize(value)\n params.set(paramKey, str)\n }\n updateUrl(params, opts)\n },\n clear: (paths, opts) => {\n if (!isClient) return\n if (paths.length === 0) {\n updateUrl(new URLSearchParams(), opts)\n return\n }\n const params = new URLSearchParams(window.location.search)\n for (const path of paths) {\n const paramKey = paramMap[path] ?? path\n params.delete(paramKey)\n }\n updateUrl(params, opts)\n },\n subscribe: (callback) => {\n if (!isClient) return () => undefined\n const handler = () => {\n const values = readImpl()\n if (values) callback(values)\n }\n window.addEventListener('popstate', handler)\n return () => window.removeEventListener('popstate', handler)\n },\n }\n}\n\nfunction buildReverseMap(map: Record<string, string>): Record<string, string> {\n const reverse: Record<string, string> = {}\n for (const [formPath, paramKey] of Object.entries(map)) {\n reverse[paramKey] = formPath\n }\n return reverse\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/storage/url/index.ts"],"sourcesContent":["import { defaultSerialize } from '../../schema'\nimport type { ParamsStorage, WriteOptions } from '../../storage'\n\nexport interface UrlStorageOptions {\n /**\n * History API strategy. `'replace'` keeps the back-stack clean (recommended\n * for filters/search). `'push'` creates a new history entry for every write.\n * Default: `'replace'`.\n */\n readonly strategy?: 'replace' | 'push'\n\n /** form path → URL search-param key. Defaults to identity. */\n readonly paramMap?: Record<string, string>\n\n /** Per-path string serializer. Default: `defaultSerialize` (same as schema). */\n readonly serialize?: Record<string, (value: unknown) => string>\n\n /** Per-path string-to-value deserializer. Default: identity (raw string). */\n readonly deserialize?: Record<string, (str: string) => unknown>\n}\n\nconst isClient = typeof window !== 'undefined'\n\n// History.pushState / replaceState don't fire popstate, so any framework that\n// navigates programmatically (kit/router, react-router, etc.) silently\n// desyncs urlStorage from the URL. We monkey-patch both methods on first\n// subscribe to emit a synthetic event, and restore them when the last\n// subscriber tears down — same trick the `history` library uses.\nconst LOCATION_CHANGE_EVENT = 'urlStorage:locationchange'\n\nlet patchRefCount = 0\nlet originalPushState: typeof window.history.pushState | null = null\nlet originalReplaceState: typeof window.history.replaceState | null = null\n\nfunction patchHistory(): void {\n if (!isClient) return\n patchRefCount++\n if (patchRefCount > 1) return\n\n originalPushState = window.history.pushState\n originalReplaceState = window.history.replaceState\n\n window.history.pushState = function patchedPushState(\n this: History,\n ...args: Parameters<typeof window.history.pushState>\n ): void {\n originalPushState!.apply(this, args)\n window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT))\n }\n window.history.replaceState = function patchedReplaceState(\n this: History,\n ...args: Parameters<typeof window.history.replaceState>\n ): void {\n originalReplaceState!.apply(this, args)\n window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT))\n }\n}\n\nfunction unpatchHistory(): void {\n if (!isClient) return\n if (patchRefCount === 0) return\n patchRefCount--\n if (patchRefCount > 0) return\n\n if (originalPushState) window.history.pushState = originalPushState\n if (originalReplaceState) window.history.replaceState = originalReplaceState\n originalPushState = null\n originalReplaceState = null\n}\n\n/**\n * URL search-param storage backend (native History API).\n *\n * - Hydrates from `window.location.search` on store creation.\n * - Reflects writes back to the URL via `replaceState` (default) or `pushState`.\n * - Subscribes to `popstate` (back/forward) AND to programmatic\n * `history.pushState`/`replaceState` calls (via a refcounted monkey-patch),\n * so any route framework that navigates with the History API stays in sync.\n * - When a key disappears from the URL (full clear or partial removal), the\n * subscribe payload signals it as `undefined` so the engine resets the\n * field to its schema default.\n *\n * `omitWhenDefault` is honored at the engine level (the store sends `undefined`\n * for paths at default; the backend deletes them from the URL).\n *\n * Limitations (v0.1):\n * - History API only — `hashchange` not supported.\n * - Cross-tab URL sync not supported (History API is per-tab).\n *\n * v0.4: per-call strategy override via `WriteOptions.history` — store callers\n * can pass `params.set(path, value, { history: 'push' })` to override the\n * adapter-level default for that single write.\n */\nexport function urlStorage<T = Record<string, unknown>>(\n options: UrlStorageOptions = {},\n): ParamsStorage<T> {\n const adapterStrategy = options.strategy ?? 'replace'\n const paramMap = options.paramMap ?? {}\n const reverseMap = buildReverseMap(paramMap)\n const serialize = options.serialize ?? {}\n const deserialize = options.deserialize ?? {}\n\n const readImpl = (): Partial<T> | undefined => {\n if (!isClient) return undefined\n try {\n const params = new URLSearchParams(window.location.search)\n const partial: Record<string, unknown> = {}\n let hasAny = false\n for (const [paramKey, raw] of params.entries()) {\n const formPath = reverseMap[paramKey] ?? paramKey\n const deserializer = deserialize[formPath]\n try {\n partial[formPath] = deserializer ? deserializer(raw) : raw\n hasAny = true\n } catch {\n // Per-path deserialize failure → skip; engine falls back to default\n }\n }\n return hasAny ? (partial as Partial<T>) : undefined\n } catch {\n return undefined\n }\n }\n\n const updateUrl = (params: URLSearchParams, opts?: WriteOptions): void => {\n if (!isClient) return\n const search = params.toString()\n const url = search ? `${window.location.pathname}?${search}` : window.location.pathname\n const strategy = opts?.history ?? adapterStrategy\n try {\n if (strategy === 'push') {\n window.history.pushState(null, '', url)\n } else {\n window.history.replaceState(null, '', url)\n }\n } catch {\n // Some browsers cap History API writes (rare) — silent fallback.\n }\n }\n\n return {\n name: 'urlStorage',\n clientOnly: true,\n read: readImpl,\n write: (values, _changed, opts) => {\n if (!isClient) return\n const params = new URLSearchParams(window.location.search)\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n const paramKey = paramMap[path] ?? path\n if (value === undefined || value === null) {\n params.delete(paramKey)\n continue\n }\n const serializer = serialize[path]\n const str = serializer ? serializer(value) : defaultSerialize(value)\n params.set(paramKey, str)\n }\n updateUrl(params, opts)\n },\n clear: (paths, opts) => {\n if (!isClient) return\n if (paths.length === 0) {\n updateUrl(new URLSearchParams(), opts)\n return\n }\n const params = new URLSearchParams(window.location.search)\n for (const path of paths) {\n const paramKey = paramMap[path] ?? path\n params.delete(paramKey)\n }\n updateUrl(params, opts)\n },\n subscribe: (callback) => {\n if (!isClient) return () => undefined\n // Track keys present on the most recent emit so we can signal removals\n // explicitly. The engine's onExternalChange only iterates raw's own\n // keys — without explicit `undefined` for disappearing keys, navigating\n // to a URL that drops a param leaves the in-memory value stale.\n let previousKeys = new Set<string>(Object.keys(readImpl() ?? {}))\n const handler = () => {\n const values = (readImpl() ?? {}) as Partial<T>\n const currentKeys = new Set(Object.keys(values))\n const merged: Record<string, unknown> = { ...(values as Record<string, unknown>) }\n let hasRemoved = false\n for (const k of previousKeys) {\n if (!currentKeys.has(k)) {\n merged[k] = undefined\n hasRemoved = true\n }\n }\n previousKeys = currentKeys\n if (currentKeys.size === 0 && !hasRemoved) return\n callback(merged as Partial<T>)\n }\n window.addEventListener('popstate', handler)\n window.addEventListener(LOCATION_CHANGE_EVENT, handler)\n patchHistory()\n return () => {\n window.removeEventListener('popstate', handler)\n window.removeEventListener(LOCATION_CHANGE_EVENT, handler)\n unpatchHistory()\n }\n },\n }\n}\n\nfunction buildReverseMap(map: Record<string, string>): Record<string, string> {\n const reverse: Record<string, string> = {}\n for (const [formPath, paramKey] of Object.entries(map)) {\n reverse[paramKey] = formPath\n }\n return reverse\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victorylabs/params",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Centralized view-state config (filters, sort, pagination, app config) with pluggable storage. Memory by default; URL/localStorage/sessionStorage opt-in. Single React hook DX, schema-validated reads, cross-component sharing.",
5
5
  "publishConfig": {
6
6
  "access": "public"