@victorylabs/params 0.3.0 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/snapshot.ts","../src/name-registry.ts"],"sourcesContent":["import { getDefByName, setPreHydrationValues } from './name-registry'\nimport type { ParamsDefinition } from './types'\n\n/**\n * Snapshot keyed by definition name. Returned from `paramsSnapshot()` and\n * accepted by `hydrateParams()` — round-trip transport for SSR.\n */\nexport type ParamsSnapshot = Record<string, unknown>\n\n/**\n * Server-side: produce a snapshot of the current values for the given\n * definitions, keyed by `def.name`. The returned object is JSON-serializable\n * (call `JSON.stringify` to embed in HTML, or pass through your framework's\n * data-loader contract).\n *\n * Each definition MUST have a `name` set (added in v0.1 for forward-compat,\n * load-bearing in v0.2). Throws otherwise.\n *\n * Throws on duplicate names — the snapshot keys would collide.\n *\n * Reads via the synchronous `def.storage.read()` path. Definitions backed by\n * an async-only backend (sync `read()` returns undefined) snapshot as\n * defaults; pair them with a server-side prefetch step before calling\n * `paramsSnapshot()` if you want their fetched values in the snapshot.\n */\nexport function paramsSnapshot(defs: readonly ParamsDefinition[]): ParamsSnapshot {\n const out: ParamsSnapshot = {}\n const seenNames = new Set<string>()\n\n for (const def of defs) {\n if (def.name === undefined) {\n throw new Error(\n 'paramsSnapshot: every definition must have a `name` set. Add `{ name: \"<unique>\" }` to defineParams.',\n )\n }\n if (seenNames.has(def.name)) {\n throw new Error(\n `paramsSnapshot: duplicate definition name '${def.name}'. Snapshot keys would collide.`,\n )\n }\n seenNames.add(def.name)\n\n let values: unknown = undefined\n try {\n values = def.storage.read() ?? {}\n } catch {\n values = {}\n }\n out[def.name] = values\n }\n\n return out\n}\n\n/**\n * Client-side: pre-seed the params store cache from a server-rendered\n * snapshot. Must run BEFORE the first `useParams(def)` / `getParamsStore(def)`\n * call to avoid a flicker.\n *\n * For each entry in the snapshot, looks up the definition by name (must have\n * been imported / `defineParams` already evaluated on the client) and seeds\n * the pre-hydration cache. The first `ParamsStore` constructed for that def\n * uses the seeded values instead of calling its storage backend's `read()`.\n *\n * Idempotent — re-calling overwrites. Definitions whose names aren't in the\n * snapshot use their normal storage hydration path.\n */\nexport function hydrateParams(snapshot: ParamsSnapshot): void {\n for (const [name, values] of Object.entries(snapshot)) {\n setPreHydrationValues(name, values)\n // Tolerate names with no matching def — the def may load lazily via a\n // dynamic import. The pre-hydration cache holds the values until the\n // matching ParamsStore is constructed.\n void getDefByName(name)\n }\n}\n","import type { ParamsDefinition } from './types'\n\n/**\n * Side-effect registry of all definitions that carry a `name`. Populated by\n * `defineParams()` at construction time so `paramsSnapshot()` and\n * `hydrateParams()` can look up defs by name.\n *\n * Last-write-wins: re-defining the same name overwrites silently (the\n * duplicate-name DEV warning is enforced separately at acquire time in\n * `store-cache.ts`).\n */\nconst nameRegistry = new Map<string, ParamsDefinition<unknown>>()\n\n/**\n * Pre-hydration cache for SSR snapshots. `hydrateParams(snapshot)` populates\n * this; `ParamsStore` checks it during initial hydration and uses the seeded\n * values instead of calling the storage backend's `read()`.\n *\n * Once consumed by a store on first hydration the entry is left in place\n * (idempotent re-hydrate) — the consumer can call `hydrateParams()` again\n * with overrides if needed before any store is constructed.\n */\nconst preHydrationCache = new Map<string, unknown>()\n\nexport function registerNamedDef<T>(def: ParamsDefinition<T>): void {\n if (def.name === undefined) return\n nameRegistry.set(def.name, def as ParamsDefinition<unknown>)\n}\n\nexport function getDefByName(name: string): ParamsDefinition<unknown> | undefined {\n return nameRegistry.get(name)\n}\n\nexport function getRegisteredDefs(): ReadonlyArray<ParamsDefinition<unknown>> {\n return Array.from(nameRegistry.values())\n}\n\nexport function setPreHydrationValues(name: string, values: unknown): void {\n preHydrationCache.set(name, values)\n}\n\nexport function takePreHydrationValues<T>(name: string | undefined): Partial<T> | undefined {\n if (name === undefined) return undefined\n if (!preHydrationCache.has(name)) return undefined\n return preHydrationCache.get(name) as Partial<T>\n}\n\n/** Test-only: clear both registries. Not exported from the package. */\nexport function _resetNameRegistry(): void {\n nameRegistry.clear()\n preHydrationCache.clear()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,IAAM,eAAe,oBAAI,IAAuC;AAWhE,IAAM,oBAAoB,oBAAI,IAAqB;AAO5C,SAAS,aAAa,MAAqD;AAChF,SAAO,aAAa,IAAI,IAAI;AAC9B;AAMO,SAAS,sBAAsB,MAAc,QAAuB;AACzE,oBAAkB,IAAI,MAAM,MAAM;AACpC;;;ADdO,SAAS,eAAe,MAAmD;AAChF,QAAM,MAAsB,CAAC;AAC7B,QAAM,YAAY,oBAAI,IAAY;AAElC,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,SAAS,QAAW;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,IAAI,IAAI,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,8CAA8C,IAAI,IAAI;AAAA,MACxD;AAAA,IACF;AACA,cAAU,IAAI,IAAI,IAAI;AAEtB,QAAI,SAAkB;AACtB,QAAI;AACF,eAAS,IAAI,QAAQ,KAAK,KAAK,CAAC;AAAA,IAClC,QAAQ;AACN,eAAS,CAAC;AAAA,IACZ;AACA,QAAI,IAAI,IAAI,IAAI;AAAA,EAClB;AAEA,SAAO;AACT;AAeO,SAAS,cAAc,UAAgC;AAC5D,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACrD,0BAAsB,MAAM,MAAM;AAIlC,SAAK,aAAa,IAAI;AAAA,EACxB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/snapshot.ts","../src/name-registry.ts"],"sourcesContent":["import { getDefByName, setPreHydrationValues } from './name-registry'\nimport type { ParamsDefinition } from './types'\n\n/**\n * Snapshot keyed by definition name. Returned from `paramsSnapshot()` and\n * accepted by `hydrateParams()` — round-trip transport for SSR.\n */\nexport type ParamsSnapshot = Record<string, unknown>\n\n/**\n * Server-side: produce a snapshot of the current values for the given\n * definitions, keyed by `def.name`. The returned object is JSON-serializable\n * (call `JSON.stringify` to embed in HTML, or pass through your framework's\n * data-loader contract).\n *\n * Each definition MUST have a `name` set throws otherwise. The name keys\n * the snapshot record and links the rehydrated value back to its definition.\n *\n * Throws on duplicate names — the snapshot keys would collide.\n *\n * Reads via the synchronous `def.storage.read()` path. Definitions backed by\n * an async-only backend (sync `read()` returns undefined) snapshot as\n * defaults; pair them with a server-side prefetch step before calling\n * `paramsSnapshot()` if you want their fetched values in the snapshot.\n */\nexport function paramsSnapshot(defs: readonly ParamsDefinition[]): ParamsSnapshot {\n const out: ParamsSnapshot = {}\n const seenNames = new Set<string>()\n\n for (const def of defs) {\n if (def.name === undefined) {\n throw new Error(\n 'paramsSnapshot: every definition must have a `name` set. Add `{ name: \"<unique>\" }` to defineParams.',\n )\n }\n if (seenNames.has(def.name)) {\n throw new Error(\n `paramsSnapshot: duplicate definition name '${def.name}'. Snapshot keys would collide.`,\n )\n }\n seenNames.add(def.name)\n\n let values: unknown = undefined\n try {\n values = def.storage.read() ?? {}\n } catch {\n values = {}\n }\n out[def.name] = values\n }\n\n return out\n}\n\n/**\n * Client-side: pre-seed the params store cache from a server-rendered\n * snapshot. Must run BEFORE the first `useParams(def)` / `getParamsStore(def)`\n * call to avoid a flicker.\n *\n * For each entry in the snapshot, looks up the definition by name (must have\n * been imported / `defineParams` already evaluated on the client) and seeds\n * the pre-hydration cache. The first `ParamsStore` constructed for that def\n * uses the seeded values instead of calling its storage backend's `read()`.\n *\n * Idempotent — re-calling overwrites. Definitions whose names aren't in the\n * snapshot use their normal storage hydration path.\n */\nexport function hydrateParams(snapshot: ParamsSnapshot): void {\n for (const [name, values] of Object.entries(snapshot)) {\n setPreHydrationValues(name, values)\n // Tolerate names with no matching def — the def may load lazily via a\n // dynamic import. The pre-hydration cache holds the values until the\n // matching ParamsStore is constructed.\n void getDefByName(name)\n }\n}\n","import type { ParamsDefinition } from './types'\n\n/**\n * Side-effect registry of all definitions that carry a `name`. Populated by\n * `defineParams()` at construction time so `paramsSnapshot()` and\n * `hydrateParams()` can look up defs by name.\n *\n * Last-write-wins: re-defining the same name overwrites silently (the\n * duplicate-name DEV warning is enforced separately at acquire time in\n * `store-cache.ts`).\n */\nconst nameRegistry = new Map<string, ParamsDefinition<unknown>>()\n\n/**\n * Pre-hydration cache for SSR snapshots. `hydrateParams(snapshot)` populates\n * this; `ParamsStore` checks it during initial hydration and uses the seeded\n * values instead of calling the storage backend's `read()`.\n *\n * Once consumed by a store on first hydration the entry is left in place\n * (idempotent re-hydrate) — the consumer can call `hydrateParams()` again\n * with overrides if needed before any store is constructed.\n */\nconst preHydrationCache = new Map<string, unknown>()\n\nexport function registerNamedDef<T>(def: ParamsDefinition<T>): void {\n if (def.name === undefined) return\n nameRegistry.set(def.name, def as ParamsDefinition<unknown>)\n}\n\nexport function getDefByName(name: string): ParamsDefinition<unknown> | undefined {\n return nameRegistry.get(name)\n}\n\nexport function getRegisteredDefs(): ReadonlyArray<ParamsDefinition<unknown>> {\n return Array.from(nameRegistry.values())\n}\n\nexport function setPreHydrationValues(name: string, values: unknown): void {\n preHydrationCache.set(name, values)\n}\n\nexport function takePreHydrationValues<T>(name: string | undefined): Partial<T> | undefined {\n if (name === undefined) return undefined\n if (!preHydrationCache.has(name)) return undefined\n return preHydrationCache.get(name) as Partial<T>\n}\n\n/** Test-only: clear both registries. Not exported from the package. */\nexport function _resetNameRegistry(): void {\n nameRegistry.clear()\n preHydrationCache.clear()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,IAAM,eAAe,oBAAI,IAAuC;AAWhE,IAAM,oBAAoB,oBAAI,IAAqB;AAO5C,SAAS,aAAa,MAAqD;AAChF,SAAO,aAAa,IAAI,IAAI;AAC9B;AAMO,SAAS,sBAAsB,MAAc,QAAuB;AACzE,oBAAkB,IAAI,MAAM,MAAM;AACpC;;;ADdO,SAAS,eAAe,MAAmD;AAChF,QAAM,MAAsB,CAAC;AAC7B,QAAM,YAAY,oBAAI,IAAY;AAElC,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,SAAS,QAAW;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,IAAI,IAAI,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,8CAA8C,IAAI,IAAI;AAAA,MACxD;AAAA,IACF;AACA,cAAU,IAAI,IAAI,IAAI;AAEtB,QAAI,SAAkB;AACtB,QAAI;AACF,eAAS,IAAI,QAAQ,KAAK,KAAK,CAAC;AAAA,IAClC,QAAQ;AACN,eAAS,CAAC;AAAA,IACZ;AACA,QAAI,IAAI,IAAI,IAAI;AAAA,EAClB;AAEA,SAAO;AACT;AAeO,SAAS,cAAc,UAAgC;AAC5D,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACrD,0BAAsB,MAAM,MAAM;AAIlC,SAAK,aAAa,IAAI;AAAA,EACxB;AACF;","names":[]}
@@ -13,8 +13,8 @@ type ParamsSnapshot = Record<string, unknown>;
13
13
  * (call `JSON.stringify` to embed in HTML, or pass through your framework's
14
14
  * data-loader contract).
15
15
  *
16
- * Each definition MUST have a `name` set (added in v0.1 for forward-compat,
17
- * load-bearing in v0.2). Throws otherwise.
16
+ * Each definition MUST have a `name` set throws otherwise. The name keys
17
+ * the snapshot record and links the rehydrated value back to its definition.
18
18
  *
19
19
  * Throws on duplicate names — the snapshot keys would collide.
20
20
  *
@@ -13,8 +13,8 @@ type ParamsSnapshot = Record<string, unknown>;
13
13
  * (call `JSON.stringify` to embed in HTML, or pass through your framework's
14
14
  * data-loader contract).
15
15
  *
16
- * Each definition MUST have a `name` set (added in v0.1 for forward-compat,
17
- * load-bearing in v0.2). Throws otherwise.
16
+ * Each definition MUST have a `name` set throws otherwise. The name keys
17
+ * the snapshot record and links the rehydrated value back to its definition.
18
18
  *
19
19
  * Throws on duplicate names — the snapshot keys would collide.
20
20
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/snapshot.ts"],"sourcesContent":["import { getDefByName, setPreHydrationValues } from './name-registry'\nimport type { ParamsDefinition } from './types'\n\n/**\n * Snapshot keyed by definition name. Returned from `paramsSnapshot()` and\n * accepted by `hydrateParams()` — round-trip transport for SSR.\n */\nexport type ParamsSnapshot = Record<string, unknown>\n\n/**\n * Server-side: produce a snapshot of the current values for the given\n * definitions, keyed by `def.name`. The returned object is JSON-serializable\n * (call `JSON.stringify` to embed in HTML, or pass through your framework's\n * data-loader contract).\n *\n * Each definition MUST have a `name` set (added in v0.1 for forward-compat,\n * load-bearing in v0.2). Throws otherwise.\n *\n * Throws on duplicate names — the snapshot keys would collide.\n *\n * Reads via the synchronous `def.storage.read()` path. Definitions backed by\n * an async-only backend (sync `read()` returns undefined) snapshot as\n * defaults; pair them with a server-side prefetch step before calling\n * `paramsSnapshot()` if you want their fetched values in the snapshot.\n */\nexport function paramsSnapshot(defs: readonly ParamsDefinition[]): ParamsSnapshot {\n const out: ParamsSnapshot = {}\n const seenNames = new Set<string>()\n\n for (const def of defs) {\n if (def.name === undefined) {\n throw new Error(\n 'paramsSnapshot: every definition must have a `name` set. Add `{ name: \"<unique>\" }` to defineParams.',\n )\n }\n if (seenNames.has(def.name)) {\n throw new Error(\n `paramsSnapshot: duplicate definition name '${def.name}'. Snapshot keys would collide.`,\n )\n }\n seenNames.add(def.name)\n\n let values: unknown = undefined\n try {\n values = def.storage.read() ?? {}\n } catch {\n values = {}\n }\n out[def.name] = values\n }\n\n return out\n}\n\n/**\n * Client-side: pre-seed the params store cache from a server-rendered\n * snapshot. Must run BEFORE the first `useParams(def)` / `getParamsStore(def)`\n * call to avoid a flicker.\n *\n * For each entry in the snapshot, looks up the definition by name (must have\n * been imported / `defineParams` already evaluated on the client) and seeds\n * the pre-hydration cache. The first `ParamsStore` constructed for that def\n * uses the seeded values instead of calling its storage backend's `read()`.\n *\n * Idempotent — re-calling overwrites. Definitions whose names aren't in the\n * snapshot use their normal storage hydration path.\n */\nexport function hydrateParams(snapshot: ParamsSnapshot): void {\n for (const [name, values] of Object.entries(snapshot)) {\n setPreHydrationValues(name, values)\n // Tolerate names with no matching def — the def may load lazily via a\n // dynamic import. The pre-hydration cache holds the values until the\n // matching ParamsStore is constructed.\n void getDefByName(name)\n }\n}\n"],"mappings":";;;;;;AAyBO,SAAS,eAAe,MAAmD;AAChF,QAAM,MAAsB,CAAC;AAC7B,QAAM,YAAY,oBAAI,IAAY;AAElC,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,SAAS,QAAW;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,IAAI,IAAI,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,8CAA8C,IAAI,IAAI;AAAA,MACxD;AAAA,IACF;AACA,cAAU,IAAI,IAAI,IAAI;AAEtB,QAAI,SAAkB;AACtB,QAAI;AACF,eAAS,IAAI,QAAQ,KAAK,KAAK,CAAC;AAAA,IAClC,QAAQ;AACN,eAAS,CAAC;AAAA,IACZ;AACA,QAAI,IAAI,IAAI,IAAI;AAAA,EAClB;AAEA,SAAO;AACT;AAeO,SAAS,cAAc,UAAgC;AAC5D,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACrD,0BAAsB,MAAM,MAAM;AAIlC,SAAK,aAAa,IAAI;AAAA,EACxB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/snapshot.ts"],"sourcesContent":["import { getDefByName, setPreHydrationValues } from './name-registry'\nimport type { ParamsDefinition } from './types'\n\n/**\n * Snapshot keyed by definition name. Returned from `paramsSnapshot()` and\n * accepted by `hydrateParams()` — round-trip transport for SSR.\n */\nexport type ParamsSnapshot = Record<string, unknown>\n\n/**\n * Server-side: produce a snapshot of the current values for the given\n * definitions, keyed by `def.name`. The returned object is JSON-serializable\n * (call `JSON.stringify` to embed in HTML, or pass through your framework's\n * data-loader contract).\n *\n * Each definition MUST have a `name` set throws otherwise. The name keys\n * the snapshot record and links the rehydrated value back to its definition.\n *\n * Throws on duplicate names — the snapshot keys would collide.\n *\n * Reads via the synchronous `def.storage.read()` path. Definitions backed by\n * an async-only backend (sync `read()` returns undefined) snapshot as\n * defaults; pair them with a server-side prefetch step before calling\n * `paramsSnapshot()` if you want their fetched values in the snapshot.\n */\nexport function paramsSnapshot(defs: readonly ParamsDefinition[]): ParamsSnapshot {\n const out: ParamsSnapshot = {}\n const seenNames = new Set<string>()\n\n for (const def of defs) {\n if (def.name === undefined) {\n throw new Error(\n 'paramsSnapshot: every definition must have a `name` set. Add `{ name: \"<unique>\" }` to defineParams.',\n )\n }\n if (seenNames.has(def.name)) {\n throw new Error(\n `paramsSnapshot: duplicate definition name '${def.name}'. Snapshot keys would collide.`,\n )\n }\n seenNames.add(def.name)\n\n let values: unknown = undefined\n try {\n values = def.storage.read() ?? {}\n } catch {\n values = {}\n }\n out[def.name] = values\n }\n\n return out\n}\n\n/**\n * Client-side: pre-seed the params store cache from a server-rendered\n * snapshot. Must run BEFORE the first `useParams(def)` / `getParamsStore(def)`\n * call to avoid a flicker.\n *\n * For each entry in the snapshot, looks up the definition by name (must have\n * been imported / `defineParams` already evaluated on the client) and seeds\n * the pre-hydration cache. The first `ParamsStore` constructed for that def\n * uses the seeded values instead of calling its storage backend's `read()`.\n *\n * Idempotent — re-calling overwrites. Definitions whose names aren't in the\n * snapshot use their normal storage hydration path.\n */\nexport function hydrateParams(snapshot: ParamsSnapshot): void {\n for (const [name, values] of Object.entries(snapshot)) {\n setPreHydrationValues(name, values)\n // Tolerate names with no matching def — the def may load lazily via a\n // dynamic import. The pre-hydration cache holds the values until the\n // matching ParamsStore is constructed.\n void getDefByName(name)\n }\n}\n"],"mappings":";;;;;;AAyBO,SAAS,eAAe,MAAmD;AAChF,QAAM,MAAsB,CAAC;AAC7B,QAAM,YAAY,oBAAI,IAAY;AAElC,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,SAAS,QAAW;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,IAAI,IAAI,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,8CAA8C,IAAI,IAAI;AAAA,MACxD;AAAA,IACF;AACA,cAAU,IAAI,IAAI,IAAI;AAEtB,QAAI,SAAkB;AACtB,QAAI;AACF,eAAS,IAAI,QAAQ,KAAK,KAAK,CAAC;AAAA,IAClC,QAAQ;AACN,eAAS,CAAC;AAAA,IACZ;AACA,QAAI,IAAI,IAAI,IAAI;AAAA,EAClB;AAEA,SAAO;AACT;AAeO,SAAS,cAAc,UAAgC;AAC5D,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACrD,0BAAsB,MAAM,MAAM;AAIlC,SAAK,aAAa,IAAI;AAAA,EACxB;AACF;","names":[]}
@@ -87,8 +87,8 @@ function cookieStorage(options = {}) {
87
87
  } catch {
88
88
  }
89
89
  }
90
- // No subscribe — cookies don't have change events. Cross-tab sync would
91
- // require BroadcastChannel; out of scope for v0.2.
90
+ // No subscribe — cookies don't emit change events. Cross-tab sync would
91
+ // require BroadcastChannel.
92
92
  };
93
93
  }
94
94
  function readRawCookie(key, readCookie) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/storage/cookie/index.ts"],"sourcesContent":["import type { ParamsStorage } from '../../storage'\n\nexport interface CookieStorageAttributes {\n path?: string\n maxAge?: number\n expires?: Date\n domain?: string\n sameSite?: 'Lax' | 'Strict' | 'None'\n secure?: boolean\n}\n\nexport interface CookieStorageOptions<T> {\n /** Cookie name. Default: `'params'`. */\n readonly key?: string\n\n /**\n * Server-side: read from the incoming request's cookies. Closure-bound at\n * factory time. Receives nothing; returns the raw cookie value (already\n * URL-decoded) or `undefined`.\n *\n * If omitted, the backend assumes a client environment and reads from\n * `document.cookie`.\n */\n readonly readCookie?: () => string | undefined\n\n /**\n * Server-side: append to the outgoing response. Closure-bound at factory\n * time. Receives the full `Set-Cookie` header value (e.g. `\"params=foo;\n * Path=/; SameSite=Lax\"`); the consumer is responsible for actually\n * sending it to the browser via their framework's response API.\n *\n * If omitted, the backend assumes a client environment and writes to\n * `document.cookie`.\n */\n readonly writeCookie?: (setCookieValue: string) => void\n\n /** Whole-blob serializer. Default: `JSON.stringify`. */\n readonly serialize?: (values: Partial<T>) => string\n\n /** Whole-blob deserializer. Default: `JSON.parse`. */\n readonly deserialize?: (raw: string) => Partial<T> | undefined\n\n /** Cookie attributes appended to writes. */\n readonly attributes?: CookieStorageAttributes\n}\n\nconst isClient = typeof window !== 'undefined' && typeof document !== 'undefined'\nconst DEFAULT_KEY = 'params'\n\n/**\n * Cookie-backed storage. Works on both the server (via closure-bound\n * `readCookie` / `writeCookie` adapters) and the client (via\n * `document.cookie`). The whole values blob is JSON-encoded under one\n * cookie key.\n *\n * Use cases:\n * - User preferences that should survive a hard refresh (theme, locale).\n * - SSR snapshot transport when paired with `paramsSnapshot()` /\n * `hydrateParams()`.\n *\n * Cookies have a ~4 KB limit per domain — for filter state, prefer\n * `urlStorage`; for app config not tied to a user account, prefer\n * `localStorage`.\n */\nexport function cookieStorage<T = Record<string, unknown>>(\n options: CookieStorageOptions<T> = {},\n): ParamsStorage<T> {\n const key = options.key ?? DEFAULT_KEY\n const serialize = options.serialize ?? (JSON.stringify as (values: Partial<T>) => string)\n const deserialize = options.deserialize ?? (JSON.parse as (raw: string) => Partial<T> | undefined)\n const attrs = options.attributes ?? { path: '/', sameSite: 'Lax' as const }\n\n const readImpl = (): Partial<T> | undefined => {\n const raw = readRawCookie(key, options.readCookie)\n if (raw === undefined) return undefined\n try {\n const parsed = deserialize(raw)\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return undefined\n }\n return parsed\n } catch {\n return undefined\n }\n }\n\n return {\n name: 'cookie',\n // Cookies WORK on the server — that's the whole point of this backend.\n // The store reads on construction, which on the server uses the\n // closure-bound readCookie. clientOnly stays false.\n clientOnly: false,\n read: readImpl,\n write: (values) => {\n try {\n const existing = readImpl() ?? ({} as Partial<T>)\n const merged: Record<string, unknown> = { ...existing }\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n if (value === undefined) {\n delete merged[path]\n } else {\n merged[path] = value\n }\n }\n if (Object.keys(merged).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(merged as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback — same contract as other backends.\n }\n },\n clear: (paths) => {\n try {\n if (paths.length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n const existing = readImpl()\n if (!existing) return\n const next: Record<string, unknown> = { ...(existing as Record<string, unknown>) }\n for (const path of paths) delete next[path]\n if (Object.keys(next).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(next as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback\n }\n },\n // No subscribe — cookies don't have change events. Cross-tab sync would\n // require BroadcastChannel; out of scope for v0.2.\n }\n}\n\nfunction readRawCookie(\n key: string,\n readCookie: (() => string | undefined) | undefined,\n): string | undefined {\n if (readCookie) {\n const v = readCookie()\n return v === undefined || v === null ? undefined : decodeURIComponent(v)\n }\n if (!isClient) return undefined\n return parseCookieJar(document.cookie, key)\n}\n\nfunction writeRawCookie(\n key: string,\n value: string,\n attrs: CookieStorageAttributes,\n writeCookie: ((setCookieValue: string) => void) | undefined,\n): void {\n const setCookieValue = formatSetCookie(key, value, attrs)\n if (writeCookie) {\n writeCookie(setCookieValue)\n return\n }\n if (!isClient) return\n document.cookie = setCookieValue\n}\n\nfunction parseCookieJar(jar: string, key: string): string | undefined {\n if (!jar) return undefined\n for (const part of jar.split(';')) {\n const eq = part.indexOf('=')\n if (eq === -1) continue\n const k = part.slice(0, eq).trim()\n if (k !== key) continue\n return decodeURIComponent(part.slice(eq + 1).trim())\n }\n return undefined\n}\n\nfunction formatSetCookie(key: string, value: string, attrs: CookieStorageAttributes): string {\n const segments: string[] = [`${key}=${encodeURIComponent(value)}`]\n if (attrs.path !== undefined) segments.push(`Path=${attrs.path}`)\n if (attrs.maxAge !== undefined) segments.push(`Max-Age=${attrs.maxAge}`)\n if (attrs.expires !== undefined) segments.push(`Expires=${attrs.expires.toUTCString()}`)\n if (attrs.domain !== undefined) segments.push(`Domain=${attrs.domain}`)\n if (attrs.sameSite !== undefined) segments.push(`SameSite=${attrs.sameSite}`)\n if (attrs.secure) segments.push('Secure')\n return segments.join('; ')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CA,IAAM,WAAW,OAAO,WAAW,eAAe,OAAO,aAAa;AACtE,IAAM,cAAc;AAiBb,SAAS,cACd,UAAmC,CAAC,GAClB;AAClB,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,YAAY,QAAQ,aAAc,KAAK;AAC7C,QAAM,cAAc,QAAQ,eAAgB,KAAK;AACjD,QAAM,QAAQ,QAAQ,cAAc,EAAE,MAAM,KAAK,UAAU,MAAe;AAE1E,QAAM,WAAW,MAA8B;AAC7C,UAAM,MAAM,cAAc,KAAK,QAAQ,UAAU;AACjD,QAAI,QAAQ,OAAW,QAAO;AAC9B,QAAI;AACF,YAAM,SAAS,YAAY,GAAG;AAC9B,UAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAC1E,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA;AAAA;AAAA;AAAA,IAIN,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,OAAO,CAAC,WAAW;AACjB,UAAI;AACF,cAAM,WAAW,SAAS,KAAM,CAAC;AACjC,cAAM,SAAkC,EAAE,GAAG,SAAS;AACtD,mBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAiC,GAAG;AAC7E,cAAI,UAAU,QAAW;AACvB,mBAAO,OAAO,IAAI;AAAA,UACpB,OAAO;AACL,mBAAO,IAAI,IAAI;AAAA,UACjB;AAAA,QACF;AACA,YAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,yBAAe,KAAK,IAAI,EAAE,GAAG,OAAO,QAAQ,EAAE,GAAG,QAAQ,WAAW;AACpE;AAAA,QACF;AACA,uBAAe,KAAK,UAAU,MAAoB,GAAG,OAAO,QAAQ,WAAW;AAAA,MACjF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,OAAO,CAAC,UAAU;AAChB,UAAI;AACF,YAAI,MAAM,WAAW,GAAG;AACtB,yBAAe,KAAK,IAAI,EAAE,GAAG,OAAO,QAAQ,EAAE,GAAG,QAAQ,WAAW;AACpE;AAAA,QACF;AACA,cAAM,WAAW,SAAS;AAC1B,YAAI,CAAC,SAAU;AACf,cAAM,OAAgC,EAAE,GAAI,SAAqC;AACjF,mBAAW,QAAQ,MAAO,QAAO,KAAK,IAAI;AAC1C,YAAI,OAAO,KAAK,IAAI,EAAE,WAAW,GAAG;AAClC,yBAAe,KAAK,IAAI,EAAE,GAAG,OAAO,QAAQ,EAAE,GAAG,QAAQ,WAAW;AACpE;AAAA,QACF;AACA,uBAAe,KAAK,UAAU,IAAkB,GAAG,OAAO,QAAQ,WAAW;AAAA,MAC/E,QAAQ;AAAA,MAER;AAAA,IACF;AAAA;AAAA;AAAA,EAGF;AACF;AAEA,SAAS,cACP,KACA,YACoB;AACpB,MAAI,YAAY;AACd,UAAM,IAAI,WAAW;AACrB,WAAO,MAAM,UAAa,MAAM,OAAO,SAAY,mBAAmB,CAAC;AAAA,EACzE;AACA,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,eAAe,SAAS,QAAQ,GAAG;AAC5C;AAEA,SAAS,eACP,KACA,OACA,OACA,aACM;AACN,QAAM,iBAAiB,gBAAgB,KAAK,OAAO,KAAK;AACxD,MAAI,aAAa;AACf,gBAAY,cAAc;AAC1B;AAAA,EACF;AACA,MAAI,CAAC,SAAU;AACf,WAAS,SAAS;AACpB;AAEA,SAAS,eAAe,KAAa,KAAiC;AACpE,MAAI,CAAC,IAAK,QAAO;AACjB,aAAW,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,OAAO,GAAI;AACf,UAAM,IAAI,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACjC,QAAI,MAAM,IAAK;AACf,WAAO,mBAAmB,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,KAAa,OAAe,OAAwC;AAC3F,QAAM,WAAqB,CAAC,GAAG,GAAG,IAAI,mBAAmB,KAAK,CAAC,EAAE;AACjE,MAAI,MAAM,SAAS,OAAW,UAAS,KAAK,QAAQ,MAAM,IAAI,EAAE;AAChE,MAAI,MAAM,WAAW,OAAW,UAAS,KAAK,WAAW,MAAM,MAAM,EAAE;AACvE,MAAI,MAAM,YAAY,OAAW,UAAS,KAAK,WAAW,MAAM,QAAQ,YAAY,CAAC,EAAE;AACvF,MAAI,MAAM,WAAW,OAAW,UAAS,KAAK,UAAU,MAAM,MAAM,EAAE;AACtE,MAAI,MAAM,aAAa,OAAW,UAAS,KAAK,YAAY,MAAM,QAAQ,EAAE;AAC5E,MAAI,MAAM,OAAQ,UAAS,KAAK,QAAQ;AACxC,SAAO,SAAS,KAAK,IAAI;AAC3B;","names":[]}
1
+ {"version":3,"sources":["../../src/storage/cookie/index.ts"],"sourcesContent":["import type { ParamsStorage } from '../../storage'\n\nexport interface CookieStorageAttributes {\n path?: string\n maxAge?: number\n expires?: Date\n domain?: string\n sameSite?: 'Lax' | 'Strict' | 'None'\n secure?: boolean\n}\n\nexport interface CookieStorageOptions<T> {\n /** Cookie name. Default: `'params'`. */\n readonly key?: string\n\n /**\n * Server-side: read from the incoming request's cookies. Closure-bound at\n * factory time. Receives nothing; returns the raw cookie value (already\n * URL-decoded) or `undefined`.\n *\n * If omitted, the backend assumes a client environment and reads from\n * `document.cookie`.\n */\n readonly readCookie?: () => string | undefined\n\n /**\n * Server-side: append to the outgoing response. Closure-bound at factory\n * time. Receives the full `Set-Cookie` header value (e.g. `\"params=foo;\n * Path=/; SameSite=Lax\"`); the consumer is responsible for actually\n * sending it to the browser via their framework's response API.\n *\n * If omitted, the backend assumes a client environment and writes to\n * `document.cookie`.\n */\n readonly writeCookie?: (setCookieValue: string) => void\n\n /** Whole-blob serializer. Default: `JSON.stringify`. */\n readonly serialize?: (values: Partial<T>) => string\n\n /** Whole-blob deserializer. Default: `JSON.parse`. */\n readonly deserialize?: (raw: string) => Partial<T> | undefined\n\n /** Cookie attributes appended to writes. */\n readonly attributes?: CookieStorageAttributes\n}\n\nconst isClient = typeof window !== 'undefined' && typeof document !== 'undefined'\nconst DEFAULT_KEY = 'params'\n\n/**\n * Cookie-backed storage. Works on both the server (via closure-bound\n * `readCookie` / `writeCookie` adapters) and the client (via\n * `document.cookie`). The whole values blob is JSON-encoded under one\n * cookie key.\n *\n * Use cases:\n * - User preferences that should survive a hard refresh (theme, locale).\n * - SSR snapshot transport when paired with `paramsSnapshot()` /\n * `hydrateParams()`.\n *\n * Cookies have a ~4 KB limit per domain — for filter state, prefer\n * `urlStorage`; for app config not tied to a user account, prefer\n * `localStorage`.\n */\nexport function cookieStorage<T = Record<string, unknown>>(\n options: CookieStorageOptions<T> = {},\n): ParamsStorage<T> {\n const key = options.key ?? DEFAULT_KEY\n const serialize = options.serialize ?? (JSON.stringify as (values: Partial<T>) => string)\n const deserialize = options.deserialize ?? (JSON.parse as (raw: string) => Partial<T> | undefined)\n const attrs = options.attributes ?? { path: '/', sameSite: 'Lax' as const }\n\n const readImpl = (): Partial<T> | undefined => {\n const raw = readRawCookie(key, options.readCookie)\n if (raw === undefined) return undefined\n try {\n const parsed = deserialize(raw)\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return undefined\n }\n return parsed\n } catch {\n return undefined\n }\n }\n\n return {\n name: 'cookie',\n // Cookies WORK on the server — that's the whole point of this backend.\n // The store reads on construction, which on the server uses the\n // closure-bound readCookie. clientOnly stays false.\n clientOnly: false,\n read: readImpl,\n write: (values) => {\n try {\n const existing = readImpl() ?? ({} as Partial<T>)\n const merged: Record<string, unknown> = { ...existing }\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n if (value === undefined) {\n delete merged[path]\n } else {\n merged[path] = value\n }\n }\n if (Object.keys(merged).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(merged as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback — same contract as other backends.\n }\n },\n clear: (paths) => {\n try {\n if (paths.length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n const existing = readImpl()\n if (!existing) return\n const next: Record<string, unknown> = { ...(existing as Record<string, unknown>) }\n for (const path of paths) delete next[path]\n if (Object.keys(next).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(next as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback\n }\n },\n // No subscribe — cookies don't emit change events. Cross-tab sync would\n // require BroadcastChannel.\n }\n}\n\nfunction readRawCookie(\n key: string,\n readCookie: (() => string | undefined) | undefined,\n): string | undefined {\n if (readCookie) {\n const v = readCookie()\n return v === undefined || v === null ? undefined : decodeURIComponent(v)\n }\n if (!isClient) return undefined\n return parseCookieJar(document.cookie, key)\n}\n\nfunction writeRawCookie(\n key: string,\n value: string,\n attrs: CookieStorageAttributes,\n writeCookie: ((setCookieValue: string) => void) | undefined,\n): void {\n const setCookieValue = formatSetCookie(key, value, attrs)\n if (writeCookie) {\n writeCookie(setCookieValue)\n return\n }\n if (!isClient) return\n document.cookie = setCookieValue\n}\n\nfunction parseCookieJar(jar: string, key: string): string | undefined {\n if (!jar) return undefined\n for (const part of jar.split(';')) {\n const eq = part.indexOf('=')\n if (eq === -1) continue\n const k = part.slice(0, eq).trim()\n if (k !== key) continue\n return decodeURIComponent(part.slice(eq + 1).trim())\n }\n return undefined\n}\n\nfunction formatSetCookie(key: string, value: string, attrs: CookieStorageAttributes): string {\n const segments: string[] = [`${key}=${encodeURIComponent(value)}`]\n if (attrs.path !== undefined) segments.push(`Path=${attrs.path}`)\n if (attrs.maxAge !== undefined) segments.push(`Max-Age=${attrs.maxAge}`)\n if (attrs.expires !== undefined) segments.push(`Expires=${attrs.expires.toUTCString()}`)\n if (attrs.domain !== undefined) segments.push(`Domain=${attrs.domain}`)\n if (attrs.sameSite !== undefined) segments.push(`SameSite=${attrs.sameSite}`)\n if (attrs.secure) segments.push('Secure')\n return segments.join('; ')\n}\n"],"mappings":"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","names":[]}
@@ -63,8 +63,8 @@ function cookieStorage(options = {}) {
63
63
  } catch {
64
64
  }
65
65
  }
66
- // No subscribe — cookies don't have change events. Cross-tab sync would
67
- // require BroadcastChannel; out of scope for v0.2.
66
+ // No subscribe — cookies don't emit change events. Cross-tab sync would
67
+ // require BroadcastChannel.
68
68
  };
