@victorylabs/params 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{chunk-NUO3GOXV.js → chunk-5UKBDZTP.js} +37 -2
  2. package/dist/chunk-5UKBDZTP.js.map +1 -0
  3. package/dist/{chunk-43PUAYQP.js → chunk-DSAHBEAQ.js} +44 -18
  4. package/dist/chunk-DSAHBEAQ.js.map +1 -0
  5. package/dist/devtools.d.cts +1 -1
  6. package/dist/devtools.d.ts +1 -1
  7. package/dist/index.cjs +79 -17
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +47 -11
  10. package/dist/index.d.ts +47 -11
  11. package/dist/index.js +5 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/forms-reverse.cjs +71 -17
  14. package/dist/integrations/forms-reverse.cjs.map +1 -1
  15. package/dist/integrations/forms-reverse.js +2 -2
  16. package/dist/integrations/forms.cjs +71 -17
  17. package/dist/integrations/forms.cjs.map +1 -1
  18. package/dist/integrations/forms.js +2 -2
  19. package/dist/{params-store-Cgbtn53j.d.cts → params-store-4Lcb1M_X.d.cts} +29 -1
  20. package/dist/{params-store-CguA9-yr.d.ts → params-store-f3pmPdw3.d.ts} +29 -1
  21. package/dist/react.cjs +127 -54
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +42 -4
  24. package/dist/react.d.ts +42 -4
  25. package/dist/react.js +38 -20
  26. package/dist/react.js.map +1 -1
  27. package/dist/storage/idb.cjs +56 -3
  28. package/dist/storage/idb.cjs.map +1 -1
  29. package/dist/storage/idb.d.cts +40 -8
  30. package/dist/storage/idb.d.ts +40 -8
  31. package/dist/storage/idb.js +56 -3
  32. package/dist/storage/idb.js.map +1 -1
  33. package/dist/storage/url.cjs.map +1 -1
  34. package/dist/storage/url.js +1 -1
  35. package/package.json +3 -2
  36. package/dist/chunk-43PUAYQP.js.map +0 -1
  37. package/dist/chunk-NUO3GOXV.js.map +0 -1
package/dist/react.js CHANGED
@@ -1,21 +1,36 @@
1
1
  import {
2
2
  acquire,
3
3
  release
4
- } from "./chunk-43PUAYQP.js";
4
+ } from "./chunk-DSAHBEAQ.js";
5
5
  import "./chunk-4T4THPFW.js";
6
6
  import "./chunk-5NSLHAHG.js";
7
7
  import {
8
8
  defaultSerialize
9
- } from "./chunk-NUO3GOXV.js";
9
+ } from "./chunk-5UKBDZTP.js";
10
+
11
+ // src/react/use-debounced-value.ts
12
+ import { useEffect, useState } from "react";
13
+ function useDebouncedValue(value, ms) {
14
+ const [debounced, setDebounced] = useState(value);
15
+ useEffect(() => {
16
+ if (ms <= 0) {
17
+ setDebounced(value);
18
+ return;
19
+ }
20
+ const id = setTimeout(() => setDebounced(value), ms);
21
+ return () => clearTimeout(id);
22
+ }, [value, ms]);
23
+ return debounced;
24
+ }
10
25
 
11
26
  // src/react/use-field-input.tsx
12
- import { useCallback, useEffect as useEffect2, useRef, useState } from "react";
27
+ import { useCallback, useEffect as useEffect3, useRef, useState as useState2 } from "react";
13
28
 
14
29
  // src/react/use-field-value.ts
15
- import { useEffect, useReducer } from "react";
30
+ import { useEffect as useEffect2, useReducer } from "react";
16
31
  function useFieldValue(store, path) {
17
32
  const [, forceRender] = useReducer((tick) => tick + 1, 0);
18
- useEffect(() => store.subscribe(path, forceRender), [store, path]);
33
+ useEffect2(() => store.subscribe(path, forceRender), [store, path]);
19
34
  return store.getValue(path);
20
35
  }
21
36
 
@@ -24,10 +39,10 @@ function useFieldInput(store, path) {
24
39
  const storeValue = useFieldValue(store, path);
25
40
  const config = store.getFieldConfig(path);
26
41
  const debounceMs = config?.debounce ?? 0;
27
- const [shadow, setShadow] = useState(null);
42
+ const [shadow, setShadow] = useState2(null);
28
43
  const debouncerRef = useRef(null);
29
44
  const lastStoreValueRef = useRef(storeValue);
30
- useEffect2(() => {
45
+ useEffect3(() => {
31
46
  if (Object.is(lastStoreValueRef.current, storeValue)) return;
32
47
  lastStoreValueRef.current = storeValue;
33
48
  if (shadow !== null && shadow !== defaultSerialize(storeValue)) {
@@ -45,7 +60,7 @@ function useFieldInput(store, path) {
45
60
  setShadow(next);
46
61
  debouncerRef.current?.trigger(next);
47
62
  } else {
48
- store.set(path, next);
63
+ store.setField(path, next);
49
64
  }
50
65
  },
51
66
  [store, path, debounceMs]
@@ -65,7 +80,7 @@ function createDebouncer(store, path, ms, onCommit) {
65
80
  let pendingValue = null;
66
81
  const commit = () => {
67
82
  if (pendingValue !== null) {
68
- store.set(path, pendingValue);
83
+ store.setField(path, pendingValue);
69
84
  pendingValue = null;
70
85
  onCommit();
71
86
  }
@@ -93,16 +108,16 @@ function createDebouncer(store, path, ms, onCommit) {
93
108
  }
94
109
 
95
110
  // src/react/use-params.tsx
96
- import { useCallback as useCallback2, useDeferredValue, useEffect as useEffect4, useReducer as useReducer2 } from "react";
111
+ import { useCallback as useCallback2, useDeferredValue, useEffect as useEffect5, useReducer as useReducer2 } from "react";
97
112
 
98
113
  // src/react/use-shared-store.ts
99
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
114
+ import { useEffect as useEffect4, useRef as useRef2 } from "react";
100
115
  function useSharedStore(def) {
101
116
  const storeRef = useRef2(null);
102
117
  if (storeRef.current === null) {
103
118
  storeRef.current = acquire(def);
104
119
  }
105
- useEffect3(
120
+ useEffect4(
106
121
  () => () => {
107
122
  release(def);
108
123
  storeRef.current = null;
@@ -116,15 +131,13 @@ function useSharedStore(def) {
116
131
  function useParams(def) {
117
132
  const store = useSharedStore(def);
118
133
  const [, forceRender] = useReducer2((tick) => tick + 1, 0);
119
- useEffect4(() => store.subscribe("", forceRender), [store]);
134
+ useEffect5(() => store.subscribe("", forceRender), [store]);
120
135
  const set = useCallback2(
121
- ((pathOrPartial, valueOrOptions, maybeOptions) => {
122
- if (typeof pathOrPartial === "string") {
123
- store.set(pathOrPartial, valueOrOptions, maybeOptions);
124
- } else {
125
- store.set(pathOrPartial, valueOrOptions);
126
- }
127
- }),
136
+ (partial, options) => store.set(partial, options),
137
+ [store]
138
+ );
139
+ const setField = useCallback2(
140
+ (path, value2, options) => store.setField(path, value2, options),
128
141
  [store]
129
142
  );
130
143
  const toggle = useCallback2(
@@ -176,11 +189,15 @@ function useParams(def) {
176
189
  const deferred = (path) => useDeferredValue(useFieldValue(store, path));
177
190
  const input = (path) => useFieldInput(store, path);
178
191
  return {
192
+ // Cast: store.getValues() returns Partial<T> for the storage layer's
193
+ // sake, but every field in ParamsDefinition declares a default so the
194
+ // hydrated view is structurally complete. See ParamsController.values.
179
195
  values: store.getValues(),
180
196
  value,
181
197
  deferred,
182
198
  input,
183
199
  set,
200
+ setField,
184
201
  toggle,
185
202
  append,
186
203
  remove: removeFn,
@@ -195,6 +212,7 @@ function useParams(def) {
195
212
  };
196
213
  }
197
214
  export {
215
+ useDebouncedValue,
198
216
  useFieldInput,
199
217
  useFieldValue,
200
218
  useParams
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/use-field-input.tsx","../src/react/use-field-value.ts","../src/react/use-params.tsx","../src/react/use-shared-store.ts"],"sourcesContent":["import type { ChangeEvent } from 'react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport type { ParamsStore } from '../params-store'\nimport { defaultSerialize } from '../schema'\nimport { useFieldValue } from './use-field-value'\n\nexport interface InputBindings {\n name: string\n value: string\n onChange: (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void\n onBlur: () => void\n}\n\ninterface DebounceController {\n trigger: (value: string) => void\n flush: () => void\n cancel: () => void\n}\n\n/**\n * Hook that returns input bindings for a single path. Handles:\n * - Reading the current store value (debounced shadow during typing)\n * - Per-field debounce from the field config\n * - Shadow cancellation when an external `set()` invalidates the pending value\n * - `onBlur` flushes any pending debounce immediately\n *\n * The bindings are spreadable directly: `<input {...p.input('query')} />`.\n */\nexport function useFieldInput(store: ParamsStore, path: string): InputBindings {\n const storeValue = useFieldValue<unknown>(store, path)\n const config = store.getFieldConfig(path)\n const debounceMs = config?.debounce ?? 0\n\n const [shadow, setShadow] = useState<string | null>(null)\n const debouncerRef = useRef<DebounceController | null>(null)\n const lastStoreValueRef = useRef(storeValue)\n\n // External change → drop shadow + cancel pending debounce.\n // Track storeValue identity so the effect ONLY reacts to actual store\n // changes — not to local shadow updates while the user is typing.\n useEffect(() => {\n if (Object.is(lastStoreValueRef.current, storeValue)) return\n lastStoreValueRef.current = storeValue\n if (shadow !== null && shadow !== defaultSerialize(storeValue)) {\n debouncerRef.current?.cancel()\n setShadow(null)\n }\n }, [storeValue, shadow])\n\n // Lazy-create the debounce controller for this hook instance\n if (debounceMs > 0 && debouncerRef.current === null) {\n debouncerRef.current = createDebouncer(store, path, debounceMs, () => setShadow(null))\n }\n\n const onChange = useCallback(\n (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {\n const next = event.target.value\n if (debounceMs > 0) {\n setShadow(next)\n debouncerRef.current?.trigger(next)\n } else {\n store.set(path, next)\n }\n },\n [store, path, debounceMs],\n )\n\n const onBlur = useCallback(() => {\n debouncerRef.current?.flush()\n }, [])\n\n return {\n name: path,\n value: shadow ?? defaultSerialize(storeValue),\n onChange,\n onBlur,\n }\n}\n\nfunction createDebouncer(\n store: ParamsStore,\n path: string,\n ms: number,\n onCommit: () => void,\n): DebounceController {\n let timer: ReturnType<typeof setTimeout> | undefined\n let pendingValue: string | null = null\n\n const commit = () => {\n if (pendingValue !== null) {\n store.set(path, pendingValue)\n pendingValue = null\n onCommit()\n }\n }\n\n return {\n trigger: (value: string) => {\n pendingValue = value\n if (timer !== undefined) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = undefined\n commit()\n }, ms)\n },\n flush: () => {\n if (timer !== undefined) clearTimeout(timer)\n timer = undefined\n commit()\n },\n cancel: () => {\n if (timer !== undefined) clearTimeout(timer)\n timer = undefined\n pendingValue = null\n },\n }\n}\n","import { useEffect, useReducer } from 'react'\n\nimport type { ParamsStore } from '../params-store'\n\n/**\n * Subscribe to a single path on the store. Re-renders the calling component\n * only when that path's value changes. Sibling-path changes don't fire.\n */\nexport function useFieldValue<TValue = unknown>(store: ParamsStore, path: string): TValue {\n const [, forceRender] = useReducer((tick: number) => tick + 1, 0)\n useEffect(() => store.subscribe(path, forceRender), [store, path])\n return store.getValue(path) as TValue\n}\n","import { useCallback, useDeferredValue, useEffect, useReducer } from 'react'\n\nimport type { ParamsStore, SetOptions } from '../params-store'\nimport type { ParamsDefinition } from '../types'\nimport type { InputBindings } from './use-field-input'\nimport { useFieldInput } from './use-field-input'\nimport { useFieldValue } from './use-field-value'\nimport { useSharedStore } from './use-shared-store'\n\nexport interface ParamsController<T> {\n /** Snapshot of all values. Re-renders the calling component on any change. */\n readonly values: Readonly<Partial<T>>\n\n /** Hook — subscribes to a single path. Call at top level of render. */\n value<TValue = unknown>(path: string): TValue\n\n /** Hook — `value(path)` wrapped in `useDeferredValue`. */\n deferred<TValue = unknown>(path: string): TValue\n\n /** Hook — input bindings for `<input {...p.input('path')} />`. */\n input(path: string): InputBindings\n\n // Pass-through to the store (callable anywhere — not hooks)\n set: ParamsStore<T>['set']\n toggle: ParamsStore<T>['toggle']\n append: ParamsStore<T>['append']\n remove: ParamsStore<T>['remove']\n removeAt: ParamsStore<T>['removeAt']\n cycle: ParamsStore<T>['cycle']\n clear: ParamsStore<T>['clear']\n reset: ParamsStore<T>['reset']\n subscribe: ParamsStore<T>['subscribe']\n toQuery: ParamsStore<T>['toQuery']\n href: ParamsStore<T>['href']\n\n /** Unstable escape hatch — for devtools, advanced integrations. */\n readonly store: ParamsStore<T>\n}\n\n/**\n * Single-hook React API for `@victorylabs/params`. Returns a controller with\n * everything a component needs:\n *\n * ```tsx\n * function Filters() {\n * const p = useParams(filtersDef)\n * return (\n * <>\n * <input {...p.input('query')} />\n * <Pagination page={p.value('page')} onPage={(page) => p.set({ page })} />\n * <ResultList query={p.deferred('query')} />\n * </>\n * )\n * }\n * ```\n *\n * Cross-component sharing is automatic — multiple `useParams(def)` calls with\n * the same definition share the same store instance via the WeakMap cache.\n */\nexport function useParams<T extends Record<string, unknown> = Record<string, unknown>>(\n def: ParamsDefinition<T>,\n): ParamsController<T> {\n const store = useSharedStore(def) as ParamsStore<T>\n\n // Subscribe to root — re-renders this caller on any field change.\n // Granular subscriptions (avoid parent re-renders) are achieved via\n // `p.value(path)` / `p.input(path)` in CHILD components.\n const [, forceRender] = useReducer((tick: number) => tick + 1, 0)\n useEffect(() => store.subscribe('', forceRender), [store])\n\n // Stable bindings for store methods (delegated as-is). The overloaded `set`\n // needs an explicit branch so TypeScript can pick the right call signature.\n const set = useCallback(\n ((\n pathOrPartial: string | Partial<T>,\n valueOrOptions?: unknown,\n maybeOptions?: SetOptions,\n ): void => {\n if (typeof pathOrPartial === 'string') {\n store.set(pathOrPartial, valueOrOptions, maybeOptions)\n } else {\n store.set(pathOrPartial, valueOrOptions as SetOptions | undefined)\n }\n }) as ParamsStore<T>['set'],\n [store],\n )\n const toggle = useCallback<ParamsStore<T>['toggle']>(\n (path, options) => store.toggle(path, options),\n [store],\n )\n const append = useCallback<ParamsStore<T>['append']>(\n (path, value, options) => store.append(path, value, options),\n [store],\n )\n const removeFn = useCallback<ParamsStore<T>['remove']>(\n (path, value, options) => store.remove(path, value, options),\n [store],\n )\n const removeAt = useCallback<ParamsStore<T>['removeAt']>(\n (path, index, options) => store.removeAt(path, index, options),\n [store],\n )\n const cycle = useCallback(\n ((\n path: string,\n valuesOrOptions?: ReadonlyArray<unknown> | SetOptions,\n maybeOptions?: SetOptions,\n ): void => {\n if (Array.isArray(valuesOrOptions)) {\n store.cycle(path, valuesOrOptions, maybeOptions)\n } else if (valuesOrOptions !== undefined) {\n store.cycle(path, valuesOrOptions as SetOptions)\n } else {\n store.cycle(path)\n }\n }) as ParamsStore<T>['cycle'],\n [store],\n )\n const clear = useCallback<ParamsStore<T>['clear']>(\n (path, options) => store.clear(path, options),\n [store],\n )\n const reset = useCallback<ParamsStore<T>['reset']>(\n (values, options) => store.reset(values, options),\n [store],\n )\n const subscribe = useCallback<ParamsStore<T>['subscribe']>(\n (path, listener) => store.subscribe(path, listener),\n [store],\n )\n const toQuery = useCallback<ParamsStore<T>['toQuery']>(\n (overrides) => store.toQuery(overrides),\n [store],\n )\n const href = useCallback<ParamsStore<T>['href']>((overrides) => store.href(overrides), [store])\n\n // The hook-based methods need access to `store` via closure. Each call\n // (`p.value('x')`) invokes its hook in the consumer's render scope.\n const value = <TValue = unknown>(path: string): TValue => useFieldValue<TValue>(store, path)\n const deferred = <TValue = unknown>(path: string): TValue =>\n useDeferredValue(useFieldValue<TValue>(store, path))\n const input = (path: string): InputBindings => useFieldInput(store, path)\n\n return {\n values: store.getValues(),\n value,\n deferred,\n input,\n set,\n toggle,\n append,\n remove: removeFn,\n removeAt,\n cycle,\n clear,\n reset,\n subscribe,\n toQuery,\n href,\n store,\n }\n}\n","import { useEffect, useRef } from 'react'\n\nimport type { ParamsStore } from '../params-store'\nimport { acquire, release } from '../store-cache'\nimport type { ParamsDefinition } from '../types'\n\n/**\n * Internal hook: acquires the shared store on mount, releases on unmount.\n * Survives React 18 Strict Mode (microtask-deferred disposal cancels itself\n * when a fresh acquire arrives in the same tick).\n *\n * Multiple components using the same definition get the same store instance\n * — that's the cross-component-sharing primitive for params.\n */\nexport function useSharedStore<T>(def: ParamsDefinition<T>): ParamsStore<T> {\n const storeRef = useRef<ParamsStore<T> | null>(null)\n if (storeRef.current === null) {\n storeRef.current = acquire(def)\n }\n\n useEffect(\n () => () => {\n release(def)\n storeRef.current = null\n },\n [def],\n )\n\n return storeRef.current\n}\n"],"mappings":"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","names":["useEffect","useEffect","useCallback","useEffect","useReducer","useEffect","useRef","useRef","useEffect","useReducer","useEffect","useCallback","value"]}
1
+ {"version":3,"sources":["../src/react/use-debounced-value.ts","../src/react/use-field-input.tsx","../src/react/use-field-value.ts","../src/react/use-params.tsx","../src/react/use-shared-store.ts"],"sourcesContent":["import { useEffect, useState } from 'react'\n\n/**\n * Returns a copy of `value` that updates only after `ms` milliseconds of\n * stability. Rapid changes cancel pending updates so only the last one\n * settles. `ms <= 0` short-circuits to a synchronous pass-through.\n *\n * Pairs with `@victorylabs/params`' field-level `debounce:` config (which\n * lives at the URL/storage layer) for cases where a component wants to\n * debounce arbitrary local state — e.g., a search input that drives a\n * downstream query but isn't itself a params field.\n *\n * @example\n * ```tsx\n * function SearchBox() {\n * const [query, setQuery] = useState('')\n * const debounced = useDebouncedValue(query, 200)\n *\n * const { data } = api.queries.search({ params: { q: debounced } })\n * return <input value={query} onChange={(e) => setQuery(e.target.value)} />\n * }\n * ```\n */\nexport function useDebouncedValue<T>(value: T, ms: number): T {\n const [debounced, setDebounced] = useState(value)\n\n useEffect(() => {\n if (ms <= 0) {\n setDebounced(value)\n return\n }\n const id = setTimeout(() => setDebounced(value), ms)\n return () => clearTimeout(id)\n }, [value, ms])\n\n return debounced\n}\n","import type { ChangeEvent } from 'react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport type { ParamsStore } from '../params-store'\nimport { defaultSerialize } from '../schema'\nimport { useFieldValue } from './use-field-value'\n\nexport interface InputBindings {\n name: string\n value: string\n onChange: (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void\n onBlur: () => void\n}\n\ninterface DebounceController {\n trigger: (value: string) => void\n flush: () => void\n cancel: () => void\n}\n\n/**\n * Hook that returns input bindings for a single path. Handles:\n * - Reading the current store value (debounced shadow during typing)\n * - Per-field debounce from the field config\n * - Shadow cancellation when an external `set()` invalidates the pending value\n * - `onBlur` flushes any pending debounce immediately\n *\n * The bindings are spreadable directly: `<input {...p.input('query')} />`.\n */\nexport function useFieldInput(store: ParamsStore, path: string): InputBindings {\n const storeValue = useFieldValue<unknown>(store, path)\n const config = store.getFieldConfig(path)\n const debounceMs = config?.debounce ?? 0\n\n const [shadow, setShadow] = useState<string | null>(null)\n const debouncerRef = useRef<DebounceController | null>(null)\n const lastStoreValueRef = useRef(storeValue)\n\n // External change → drop shadow + cancel pending debounce.\n // Track storeValue identity so the effect ONLY reacts to actual store\n // changes — not to local shadow updates while the user is typing.\n useEffect(() => {\n if (Object.is(lastStoreValueRef.current, storeValue)) return\n lastStoreValueRef.current = storeValue\n if (shadow !== null && shadow !== defaultSerialize(storeValue)) {\n debouncerRef.current?.cancel()\n setShadow(null)\n }\n }, [storeValue, shadow])\n\n // Lazy-create the debounce controller for this hook instance\n if (debounceMs > 0 && debouncerRef.current === null) {\n debouncerRef.current = createDebouncer(store, path, debounceMs, () => setShadow(null))\n }\n\n const onChange = useCallback(\n (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {\n const next = event.target.value\n if (debounceMs > 0) {\n setShadow(next)\n debouncerRef.current?.trigger(next)\n } else {\n store.setField(path, next)\n }\n },\n [store, path, debounceMs],\n )\n\n const onBlur = useCallback(() => {\n debouncerRef.current?.flush()\n }, [])\n\n return {\n name: path,\n value: shadow ?? defaultSerialize(storeValue),\n onChange,\n onBlur,\n }\n}\n\nfunction createDebouncer(\n store: ParamsStore,\n path: string,\n ms: number,\n onCommit: () => void,\n): DebounceController {\n let timer: ReturnType<typeof setTimeout> | undefined\n let pendingValue: string | null = null\n\n const commit = () => {\n if (pendingValue !== null) {\n store.setField(path, pendingValue)\n pendingValue = null\n onCommit()\n }\n }\n\n return {\n trigger: (value: string) => {\n pendingValue = value\n if (timer !== undefined) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = undefined\n commit()\n }, ms)\n },\n flush: () => {\n if (timer !== undefined) clearTimeout(timer)\n timer = undefined\n commit()\n },\n cancel: () => {\n if (timer !== undefined) clearTimeout(timer)\n timer = undefined\n pendingValue = null\n },\n }\n}\n","import { useEffect, useReducer } from 'react'\n\nimport type { ParamsStore } from '../params-store'\n\n/**\n * Subscribe to a single path on the store. Re-renders the calling component\n * only when that path's value changes. Sibling-path changes don't fire.\n */\nexport function useFieldValue<TValue = unknown>(store: ParamsStore, path: string): TValue {\n const [, forceRender] = useReducer((tick: number) => tick + 1, 0)\n useEffect(() => store.subscribe(path, forceRender), [store, path])\n return store.getValue(path) as TValue\n}\n","import { useCallback, useDeferredValue, useEffect, useReducer } from 'react'\n\nimport type { ParamsStore, SetOptions } from '../params-store'\nimport type { ParamsDefinition } from '../types'\nimport type { InputBindings } from './use-field-input'\nimport { useFieldInput } from './use-field-input'\nimport { useFieldValue } from './use-field-value'\nimport { useSharedStore } from './use-shared-store'\n\nexport interface ParamsController<T> {\n /**\n * Snapshot of all values, with defaults hydrated. Re-renders the calling\n * component on any change.\n *\n * Typed as `Readonly<T>` (not `Readonly<Partial<T>>`) because every field\n * in a `ParamsDefinition` declares a default — either via the schema's\n * `.default()` or a plain spec's `default:` field — so the controller's\n * view is always complete. The underlying storage layer remains `Partial`\n * (URL/localStorage may be missing keys); the controller fills those in.\n */\n readonly values: Readonly<T>\n\n /** Hook — subscribes to a single path. Call at top level of render. */\n value<TValue = unknown>(path: string): TValue\n\n /** Hook — `value(path)` wrapped in `useDeferredValue`. */\n deferred<TValue = unknown>(path: string): TValue\n\n /** Hook — input bindings for `<input {...p.input('path')} />`. */\n input(path: string): InputBindings\n\n // Pass-through to the store (callable anywhere — not hooks)\n /**\n * Apply a partial update — write multiple fields at once.\n * For single-field writes use {@link setField}.\n */\n set: ParamsStore<T>['set']\n /** Write a single field by path. Replaces the old `set(path, value)` form. */\n setField: ParamsStore<T>['setField']\n toggle: ParamsStore<T>['toggle']\n append: ParamsStore<T>['append']\n remove: ParamsStore<T>['remove']\n removeAt: ParamsStore<T>['removeAt']\n cycle: ParamsStore<T>['cycle']\n clear: ParamsStore<T>['clear']\n reset: ParamsStore<T>['reset']\n subscribe: ParamsStore<T>['subscribe']\n toQuery: ParamsStore<T>['toQuery']\n href: ParamsStore<T>['href']\n\n /** Unstable escape hatch — for devtools, advanced integrations. */\n readonly store: ParamsStore<T>\n}\n\n/**\n * Single-hook React API for `@victorylabs/params`. Returns a controller with\n * everything a component needs:\n *\n * ```tsx\n * function Filters() {\n * const p = useParams(filtersDef)\n * return (\n * <>\n * <input {...p.input('query')} />\n * <Pagination page={p.value('page')} onPage={(page) => p.set({ page })} />\n * <ResultList query={p.deferred('query')} />\n * </>\n * )\n * }\n * ```\n *\n * Cross-component sharing is automatic — multiple `useParams(def)` calls with\n * the same definition share the same store instance via the WeakMap cache.\n */\nexport function useParams<T extends Record<string, unknown> = Record<string, unknown>>(\n def: ParamsDefinition<T>,\n): ParamsController<T> {\n const store = useSharedStore(def) as ParamsStore<T>\n\n // Subscribe to root — re-renders this caller on any field change.\n // Granular subscriptions (avoid parent re-renders) are achieved via\n // `p.value(path)` / `p.input(path)` in CHILD components.\n const [, forceRender] = useReducer((tick: number) => tick + 1, 0)\n useEffect(() => store.subscribe('', forceRender), [store])\n\n // Stable bindings for store methods (delegated as-is).\n const set = useCallback<ParamsStore<T>['set']>(\n (partial, options) => store.set(partial, options),\n [store],\n )\n const setField = useCallback<ParamsStore<T>['setField']>(\n (path, value, options) => store.setField(path, value, options),\n [store],\n )\n const toggle = useCallback<ParamsStore<T>['toggle']>(\n (path, options) => store.toggle(path, options),\n [store],\n )\n const append = useCallback<ParamsStore<T>['append']>(\n (path, value, options) => store.append(path, value, options),\n [store],\n )\n const removeFn = useCallback<ParamsStore<T>['remove']>(\n (path, value, options) => store.remove(path, value, options),\n [store],\n )\n const removeAt = useCallback<ParamsStore<T>['removeAt']>(\n (path, index, options) => store.removeAt(path, index, options),\n [store],\n )\n const cycle = useCallback(\n ((\n path: string,\n valuesOrOptions?: ReadonlyArray<unknown> | SetOptions,\n maybeOptions?: SetOptions,\n ): void => {\n if (Array.isArray(valuesOrOptions)) {\n store.cycle(path, valuesOrOptions, maybeOptions)\n } else if (valuesOrOptions !== undefined) {\n store.cycle(path, valuesOrOptions as SetOptions)\n } else {\n store.cycle(path)\n }\n }) as ParamsStore<T>['cycle'],\n [store],\n )\n const clear = useCallback<ParamsStore<T>['clear']>(\n (path, options) => store.clear(path, options),\n [store],\n )\n const reset = useCallback<ParamsStore<T>['reset']>(\n (values, options) => store.reset(values, options),\n [store],\n )\n const subscribe = useCallback<ParamsStore<T>['subscribe']>(\n (path, listener) => store.subscribe(path, listener),\n [store],\n )\n const toQuery = useCallback<ParamsStore<T>['toQuery']>(\n (overrides) => store.toQuery(overrides),\n [store],\n )\n const href = useCallback<ParamsStore<T>['href']>((overrides) => store.href(overrides), [store])\n\n // The hook-based methods need access to `store` via closure. Each call\n // (`p.value('x')`) invokes its hook in the consumer's render scope.\n const value = <TValue = unknown>(path: string): TValue => useFieldValue<TValue>(store, path)\n const deferred = <TValue = unknown>(path: string): TValue =>\n useDeferredValue(useFieldValue<TValue>(store, path))\n const input = (path: string): InputBindings => useFieldInput(store, path)\n\n return {\n // Cast: store.getValues() returns Partial<T> for the storage layer's\n // sake, but every field in ParamsDefinition declares a default so the\n // hydrated view is structurally complete. See ParamsController.values.\n values: store.getValues() as Readonly<T>,\n value,\n deferred,\n input,\n set,\n setField,\n toggle,\n append,\n remove: removeFn,\n removeAt,\n cycle,\n clear,\n reset,\n subscribe,\n toQuery,\n href,\n store,\n }\n}\n","import { useEffect, useRef } from 'react'\n\nimport type { ParamsStore } from '../params-store'\nimport { acquire, release } from '../store-cache'\nimport type { ParamsDefinition } from '../types'\n\n/**\n * Internal hook: acquires the shared store on mount, releases on unmount.\n * Survives React 18 Strict Mode (microtask-deferred disposal cancels itself\n * when a fresh acquire arrives in the same tick).\n *\n * Multiple components using the same definition get the same store instance\n * — that's the cross-component-sharing primitive for params.\n */\nexport function useSharedStore<T>(def: ParamsDefinition<T>): ParamsStore<T> {\n const storeRef = useRef<ParamsStore<T> | null>(null)\n if (storeRef.current === null) {\n storeRef.current = acquire(def)\n }\n\n useEffect(\n () => () => {\n release(def)\n storeRef.current = null\n },\n [def],\n )\n\n return storeRef.current\n}\n"],"mappings":"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","names":["useEffect","useState","useEffect","useState","useEffect","useCallback","useEffect","useReducer","useEffect","useRef","useRef","useEffect","useReducer","useEffect","useCallback","value"]}
@@ -29,23 +29,41 @@ function idbStorage(opts) {
29
29
  const storeName = opts.store ?? "params";
30
30
  const recordKey = opts.key ?? "default";
31
31
  const version = opts.version ?? 1;
32
+ const broadcastEnabled = opts.broadcast === true;
33
+ const onUpgrade = opts.onUpgrade;
34
+ const channelName = `@victorylabs/params:${dbName}:${storeName}:${recordKey}`;
32
35
  let dbHandle;
33
36
  const openDb = async () => {
34
37
  if (!isClient) throw new Error("idbStorage: window.indexedDB unavailable");
35
38
  if (dbHandle && dbHandle.version === version) return dbHandle;
36
39
  return new Promise((resolve, reject) => {
37
40
  const req = window.indexedDB.open(dbName, version);
38
- req.onupgradeneeded = () => {
41
+ req.onupgradeneeded = (event) => {
39
42
  const db = req.result;
40
43
  if (!db.objectStoreNames.contains(storeName)) {
41
44
  db.createObjectStore(storeName);
42
45
  }
46
+ if (onUpgrade && req.transaction) {
47
+ onUpgrade({
48
+ db,
49
+ transaction: req.transaction,
50
+ oldVersion: event.oldVersion,
51
+ newVersion: event.newVersion ?? version
52
+ });
53
+ }
43
54
  };
44
55
  req.onsuccess = () => {
45
56
  dbHandle = req.result;
46
57
  dbHandle.onclose = () => {
47
58
  dbHandle = void 0;
48
59
  };
60
+ dbHandle.onversionchange = () => {
61
+ try {
62
+ dbHandle?.close();
63
+ } catch {
64
+ }
65
+ dbHandle = void 0;
66
+ };
49
67
  resolve(req.result);
50
68
  };
51
69
  req.onerror = () => reject(req.error);
@@ -65,7 +83,30 @@ function idbStorage(opts) {
65
83
  return op(await openDb());
66
84
  }
67
85
  };
68
- return {
86
+ const broadcastSupported = () => broadcastEnabled && typeof BroadcastChannel !== "undefined";
87
+ let sharedChannel;
88
+ const subscribers = /* @__PURE__ */ new Set();
89
+ const ensureChannel = () => {
90
+ if (!broadcastSupported()) return void 0;
91
+ if (sharedChannel === void 0) {
92
+ sharedChannel = new BroadcastChannel(channelName);
93
+ sharedChannel.onmessage = (event) => {
94
+ const data = event.data;
95
+ const values = data?.values ?? {};
96
+ for (const cb of [...subscribers]) cb(values);
97
+ };
98
+ }
99
+ return sharedChannel;
100
+ };
101
+ const broadcast = (values) => {
102
+ const ch = ensureChannel();
103
+ if (ch === void 0) return;
104
+ try {
105
+ ch.postMessage({ values });
106
+ } catch {
107
+ }
108
+ };
109
+ const storage = {
69
110
  name: "idbStorage",
70
111
  clientOnly: true,
71
112
  // IDB has no synchronous API. Engine consults `readAsync` for hydration;
@@ -97,6 +138,7 @@ function idbStorage(opts) {
97
138
  req.onerror = () => reject(req.error);
98
139
  })
99
140
  );
141
+ broadcast(values);
100
142
  } catch {
101
143
  }
102
144
  },
@@ -112,6 +154,7 @@ function idbStorage(opts) {
112
154
  req.onerror = () => reject(req.error);
113
155
  })
114
156
  );
157
+ broadcast(void 0);
115
158
  return;
116
159
  }
117
160
  const current = await withVersionRetry(
@@ -131,11 +174,21 @@ function idbStorage(opts) {
131
174
  req.onerror = () => reject(req.error);
132
175
  })
133
176
  );
177
+ broadcast(current);
134
178
  } catch {
135
179
  }
136
180
  }
137
- // No `subscribe` — IDB doesn't fire native change events. v0.6+ via BroadcastChannel.
138
181
  };
182
+ if (broadcastSupported()) {
183
+ storage.subscribe = (callback) => {
184
+ ensureChannel();
185
+ subscribers.add(callback);
186
+ return () => {
187
+ subscribers.delete(callback);
188
+ };
189
+ };
190
+ }
191
+ return storage;
139
192
  }
140
193
  // Annotate the CommonJS export names for ESM import in node:
141
194
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/storage/idb/index.ts"],"sourcesContent":["import type { ParamsStorage } from '../../storage'\n\nexport interface IdbStorageOptions {\n /** Database name. Required. */\n readonly db: string\n /** Object store name within the database. Default: `'params'`. */\n readonly store?: string\n /** Record key within the store. Default: `'default'`. */\n readonly key?: string\n /** Schema version. Bump if you change the value shape across releases. Default: `1`. */\n readonly version?: number\n}\n\nconst isClient = typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined'\n\n/**\n * IndexedDB storage backend.\n *\n * Async-only — `read()` returns `undefined` synchronously; the engine should\n * consult `readAsync()` for hydration. Single record per definition (the\n * `key` option), stored as a structured-clonable object — no JSON\n * serialization layer.\n *\n * v0.5 limitations (likely v0.6+ candidates if a consumer asks):\n * - **No cross-tab sync** (no BroadcastChannel). The same database is shared\n * across tabs, but writes from one tab don't push to another. Subscribe is\n * intentionally absent.\n * - **No consumer-supplied `onupgradeneeded` hook.** The `version` option\n * triggers an upgrade transaction and the lib creates the object store on\n * first open, but you can't migrate from a previous shape.\n */\nexport function idbStorage<T = Record<string, unknown>>(opts: IdbStorageOptions): ParamsStorage<T> {\n const dbName = opts.db\n const storeName = opts.store ?? 'params'\n const recordKey = opts.key ?? 'default'\n const version = opts.version ?? 1\n\n // Cache the database handle. Re-opens on demand if closed.\n let dbHandle: IDBDatabase | undefined\n const openDb = async (): Promise<IDBDatabase> => {\n if (!isClient) throw new Error('idbStorage: window.indexedDB unavailable')\n if (dbHandle && dbHandle.version === version) return dbHandle\n return new Promise((resolve, reject) => {\n const req = window.indexedDB.open(dbName, version)\n req.onupgradeneeded = () => {\n const db = req.result\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName)\n }\n }\n req.onsuccess = () => {\n dbHandle = req.result\n dbHandle.onclose = () => {\n dbHandle = undefined\n }\n resolve(req.result)\n }\n req.onerror = () => reject(req.error)\n })\n }\n\n /**\n * v0.5: retry once on `VersionError`. Another tab opening the DB at a\n * different version invalidates our cached handle. Close it, re-open, and\n * retry the operation. After one retry, fall back to silent (existing\n * contract).\n */\n const withVersionRetry = async <R>(op: (db: IDBDatabase) => Promise<R>): Promise<R> => {\n try {\n return await op(await openDb())\n } catch (err) {\n const isVersionError =\n typeof DOMException !== 'undefined' &&\n err instanceof DOMException &&\n err.name === 'VersionError'\n if (!isVersionError) throw err\n try {\n dbHandle?.close()\n } catch {\n // ignore — handle may already be invalid\n }\n dbHandle = undefined\n return op(await openDb())\n }\n }\n\n return {\n name: 'idbStorage',\n clientOnly: true,\n\n // IDB has no synchronous API. Engine consults `readAsync` for hydration;\n // `read()` always returns undefined.\n read: () => undefined,\n\n readAsync: async () => {\n if (!isClient) return undefined\n try {\n return await withVersionRetry(\n (db) =>\n new Promise<Partial<T> | undefined>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readonly')\n const req = tx.objectStore(storeName).get(recordKey)\n req.onsuccess = () => resolve((req.result as Partial<T> | undefined) ?? undefined)\n req.onerror = () => reject(req.error)\n }),\n )\n } catch {\n return undefined\n }\n },\n\n write: async (values, _changed, _options) => {\n if (!isClient) return\n try {\n await withVersionRetry(\n (db) =>\n new Promise<void>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const req = tx.objectStore(storeName).put(values, recordKey)\n req.onsuccess = () => resolve()\n req.onerror = () => reject(req.error)\n }),\n )\n } catch {\n // Silent fallback per the storage error contract — values still live in memory.\n }\n },\n\n clear: async (paths, _options) => {\n if (!isClient) return\n try {\n if (paths.length === 0) {\n // Clear everything — delete the record.\n await withVersionRetry(\n (db) =>\n new Promise<void>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const req = tx.objectStore(storeName).delete(recordKey)\n req.onsuccess = () => resolve()\n req.onerror = () => reject(req.error)\n }),\n )\n return\n }\n // Per-path clear: read current, omit cleared paths, write back.\n const current = await withVersionRetry(\n (db) =>\n new Promise<Record<string, unknown>>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readonly')\n const req = tx.objectStore(storeName).get(recordKey)\n req.onsuccess = () =>\n resolve((req.result as Record<string, unknown> | undefined) ?? {})\n req.onerror = () => reject(req.error)\n }),\n )\n for (const p of paths) delete current[p]\n await withVersionRetry(\n (db) =>\n new Promise<void>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const req = tx.objectStore(storeName).put(current, recordKey)\n req.onsuccess = () => resolve()\n req.onerror = () => reject(req.error)\n }),\n )\n } catch {\n // Silent fallback.\n }\n },\n\n // No `subscribe` — IDB doesn't fire native change events. v0.6+ via BroadcastChannel.\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/storage/idb/index.ts"],"sourcesContent":["import type { ParamsStorage } from '../../storage'\n\n/**\n * Consumer-supplied schema-migration callback. Invoked inside\n * `onupgradeneeded` after the lib ensures the configured object store exists,\n * so consumers don't need to defensively check for it. The upgrade\n * `transaction` is provided so consumers can iterate cursors against the\n * existing store synchronously to migrate record shapes.\n *\n * Errors thrown here abort the upgrade — the open request fails and\n * `idbStorage`'s silent-fallback contract returns `undefined` to the engine.\n */\nexport type IdbUpgradeCallback = (ctx: {\n readonly db: IDBDatabase\n readonly transaction: IDBTransaction\n readonly oldVersion: number\n readonly newVersion: number\n}) => void\n\nexport interface IdbStorageOptions {\n /** Database name. Required. */\n readonly db: string\n /** Object store name within the database. Default: `'params'`. */\n readonly store?: string\n /** Record key within the store. Default: `'default'`. */\n readonly key?: string\n /** Schema version. Bump if you change the value shape across releases. Default: `1`. */\n readonly version?: number\n /**\n * Cross-tab sync via {@link BroadcastChannel}. When `true`, every successful\n * write broadcasts the new values to other tabs subscribed to the same\n * `(db, store, key)` triple; the receiving tab feeds them back through the\n * `subscribe` callback exactly as it would for a `popstate` or `storage`\n * event. Default: `false` — preserves the v0.5 \"no subscribe\" behavior so\n * existing consumers see no change.\n *\n * Channel name is derived from `(dbName, storeName, recordKey)`. SSR-safe:\n * skips registration when `BroadcastChannel` is unavailable.\n */\n readonly broadcast?: boolean\n /**\n * Schema-migration callback. See {@link IdbUpgradeCallback}. Most consumers\n * don't need this — the lib creates the object store on first open. Reach\n * for it only when you bump `version` and need to add indexes or migrate\n * record shapes.\n */\n readonly onUpgrade?: IdbUpgradeCallback\n}\n\nconst isClient = typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined'\n\n/**\n * IndexedDB storage backend.\n *\n * Async-only — `read()` returns `undefined` synchronously; the engine should\n * consult `readAsync()` for hydration. Single record per definition (the\n * `key` option), stored as a structured-clonable object — no JSON\n * serialization layer.\n *\n * v0.6 capabilities (opt-in, both backward-compatible):\n * - **`broadcast: true`** for cross-tab sync via `BroadcastChannel`.\n * - **`onUpgrade`** for consumer-supplied schema migrations across `version`\n * bumps (add indexes, transform records).\n */\nexport function idbStorage<T = Record<string, unknown>>(opts: IdbStorageOptions): ParamsStorage<T> {\n const dbName = opts.db\n const storeName = opts.store ?? 'params'\n const recordKey = opts.key ?? 'default'\n const version = opts.version ?? 1\n const broadcastEnabled = opts.broadcast === true\n const onUpgrade = opts.onUpgrade\n const channelName = `@victorylabs/params:${dbName}:${storeName}:${recordKey}`\n\n // Cache the database handle. Re-opens on demand if closed.\n let dbHandle: IDBDatabase | undefined\n const openDb = async (): Promise<IDBDatabase> => {\n if (!isClient) throw new Error('idbStorage: window.indexedDB unavailable')\n if (dbHandle && dbHandle.version === version) return dbHandle\n return new Promise((resolve, reject) => {\n const req = window.indexedDB.open(dbName, version)\n req.onupgradeneeded = (event) => {\n const db = req.result\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName)\n }\n if (onUpgrade && req.transaction) {\n onUpgrade({\n db,\n transaction: req.transaction,\n oldVersion: event.oldVersion,\n newVersion: event.newVersion ?? version,\n })\n }\n }\n req.onsuccess = () => {\n dbHandle = req.result\n dbHandle.onclose = () => {\n dbHandle = undefined\n }\n // When another tab opens the same DB at a higher version, the browser\n // fires `versionchange` against this handle. If we don't close it,\n // the other tab's upgrade blocks indefinitely. Close → fall through\n // to the `withVersionRetry` path on the next op.\n dbHandle.onversionchange = () => {\n try {\n dbHandle?.close()\n } catch {\n // ignore\n }\n dbHandle = undefined\n }\n resolve(req.result)\n }\n req.onerror = () => reject(req.error)\n })\n }\n\n /**\n * v0.5: retry once on `VersionError`. Another tab opening the DB at a\n * different version invalidates our cached handle. Close it, re-open, and\n * retry the operation. After one retry, fall back to silent (existing\n * contract).\n */\n const withVersionRetry = async <R>(op: (db: IDBDatabase) => Promise<R>): Promise<R> => {\n try {\n return await op(await openDb())\n } catch (err) {\n const isVersionError =\n typeof DOMException !== 'undefined' &&\n err instanceof DOMException &&\n err.name === 'VersionError'\n if (!isVersionError) throw err\n try {\n dbHandle?.close()\n } catch {\n // ignore — handle may already be invalid\n }\n dbHandle = undefined\n return op(await openDb())\n }\n }\n\n // ── Cross-tab broadcast (opt-in via `broadcast: true`) ────────────────\n // Per the BroadcastChannel spec, posting a message on a channel does NOT\n // deliver it back to that same channel instance — but it DOES deliver it\n // to other BroadcastChannel instances with the same name in the same agent\n // and any other agent. We exploit that by sharing ONE channel per storage\n // instance for both posting and receiving: local writes never re-trigger\n // our own subscribers; cross-tab writes do.\n const broadcastSupported = (): boolean =>\n broadcastEnabled && typeof BroadcastChannel !== 'undefined'\n let sharedChannel: BroadcastChannel | undefined\n const subscribers = new Set<(values: Partial<T>) => void>()\n const ensureChannel = (): BroadcastChannel | undefined => {\n if (!broadcastSupported()) return undefined\n if (sharedChannel === undefined) {\n sharedChannel = new BroadcastChannel(channelName)\n sharedChannel.onmessage = (event) => {\n const data = event.data as { values?: Partial<T> } | null | undefined\n const values = data?.values ?? ({} as Partial<T>)\n for (const cb of [...subscribers]) cb(values)\n }\n }\n return sharedChannel\n }\n const broadcast = (values: Partial<T> | undefined): void => {\n const ch = ensureChannel()\n if (ch === undefined) return\n try {\n ch.postMessage({ values })\n } catch {\n // BroadcastChannel can throw if the message isn't structured-cloneable;\n // silent per the storage error contract.\n }\n }\n\n const storage: ParamsStorage<T> = {\n name: 'idbStorage',\n clientOnly: true,\n\n // IDB has no synchronous API. Engine consults `readAsync` for hydration;\n // `read()` always returns undefined.\n read: () => undefined,\n\n readAsync: async () => {\n if (!isClient) return undefined\n try {\n return await withVersionRetry(\n (db) =>\n new Promise<Partial<T> | undefined>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readonly')\n const req = tx.objectStore(storeName).get(recordKey)\n req.onsuccess = () => resolve((req.result as Partial<T> | undefined) ?? undefined)\n req.onerror = () => reject(req.error)\n }),\n )\n } catch {\n return undefined\n }\n },\n\n write: async (values, _changed, _options) => {\n if (!isClient) return\n try {\n await withVersionRetry(\n (db) =>\n new Promise<void>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const req = tx.objectStore(storeName).put(values, recordKey)\n req.onsuccess = () => resolve()\n req.onerror = () => reject(req.error)\n }),\n )\n broadcast(values)\n } catch {\n // Silent fallback per the storage error contract — values still live in memory.\n }\n },\n\n clear: async (paths, _options) => {\n if (!isClient) return\n try {\n if (paths.length === 0) {\n // Clear everything — delete the record.\n await withVersionRetry(\n (db) =>\n new Promise<void>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const req = tx.objectStore(storeName).delete(recordKey)\n req.onsuccess = () => resolve()\n req.onerror = () => reject(req.error)\n }),\n )\n broadcast(undefined)\n return\n }\n // Per-path clear: read current, omit cleared paths, write back.\n const current = await withVersionRetry(\n (db) =>\n new Promise<Record<string, unknown>>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readonly')\n const req = tx.objectStore(storeName).get(recordKey)\n req.onsuccess = () =>\n resolve((req.result as Record<string, unknown> | undefined) ?? {})\n req.onerror = () => reject(req.error)\n }),\n )\n for (const p of paths) delete current[p]\n await withVersionRetry(\n (db) =>\n new Promise<void>((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const req = tx.objectStore(storeName).put(current, recordKey)\n req.onsuccess = () => resolve()\n req.onerror = () => reject(req.error)\n }),\n )\n broadcast(current as Partial<T>)\n } catch {\n // Silent fallback.\n }\n },\n }\n\n if (broadcastSupported()) {\n storage.subscribe = (callback) => {\n ensureChannel()\n subscribers.add(callback)\n return () => {\n subscribers.delete(callback)\n // Keep the shared channel open for the lifetime of the storage\n // instance — the next write needs it for outbound posts, even after\n // the last subscribe teardown.\n }\n }\n }\n\n return storage\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiDA,IAAM,WAAW,OAAO,WAAW,eAAe,OAAO,OAAO,cAAc;AAevE,SAAS,WAAwC,MAA2C;AACjG,QAAM,SAAS,KAAK;AACpB,QAAM,YAAY,KAAK,SAAS;AAChC,QAAM,YAAY,KAAK,OAAO;AAC9B,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,mBAAmB,KAAK,cAAc;AAC5C,QAAM,YAAY,KAAK;AACvB,QAAM,cAAc,uBAAuB,MAAM,IAAI,SAAS,IAAI,SAAS;AAG3E,MAAI;AACJ,QAAM,SAAS,YAAkC;AAC/C,QAAI,CAAC,SAAU,OAAM,IAAI,MAAM,0CAA0C;AACzE,QAAI,YAAY,SAAS,YAAY,QAAS,QAAO;AACrD,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,MAAM,OAAO,UAAU,KAAK,QAAQ,OAAO;AACjD,UAAI,kBAAkB,CAAC,UAAU;AAC/B,cAAM,KAAK,IAAI;AACf,YAAI,CAAC,GAAG,iBAAiB,SAAS,SAAS,GAAG;AAC5C,aAAG,kBAAkB,SAAS;AAAA,QAChC;AACA,YAAI,aAAa,IAAI,aAAa;AAChC,oBAAU;AAAA,YACR;AAAA,YACA,aAAa,IAAI;AAAA,YACjB,YAAY,MAAM;AAAA,YAClB,YAAY,MAAM,cAAc;AAAA,UAClC,CAAC;AAAA,QACH;AAAA,MACF;AACA,UAAI,YAAY,MAAM;AACpB,mBAAW,IAAI;AACf,iBAAS,UAAU,MAAM;AACvB,qBAAW;AAAA,QACb;AAKA,iBAAS,kBAAkB,MAAM;AAC/B,cAAI;AACF,sBAAU,MAAM;AAAA,UAClB,QAAQ;AAAA,UAER;AACA,qBAAW;AAAA,QACb;AACA,gBAAQ,IAAI,MAAM;AAAA,MACpB;AACA,UAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,IACtC,CAAC;AAAA,EACH;AAQA,QAAM,mBAAmB,OAAU,OAAoD;AACrF,QAAI;AACF,aAAO,MAAM,GAAG,MAAM,OAAO,CAAC;AAAA,IAChC,SAAS,KAAK;AACZ,YAAM,iBACJ,OAAO,iBAAiB,eACxB,eAAe,gBACf,IAAI,SAAS;AACf,UAAI,CAAC,eAAgB,OAAM;AAC3B,UAAI;AACF,kBAAU,MAAM;AAAA,MAClB,QAAQ;AAAA,MAER;AACA,iBAAW;AACX,aAAO,GAAG,MAAM,OAAO,CAAC;AAAA,IAC1B;AAAA,EACF;AASA,QAAM,qBAAqB,MACzB,oBAAoB,OAAO,qBAAqB;AAClD,MAAI;AACJ,QAAM,cAAc,oBAAI,IAAkC;AAC1D,QAAM,gBAAgB,MAAoC;AACxD,QAAI,CAAC,mBAAmB,EAAG,QAAO;AAClC,QAAI,kBAAkB,QAAW;AAC/B,sBAAgB,IAAI,iBAAiB,WAAW;AAChD,oBAAc,YAAY,CAAC,UAAU;AACnC,cAAM,OAAO,MAAM;AACnB,cAAM,SAAS,MAAM,UAAW,CAAC;AACjC,mBAAW,MAAM,CAAC,GAAG,WAAW,EAAG,IAAG,MAAM;AAAA,MAC9C;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,QAAM,YAAY,CAAC,WAAyC;AAC1D,UAAM,KAAK,cAAc;AACzB,QAAI,OAAO,OAAW;AACtB,QAAI;AACF,SAAG,YAAY,EAAE,OAAO,CAAC;AAAA,IAC3B,QAAQ;AAAA,IAGR;AAAA,EACF;AAEA,QAAM,UAA4B;AAAA,IAChC,MAAM;AAAA,IACN,YAAY;AAAA;AAAA;AAAA,IAIZ,MAAM,MAAM;AAAA,IAEZ,WAAW,YAAY;AACrB,UAAI,CAAC,SAAU,QAAO;AACtB,UAAI;AACF,eAAO,MAAM;AAAA,UACX,CAAC,OACC,IAAI,QAAgC,CAAC,SAAS,WAAW;AACvD,kBAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,kBAAM,MAAM,GAAG,YAAY,SAAS,EAAE,IAAI,SAAS;AACnD,gBAAI,YAAY,MAAM,QAAS,IAAI,UAAqC,MAAS;AACjF,gBAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,UACtC,CAAC;AAAA,QACL;AAAA,MACF,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,OAAO,OAAO,QAAQ,UAAU,aAAa;AAC3C,UAAI,CAAC,SAAU;AACf,UAAI;AACF,cAAM;AAAA,UACJ,CAAC,OACC,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,kBAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,kBAAM,MAAM,GAAG,YAAY,SAAS,EAAE,IAAI,QAAQ,SAAS;AAC3D,gBAAI,YAAY,MAAM,QAAQ;AAC9B,gBAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,UACtC,CAAC;AAAA,QACL;AACA,kBAAU,MAAM;AAAA,MAClB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,OAAO,OAAO,OAAO,aAAa;AAChC,UAAI,CAAC,SAAU;AACf,UAAI;AACF,YAAI,MAAM,WAAW,GAAG;AAEtB,gBAAM;AAAA,YACJ,CAAC,OACC,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,oBAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,oBAAM,MAAM,GAAG,YAAY,SAAS,EAAE,OAAO,SAAS;AACtD,kBAAI,YAAY,MAAM,QAAQ;AAC9B,kBAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,YACtC,CAAC;AAAA,UACL;AACA,oBAAU,MAAS;AACnB;AAAA,QACF;AAEA,cAAM,UAAU,MAAM;AAAA,UACpB,CAAC,OACC,IAAI,QAAiC,CAAC,SAAS,WAAW;AACxD,kBAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,kBAAM,MAAM,GAAG,YAAY,SAAS,EAAE,IAAI,SAAS;AACnD,gBAAI,YAAY,MACd,QAAS,IAAI,UAAkD,CAAC,CAAC;AACnE,gBAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,UACtC,CAAC;AAAA,QACL;AACA,mBAAW,KAAK,MAAO,QAAO,QAAQ,CAAC;AACvC,cAAM;AAAA,UACJ,CAAC,OACC,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,kBAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,kBAAM,MAAM,GAAG,YAAY,SAAS,EAAE,IAAI,SAAS,SAAS;AAC5D,gBAAI,YAAY,MAAM,QAAQ;AAC9B,gBAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,UACtC,CAAC;AAAA,QACL;AACA,kBAAU,OAAqB;AAAA,MACjC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,MAAI,mBAAmB,GAAG;AACxB,YAAQ,YAAY,CAAC,aAAa;AAChC,oBAAc;AACd,kBAAY,IAAI,QAAQ;AACxB,aAAO,MAAM;AACX,oBAAY,OAAO,QAAQ;AAAA,MAI7B;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -1,5 +1,21 @@
1
1
  import { P as ParamsStorage } from '../storage-DBLIRR-4.cjs';
2
2
 
3
+ /**
4
+ * Consumer-supplied schema-migration callback. Invoked inside
5
+ * `onupgradeneeded` after the lib ensures the configured object store exists,
6
+ * so consumers don't need to defensively check for it. The upgrade
7
+ * `transaction` is provided so consumers can iterate cursors against the
8
+ * existing store synchronously to migrate record shapes.
9
+ *
10
+ * Errors thrown here abort the upgrade — the open request fails and
11
+ * `idbStorage`'s silent-fallback contract returns `undefined` to the engine.
12
+ */
13
+ type IdbUpgradeCallback = (ctx: {
14
+ readonly db: IDBDatabase;
15
+ readonly transaction: IDBTransaction;
16
+ readonly oldVersion: number;
17
+ readonly newVersion: number;
18
+ }) => void;
3
19
  interface IdbStorageOptions {
4
20
  /** Database name. Required. */
5
21
  readonly db: string;
@@ -9,6 +25,25 @@ interface IdbStorageOptions {
9
25
  readonly key?: string;
10
26
  /** Schema version. Bump if you change the value shape across releases. Default: `1`. */
11
27
  readonly version?: number;
28
+ /**
29
+ * Cross-tab sync via {@link BroadcastChannel}. When `true`, every successful
30
+ * write broadcasts the new values to other tabs subscribed to the same
31
+ * `(db, store, key)` triple; the receiving tab feeds them back through the
32
+ * `subscribe` callback exactly as it would for a `popstate` or `storage`
33
+ * event. Default: `false` — preserves the v0.5 "no subscribe" behavior so
34
+ * existing consumers see no change.
35
+ *
36
+ * Channel name is derived from `(dbName, storeName, recordKey)`. SSR-safe:
37
+ * skips registration when `BroadcastChannel` is unavailable.
38
+ */
39
+ readonly broadcast?: boolean;
40
+ /**
41
+ * Schema-migration callback. See {@link IdbUpgradeCallback}. Most consumers
42
+ * don't need this — the lib creates the object store on first open. Reach
43
+ * for it only when you bump `version` and need to add indexes or migrate
44
+ * record shapes.
45
+ */
46
+ readonly onUpgrade?: IdbUpgradeCallback;
12
47
  }
13
48
  /**
14
49
  * IndexedDB storage backend.
@@ -18,14 +53,11 @@ interface IdbStorageOptions {
18
53
  * `key` option), stored as a structured-clonable object — no JSON
19
54
  * serialization layer.
20
55
  *
21
- * v0.5 limitations (likely v0.6+ candidates if a consumer asks):
22
- * - **No cross-tab sync** (no BroadcastChannel). The same database is shared
23
- * across tabs, but writes from one tab don't push to another. Subscribe is
24
- * intentionally absent.
25
- * - **No consumer-supplied `onupgradeneeded` hook.** The `version` option
26
- * triggers an upgrade transaction and the lib creates the object store on
27
- * first open, but you can't migrate from a previous shape.
56
+ * v0.6 capabilities (opt-in, both backward-compatible):
57
+ * - **`broadcast: true`** for cross-tab sync via `BroadcastChannel`.
58
+ * - **`onUpgrade`** for consumer-supplied schema migrations across `version`
59
+ * bumps (add indexes, transform records).
28
60
  */
29
61
  declare function idbStorage<T = Record<string, unknown>>(opts: IdbStorageOptions): ParamsStorage<T>;
30
62
 
31
- export { type IdbStorageOptions, idbStorage };
63
+ export { type IdbStorageOptions, type IdbUpgradeCallback, idbStorage };
@@ -1,5 +1,21 @@
1
1
  import { P as ParamsStorage } from '../storage-DBLIRR-4.js';
2
2
 
3
+ /**
4
+ * Consumer-supplied schema-migration callback. Invoked inside
5
+ * `onupgradeneeded` after the lib ensures the configured object store exists,
6
+ * so consumers don't need to defensively check for it. The upgrade
7
+ * `transaction` is provided so consumers can iterate cursors against the
8
+ * existing store synchronously to migrate record shapes.
9
+ *
10
+ * Errors thrown here abort the upgrade — the open request fails and
11
+ * `idbStorage`'s silent-fallback contract returns `undefined` to the engine.
12
+ */
13
+ type IdbUpgradeCallback = (ctx: {
14
+ readonly db: IDBDatabase;
15
+ readonly transaction: IDBTransaction;
16
+ readonly oldVersion: number;
17
+ readonly newVersion: number;
18
+ }) => void;
3
19
  interface IdbStorageOptions {
4
20
  /** Database name. Required. */
5
21
  readonly db: string;
@@ -9,6 +25,25 @@ interface IdbStorageOptions {
9
25
  readonly key?: string;
10
26
  /** Schema version. Bump if you change the value shape across releases. Default: `1`. */
11
27
  readonly version?: number;
28
+ /**
29
+ * Cross-tab sync via {@link BroadcastChannel}. When `true`, every successful
30
+ * write broadcasts the new values to other tabs subscribed to the same
31
+ * `(db, store, key)` triple; the receiving tab feeds them back through the
32
+ * `subscribe` callback exactly as it would for a `popstate` or `storage`
33
+ * event. Default: `false` — preserves the v0.5 "no subscribe" behavior so
34
+ * existing consumers see no change.
35
+ *
36
+ * Channel name is derived from `(dbName, storeName, recordKey)`. SSR-safe:
37
+ * skips registration when `BroadcastChannel` is unavailable.
38
+ */
39
+ readonly broadcast?: boolean;
40
+ /**
41
+ * Schema-migration callback. See {@link IdbUpgradeCallback}. Most consumers
42
+ * don't need this — the lib creates the object store on first open. Reach
43
+ * for it only when you bump `version` and need to add indexes or migrate
44
+ * record shapes.
45
+ */
46
+ readonly onUpgrade?: IdbUpgradeCallback;
12
47
  }
13
48
  /**
14
49
  * IndexedDB storage backend.
@@ -18,14 +53,11 @@ interface IdbStorageOptions {
18
53
  * `key` option), stored as a structured-clonable object — no JSON
19
54
  * serialization layer.
20
55
  *
21
- * v0.5 limitations (likely v0.6+ candidates if a consumer asks):
22
- * - **No cross-tab sync** (no BroadcastChannel). The same database is shared
23
- * across tabs, but writes from one tab don't push to another. Subscribe is
24
- * intentionally absent.
25
- * - **No consumer-supplied `onupgradeneeded` hook.** The `version` option
26
- * triggers an upgrade transaction and the lib creates the object store on
27
- * first open, but you can't migrate from a previous shape.
56
+ * v0.6 capabilities (opt-in, both backward-compatible):
57
+ * - **`broadcast: true`** for cross-tab sync via `BroadcastChannel`.
58
+ * - **`onUpgrade`** for consumer-supplied schema migrations across `version`
59
+ * bumps (add indexes, transform records).
28
60
  */
29
61
  declare function idbStorage<T = Record<string, unknown>>(opts: IdbStorageOptions): ParamsStorage<T>;
30
62
 
31
- export { type IdbStorageOptions, idbStorage };
63
+ export { type IdbStorageOptions, type IdbUpgradeCallback, idbStorage };
@@ -5,23 +5,41 @@ function idbStorage(opts) {
5
5
  const storeName = opts.store ?? "params";
6
6
  const recordKey = opts.key ?? "default";
7
7
  const version = opts.version ?? 1;
8
+ const broadcastEnabled = opts.broadcast === true;
9
+ const onUpgrade = opts.onUpgrade;
10
+ const channelName = `@victorylabs/params:${dbName}:${storeName}:${recordKey}`;
8
11
  let dbHandle;
9
12
  const openDb = async () => {
10
13
  if (!isClient) throw new Error("idbStorage: window.indexedDB unavailable");
11
14
  if (dbHandle && dbHandle.version === version) return dbHandle;
12
15
  return new Promise((resolve, reject) => {
13
16
  const req = window.indexedDB.open(dbName, version);
14
- req.onupgradeneeded = () => {
17
+ req.onupgradeneeded = (event) => {
15
18
  const db = req.result;
16
19
  if (!db.objectStoreNames.contains(storeName)) {
17
20
  db.createObjectStore(storeName);
18
21
  }
22
+ if (onUpgrade && req.transaction) {
23
+ onUpgrade({
24
+ db,
25
+ transaction: req.transaction,
26
+ oldVersion: event.oldVersion,
27
+ newVersion: event.newVersion ?? version
28
+ });
29
+ }
19
30
  };
20
31
  req.onsuccess = () => {
21
32
  dbHandle = req.result;
22
33
  dbHandle.onclose = () => {
23
34
  dbHandle = void 0;
24
35
  };
36
+ dbHandle.onversionchange = () => {
37
+ try {
38
+ dbHandle?.close();
39
+ } catch {
40
+ }
41
+ dbHandle = void 0;
42
+ };
25
43
  resolve(req.result);
26
44
  };
27
45
  req.onerror = () => reject(req.error);
@@ -41,7 +59,30 @@ function idbStorage(opts) {
41
59
  return op(await openDb());
42
60
  }
43
61
  };
44
- return {
62
+ const broadcastSupported = () => broadcastEnabled && typeof BroadcastChannel !== "undefined";
63
+ let sharedChannel;
64
+ const subscribers = /* @__PURE__ */ new Set();
65
+ const ensureChannel = () => {
66
+ if (!broadcastSupported()) return void 0;
67
+ if (sharedChannel === void 0) {
68
+ sharedChannel = new BroadcastChannel(channelName);
69
+ sharedChannel.onmessage = (event) => {
70
+ const data = event.data;
71
+ const values = data?.values ?? {};
72
+ for (const cb of [...subscribers]) cb(values);
73
+ };
74
+ }
75
+ return sharedChannel;
76
+ };
77
+ const broadcast = (values) => {
78
+ const ch = ensureChannel();
79
+ if (ch === void 0) return;
80
+ try {
81
+ ch.postMessage({ values });
82
+ } catch {
83
+ }
84
+ };
85
+ const storage = {
45
86
  name: "idbStorage",
46
87
  clientOnly: true,
47
88
  // IDB has no synchronous API. Engine consults `readAsync` for hydration;
@@ -73,6 +114,7 @@ function idbStorage(opts) {
73
114
  req.onerror = () => reject(req.error);
74
115
  })
75
116
  );
117
+ broadcast(values);
76
118
  } catch {
77
119
  }
78
120
  },
@@ -88,6 +130,7 @@ function idbStorage(opts) {
88
130
  req.onerror = () => reject(req.error);
89
131
  })
90
132
  );
133
+ broadcast(void 0);
91
134
  return;
92
135
  }
93
136
  const current = await withVersionRetry(
@@ -107,11 +150,21 @@ function idbStorage(opts) {
107
150
  req.onerror = () => reject(req.error);
108
151
  })
109
152
  );
153
+ broadcast(current);
110
154
  } catch {
111
155
  }
112
156
  }
113
- // No `subscribe` — IDB doesn't fire native change events. v0.6+ via BroadcastChannel.
114
157
  };
158
+ if (broadcastSupported()) {
159
+ storage.subscribe = (callback) => {
160
+ ensureChannel();
161
+ subscribers.add(callback);
162
+ return () => {
163
+ subscribers.delete(callback);
164
+ };
165
+ };
166
+ }
167
+ return storage;
115
168
  }
116
169
  export {
117
170
  idbStorage