69
69
  }
70
70
  function readRawCookie(key, readCookie) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/storage/cookie/index.ts"],"sourcesContent":["import type { ParamsStorage } from '../../storage'\n\nexport interface CookieStorageAttributes {\n path?: string\n maxAge?: number\n expires?: Date\n domain?: string\n sameSite?: 'Lax' | 'Strict' | 'None'\n secure?: boolean\n}\n\nexport interface CookieStorageOptions<T> {\n /** Cookie name. Default: `'params'`. */\n readonly key?: string\n\n /**\n * Server-side: read from the incoming request's cookies. Closure-bound at\n * factory time. Receives nothing; returns the raw cookie value (already\n * URL-decoded) or `undefined`.\n *\n * If omitted, the backend assumes a client environment and reads from\n * `document.cookie`.\n */\n readonly readCookie?: () => string | undefined\n\n /**\n * Server-side: append to the outgoing response. Closure-bound at factory\n * time. Receives the full `Set-Cookie` header value (e.g. `\"params=foo;\n * Path=/; SameSite=Lax\"`); the consumer is responsible for actually\n * sending it to the browser via their framework's response API.\n *\n * If omitted, the backend assumes a client environment and writes to\n * `document.cookie`.\n */\n readonly writeCookie?: (setCookieValue: string) => void\n\n /** Whole-blob serializer. Default: `JSON.stringify`. */\n readonly serialize?: (values: Partial<T>) => string\n\n /** Whole-blob deserializer. Default: `JSON.parse`. */\n readonly deserialize?: (raw: string) => Partial<T> | undefined\n\n /** Cookie attributes appended to writes. */\n readonly attributes?: CookieStorageAttributes\n}\n\nconst isClient = typeof window !== 'undefined' && typeof document !== 'undefined'\nconst DEFAULT_KEY = 'params'\n\n/**\n * Cookie-backed storage. Works on both the server (via closure-bound\n * `readCookie` / `writeCookie` adapters) and the client (via\n * `document.cookie`). The whole values blob is JSON-encoded under one\n * cookie key.\n *\n * Use cases:\n * - User preferences that should survive a hard refresh (theme, locale).\n * - SSR snapshot transport when paired with `paramsSnapshot()` /\n * `hydrateParams()`.\n *\n * Cookies have a ~4 KB limit per domain — for filter state, prefer\n * `urlStorage`; for app config not tied to a user account, prefer\n * `localStorage`.\n */\nexport function cookieStorage<T = Record<string, unknown>>(\n options: CookieStorageOptions<T> = {},\n): ParamsStorage<T> {\n const key = options.key ?? DEFAULT_KEY\n const serialize = options.serialize ?? (JSON.stringify as (values: Partial<T>) => string)\n const deserialize = options.deserialize ?? (JSON.parse as (raw: string) => Partial<T> | undefined)\n const attrs = options.attributes ?? { path: '/', sameSite: 'Lax' as const }\n\n const readImpl = (): Partial<T> | undefined => {\n const raw = readRawCookie(key, options.readCookie)\n if (raw === undefined) return undefined\n try {\n const parsed = deserialize(raw)\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return undefined\n }\n return parsed\n } catch {\n return undefined\n }\n }\n\n return {\n name: 'cookie',\n // Cookies WORK on the server — that's the whole point of this backend.\n // The store reads on construction, which on the server uses the\n // closure-bound readCookie. clientOnly stays false.\n clientOnly: false,\n read: readImpl,\n write: (values) => {\n try {\n const existing = readImpl() ?? ({} as Partial<T>)\n const merged: Record<string, unknown> = { ...existing }\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n if (value === undefined) {\n delete merged[path]\n } else {\n merged[path] = value\n }\n }\n if (Object.keys(merged).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(merged as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback — same contract as other backends.\n }\n },\n clear: (paths) => {\n try {\n if (paths.length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n const existing = readImpl()\n if (!existing) return\n const next: Record<string, unknown> = { ...(existing as Record<string, unknown>) }\n for (const path of paths) delete next[path]\n if (Object.keys(next).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(next as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback\n }\n },\n // No subscribe — cookies don't have change events. Cross-tab sync would\n // require BroadcastChannel; out of scope for v0.2.\n }\n}\n\nfunction readRawCookie(\n key: string,\n readCookie: (() => string | undefined) | undefined,\n): string | undefined {\n if (readCookie) {\n const v = readCookie()\n return v === undefined || v === null ? undefined : decodeURIComponent(v)\n }\n if (!isClient) return undefined\n return parseCookieJar(document.cookie, key)\n}\n\nfunction writeRawCookie(\n key: string,\n value: string,\n attrs: CookieStorageAttributes,\n writeCookie: ((setCookieValue: string) => void) | undefined,\n): void {\n const setCookieValue = formatSetCookie(key, value, attrs)\n if (writeCookie) {\n writeCookie(setCookieValue)\n return\n }\n if (!isClient) return\n document.cookie = setCookieValue\n}\n\nfunction parseCookieJar(jar: string, key: string): string | undefined {\n if (!jar) return undefined\n for (const part of jar.split(';')) {\n const eq = part.indexOf('=')\n if (eq === -1) continue\n const k = part.slice(0, eq).trim()\n if (k !== key) continue\n return decodeURIComponent(part.slice(eq + 1).trim())\n }\n return undefined\n}\n\nfunction formatSetCookie(key: string, value: string, attrs: CookieStorageAttributes): string {\n const segments: string[] = [`${key}=${encodeURIComponent(value)}`]\n if (attrs.path !== undefined) segments.push(`Path=${attrs.path}`)\n if (attrs.maxAge !== undefined) segments.push(`Max-Age=${attrs.maxAge}`)\n if (attrs.expires !== undefined) segments.push(`Expires=${attrs.expires.toUTCString()}`)\n if (attrs.domain !== undefined) segments.push(`Domain=${attrs.domain}`)\n if (attrs.sameSite !== undefined) segments.push(`SameSite=${attrs.sameSite}`)\n if (attrs.secure) segments.push('Secure')\n return segments.join('; ')\n}\n"],"mappings":";AA8CA,IAAM,WAAW,OAAO,WAAW,eAAe,OAAO,aAAa;AACtE,IAAM,cAAc;AAiBb,SAAS,cACd,UAAmC,CAAC,GAClB;AAClB,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,YAAY,QAAQ,aAAc,KAAK;AAC7C,QAAM,cAAc,QAAQ,eAAgB,KAAK;AACjD,QAAM,QAAQ,QAAQ,cAAc,EAAE,MAAM,KAAK,UAAU,MAAe;AAE1E,QAAM,WAAW,MAA8B;AAC7C,UAAM,MAAM,cAAc,KAAK,QAAQ,UAAU;AACjD,QAAI,QAAQ,OAAW,QAAO;AAC9B,QAAI;AACF,YAAM,SAAS,YAAY,GAAG;AAC9B,UAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAC1E,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA;AAAA;AAAA;AAAA,IAIN,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,OAAO,CAAC,WAAW;AACjB,UAAI;AACF,cAAM,WAAW,SAAS,KAAM,CAAC;AACjC,cAAM,SAAkC,EAAE,GAAG,SAAS;AACtD,mBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAiC,GAAG;AAC7E,cAAI,UAAU,QAAW;AACvB,mBAAO,OAAO,IAAI;AAAA,UACpB,OAAO;AACL,mBAAO,IAAI,IAAI;AAAA,UACjB;AAAA,QACF;AACA,YAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,yBAAe,KAAK,IAAI,EAAE,GAAG,OAAO,QAAQ,EAAE,GAAG,QAAQ,WAAW;AACpE;AAAA,QACF;AACA,uBAAe,KAAK,UAAU,MAAoB,GAAG,OAAO,QAAQ,WAAW;AAAA,MACjF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,OAAO,CAAC,UAAU;AAChB,UAAI;AACF,YAAI,MAAM,WAAW,GAAG;AACtB,yBAAe,KAAK,IAAI,EAAE,GAAG,OAAO,QAAQ,EAAE,GAAG,QAAQ,WAAW;AACpE;AAAA,QACF;AACA,cAAM,WAAW,SAAS;AAC1B,YAAI,CAAC,SAAU;AACf,cAAM,OAAgC,EAAE,GAAI,SAAqC;AACjF,mBAAW,QAAQ,MAAO,QAAO,KAAK,IAAI;AAC1C,YAAI,OAAO,KAAK,IAAI,EAAE,WAAW,GAAG;AAClC,yBAAe,KAAK,IAAI,EAAE,GAAG,OAAO,QAAQ,EAAE,GAAG,QAAQ,WAAW;AACpE;AAAA,QACF;AACA,uBAAe,KAAK,UAAU,IAAkB,GAAG,OAAO,QAAQ,WAAW;AAAA,MAC/E,QAAQ;AAAA,MAER;AAAA,IACF;AAAA;AAAA;AAAA,EAGF;AACF;AAEA,SAAS,cACP,KACA,YACoB;AACpB,MAAI,YAAY;AACd,UAAM,IAAI,WAAW;AACrB,WAAO,MAAM,UAAa,MAAM,OAAO,SAAY,mBAAmB,CAAC;AAAA,EACzE;AACA,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,eAAe,SAAS,QAAQ,GAAG;AAC5C;AAEA,SAAS,eACP,KACA,OACA,OACA,aACM;AACN,QAAM,iBAAiB,gBAAgB,KAAK,OAAO,KAAK;AACxD,MAAI,aAAa;AACf,gBAAY,cAAc;AAC1B;AAAA,EACF;AACA,MAAI,CAAC,SAAU;AACf,WAAS,SAAS;AACpB;AAEA,SAAS,eAAe,KAAa,KAAiC;AACpE,MAAI,CAAC,IAAK,QAAO;AACjB,aAAW,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,OAAO,GAAI;AACf,UAAM,IAAI,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACjC,QAAI,MAAM,IAAK;AACf,WAAO,mBAAmB,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,KAAa,OAAe,OAAwC;AAC3F,QAAM,WAAqB,CAAC,GAAG,GAAG,IAAI,mBAAmB,KAAK,CAAC,EAAE;AACjE,MAAI,MAAM,SAAS,OAAW,UAAS,KAAK,QAAQ,MAAM,IAAI,EAAE;AAChE,MAAI,MAAM,WAAW,OAAW,UAAS,KAAK,WAAW,MAAM,MAAM,EAAE;AACvE,MAAI,MAAM,YAAY,OAAW,UAAS,KAAK,WAAW,MAAM,QAAQ,YAAY,CAAC,EAAE;AACvF,MAAI,MAAM,WAAW,OAAW,UAAS,KAAK,UAAU,MAAM,MAAM,EAAE;AACtE,MAAI,MAAM,aAAa,OAAW,UAAS,KAAK,YAAY,MAAM,QAAQ,EAAE;AAC5E,MAAI,MAAM,OAAQ,UAAS,KAAK,QAAQ;AACxC,SAAO,SAAS,KAAK,IAAI;AAC3B;","names":[]}
1
+ {"version":3,"sources":["../../src/storage/cookie/index.ts"],"sourcesContent":["import type { ParamsStorage } from '../../storage'\n\nexport interface CookieStorageAttributes {\n path?: string\n maxAge?: number\n expires?: Date\n domain?: string\n sameSite?: 'Lax' | 'Strict' | 'None'\n secure?: boolean\n}\n\nexport interface CookieStorageOptions<T> {\n /** Cookie name. Default: `'params'`. */\n readonly key?: string\n\n /**\n * Server-side: read from the incoming request's cookies. Closure-bound at\n * factory time. Receives nothing; returns the raw cookie value (already\n * URL-decoded) or `undefined`.\n *\n * If omitted, the backend assumes a client environment and reads from\n * `document.cookie`.\n */\n readonly readCookie?: () => string | undefined\n\n /**\n * Server-side: append to the outgoing response. Closure-bound at factory\n * time. Receives the full `Set-Cookie` header value (e.g. `\"params=foo;\n * Path=/; SameSite=Lax\"`); the consumer is responsible for actually\n * sending it to the browser via their framework's response API.\n *\n * If omitted, the backend assumes a client environment and writes to\n * `document.cookie`.\n */\n readonly writeCookie?: (setCookieValue: string) => void\n\n /** Whole-blob serializer. Default: `JSON.stringify`. */\n readonly serialize?: (values: Partial<T>) => string\n\n /** Whole-blob deserializer. Default: `JSON.parse`. */\n readonly deserialize?: (raw: string) => Partial<T> | undefined\n\n /** Cookie attributes appended to writes. */\n readonly attributes?: CookieStorageAttributes\n}\n\nconst isClient = typeof window !== 'undefined' && typeof document !== 'undefined'\nconst DEFAULT_KEY = 'params'\n\n/**\n * Cookie-backed storage. Works on both the server (via closure-bound\n * `readCookie` / `writeCookie` adapters) and the client (via\n * `document.cookie`). The whole values blob is JSON-encoded under one\n * cookie key.\n *\n * Use cases:\n * - User preferences that should survive a hard refresh (theme, locale).\n * - SSR snapshot transport when paired with `paramsSnapshot()` /\n * `hydrateParams()`.\n *\n * Cookies have a ~4 KB limit per domain — for filter state, prefer\n * `urlStorage`; for app config not tied to a user account, prefer\n * `localStorage`.\n */\nexport function cookieStorage<T = Record<string, unknown>>(\n options: CookieStorageOptions<T> = {},\n): ParamsStorage<T> {\n const key = options.key ?? DEFAULT_KEY\n const serialize = options.serialize ?? (JSON.stringify as (values: Partial<T>) => string)\n const deserialize = options.deserialize ?? (JSON.parse as (raw: string) => Partial<T> | undefined)\n const attrs = options.attributes ?? { path: '/', sameSite: 'Lax' as const }\n\n const readImpl = (): Partial<T> | undefined => {\n const raw = readRawCookie(key, options.readCookie)\n if (raw === undefined) return undefined\n try {\n const parsed = deserialize(raw)\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return undefined\n }\n return parsed\n } catch {\n return undefined\n }\n }\n\n return {\n name: 'cookie',\n // Cookies WORK on the server — that's the whole point of this backend.\n // The store reads on construction, which on the server uses the\n // closure-bound readCookie. clientOnly stays false.\n clientOnly: false,\n read: readImpl,\n write: (values) => {\n try {\n const existing = readImpl() ?? ({} as Partial<T>)\n const merged: Record<string, unknown> = { ...existing }\n for (const [path, value] of Object.entries(values as Record<string, unknown>)) {\n if (value === undefined) {\n delete merged[path]\n } else {\n merged[path] = value\n }\n }\n if (Object.keys(merged).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(merged as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback — same contract as other backends.\n }\n },\n clear: (paths) => {\n try {\n if (paths.length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n const existing = readImpl()\n if (!existing) return\n const next: Record<string, unknown> = { ...(existing as Record<string, unknown>) }\n for (const path of paths) delete next[path]\n if (Object.keys(next).length === 0) {\n writeRawCookie(key, '', { ...attrs, maxAge: 0 }, options.writeCookie)\n return\n }\n writeRawCookie(key, serialize(next as Partial<T>), attrs, options.writeCookie)\n } catch {\n // Silent fallback\n }\n },\n // No subscribe — cookies don't emit change events. Cross-tab sync would\n // require BroadcastChannel.\n }\n}\n\nfunction readRawCookie(\n key: string,\n readCookie: (() => string | undefined) | undefined,\n): string | undefined {\n if (readCookie) {\n const v = readCookie()\n return v === undefined || v === null ? undefined : decodeURIComponent(v)\n }\n if (!isClient) return undefined\n return parseCookieJar(document.cookie, key)\n}\n\nfunction writeRawCookie(\n key: string,\n value: string,\n attrs: CookieStorageAttributes,\n writeCookie: ((setCookieValue: string) => void) | undefined,\n): void {\n const setCookieValue = formatSetCookie(key, value, attrs)\n if (writeCookie) {\n writeCookie(setCookieValue)\n return\n }\n if (!isClient) return\n document.cookie = setCookieValue\n}\n\nfunction parseCookieJar(jar: string, key: string): string | undefined {\n if (!jar) return undefined\n for (const part of jar.split(';')) {\n const eq = part.indexOf('=')\n if (eq === -1) continue\n const k = part.slice(0, eq).trim()\n if (k !== key) continue\n return decodeURIComponent(part.slice(eq + 1).trim())\n }\n return undefined\n}\n\nfunction formatSetCookie(key: string, value: string, attrs: CookieStorageAttributes): string {\n const segments: string[] = [`${key}=${encodeURIComponent(value)}`]\n if (attrs.path !== undefined) segments.push(`Path=${attrs.path}`)\n if (attrs.maxAge !== undefined) segments.push(`Max-Age=${attrs.maxAge}`)\n if (attrs.expires !== undefined) segments.push(`Expires=${attrs.expires.toUTCString()}`)\n if (attrs.domain !== undefined) segments.push(`Domain=${attrs.domain}`)\n if (attrs.sameSite !== undefined) segments.push(`SameSite=${attrs.sameSite}`)\n if (attrs.secure) segments.push('Secure')\n return segments.join('; ')\n}\n"],"mappings":"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","names":[]}
@@ -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 ?? {};
@@ -114,7 +143,13 @@ function urlStorage(options = {}) {
114
143
  if (values) callback(values);
115
144
  };
116
145
  window.addEventListener("popstate", handler);
117
- return () => window.removeEventListener("popstate", handler);
146
+ window.addEventListener(LOCATION_CHANGE_EVENT, handler);
147
+ patchHistory();
148
+ return () => {
149
+ window.removeEventListener("popstate", handler);
150
+ window.removeEventListener(LOCATION_CHANGE_EVENT, handler);
151
+ unpatchHistory();
152
+ };
118
153
  }
119
154
  };
120
155
  }
@@ -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":";;;;;;;;;;;;;;;;;;;;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;AAoB5B,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;AAC5B,YAAM,UAAU,MAAM;AACpB,cAAM,SAAS,SAAS;AACxB,YAAI,OAAQ,UAAS,MAAM;AAAA,MAC7B;AACA,aAAO,iBAAiB,YAAY,OAAO;AAC3C,aAAO,MAAM,OAAO,oBAAoB,YAAY,OAAO;AAAA,IAC7D;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":[]}
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 *\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 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":"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","names":[]}
@@ -19,7 +19,9 @@ 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.
23
25
  *
24
26
  * `omitWhenDefault` is honored at the engine level (the store sends `undefined`
25
27
  * for paths at default; the backend deletes them from the URL).
@@ -19,7 +19,9 @@ 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.
23
25
  *
24
26
  * `omitWhenDefault` is honored at the engine level (the store sends `undefined`
25
27
  * 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 ?? {};
@@ -83,7 +112,13 @@ function urlStorage(options = {}) {
83
112
  if (values) callback(values);
84
113
  };
85
114
  window.addEventListener("popstate", handler);
86
- return () => window.removeEventListener("popstate", handler);
115
+ window.addEventListener(LOCATION_CHANGE_EVENT, handler);
116
+ patchHistory();
117
+ return () => {
118
+ window.removeEventListener("popstate", handler);
119
+ window.removeEventListener(LOCATION_CHANGE_EVENT, handler);
120
+ unpatchHistory();
121
+ };
87
122
  }
88
123
  };
89
124
  }
@@ -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 *\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 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.3.0",
3
+ "version": "0.5.0",
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"
@@ -87,7 +87,7 @@
87
87
  "fake-indexeddb": "^6.0.0",
88
88
  "valibot": "^1.3.1",
89
89
  "zod": "^3.23.8",
90
- "@victorylabs/forms": "0.3.0",
90
+ "@victorylabs/forms": "0.4.0",
91
91
  "@victorylabs/utils": "0.1.0"
92
92
  },
93
93
  "peerDependencies": {