@pyreon/storage 0.12.13 → 0.12.14

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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"32d916ff-1","name":"registry.ts"},{"uid":"32d916ff-3","name":"utils.ts"},{"uid":"32d916ff-5","name":"cookie.ts"},{"uid":"32d916ff-7","name":"custom.ts"},{"uid":"32d916ff-9","name":"indexed-db.ts"},{"uid":"32d916ff-11","name":"local.ts"},{"uid":"32d916ff-13","name":"session.ts"},{"uid":"32d916ff-15","name":"clear.ts"},{"uid":"32d916ff-17","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"32d916ff-1":{"renderedLength":1076,"gzipLength":468,"brotliLength":0,"metaUid":"32d916ff-0"},"32d916ff-3":{"renderedLength":1231,"gzipLength":565,"brotliLength":0,"metaUid":"32d916ff-2"},"32d916ff-5":{"renderedLength":3215,"gzipLength":1193,"brotliLength":0,"metaUid":"32d916ff-4"},"32d916ff-7":{"renderedLength":2242,"gzipLength":890,"brotliLength":0,"metaUid":"32d916ff-6"},"32d916ff-9":{"renderedLength":4168,"gzipLength":1353,"brotliLength":0,"metaUid":"32d916ff-8"},"32d916ff-11":{"renderedLength":2341,"gzipLength":901,"brotliLength":0,"metaUid":"32d916ff-10"},"32d916ff-13":{"renderedLength":932,"gzipLength":464,"brotliLength":0,"metaUid":"32d916ff-12"},"32d916ff-15":{"renderedLength":1489,"gzipLength":604,"brotliLength":0,"metaUid":"32d916ff-14"},"32d916ff-17":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"32d916ff-16"}},"nodeMetas":{"32d916ff-0":{"id":"/src/registry.ts","moduleParts":{"index.js":"32d916ff-1"},"imported":[],"importedBy":[{"uid":"32d916ff-16"},{"uid":"32d916ff-4"},{"uid":"32d916ff-6"},{"uid":"32d916ff-8"},{"uid":"32d916ff-10"},{"uid":"32d916ff-12"},{"uid":"32d916ff-14"}]},"32d916ff-2":{"id":"/src/utils.ts","moduleParts":{"index.js":"32d916ff-3"},"imported":[],"importedBy":[{"uid":"32d916ff-4"},{"uid":"32d916ff-6"},{"uid":"32d916ff-8"},{"uid":"32d916ff-10"},{"uid":"32d916ff-12"},{"uid":"32d916ff-14"}]},"32d916ff-4":{"id":"/src/cookie.ts","moduleParts":{"index.js":"32d916ff-5"},"imported":[{"uid":"32d916ff-18"},{"uid":"32d916ff-0"},{"uid":"32d916ff-2"}],"importedBy":[{"uid":"32d916ff-16"}]},"32d916ff-6":{"id":"/src/custom.ts","moduleParts":{"index.js":"32d916ff-7"},"imported":[{"uid":"32d916ff-18"},{"uid":"32d916ff-0"},{"uid":"32d916ff-2"}],"importedBy":[{"uid":"32d916ff-16"}]},"32d916ff-8":{"id":"/src/indexed-db.ts","moduleParts":{"index.js":"32d916ff-9"},"imported":[{"uid":"32d916ff-18"},{"uid":"32d916ff-0"},{"uid":"32d916ff-2"}],"importedBy":[{"uid":"32d916ff-16"}]},"32d916ff-10":{"id":"/src/local.ts","moduleParts":{"index.js":"32d916ff-11"},"imported":[{"uid":"32d916ff-18"},{"uid":"32d916ff-0"},{"uid":"32d916ff-2"}],"importedBy":[{"uid":"32d916ff-16"},{"uid":"32d916ff-12"}]},"32d916ff-12":{"id":"/src/session.ts","moduleParts":{"index.js":"32d916ff-13"},"imported":[{"uid":"32d916ff-18"},{"uid":"32d916ff-10"},{"uid":"32d916ff-0"},{"uid":"32d916ff-2"}],"importedBy":[{"uid":"32d916ff-16"}]},"32d916ff-14":{"id":"/src/clear.ts","moduleParts":{"index.js":"32d916ff-15"},"imported":[{"uid":"32d916ff-0"},{"uid":"32d916ff-2"}],"importedBy":[{"uid":"32d916ff-16"}]},"32d916ff-16":{"id":"/src/index.ts","moduleParts":{"index.js":"32d916ff-17"},"imported":[{"uid":"32d916ff-4"},{"uid":"32d916ff-6"},{"uid":"32d916ff-8"},{"uid":"32d916ff-10"},{"uid":"32d916ff-12"},{"uid":"32d916ff-14"},{"uid":"32d916ff-0"}],"importedBy":[],"isEntry":true},"32d916ff-18":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"32d916ff-4"},{"uid":"32d916ff-6"},{"uid":"32d916ff-8"},{"uid":"32d916ff-10"},{"uid":"32d916ff-12"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"9a20036d-1","name":"registry.ts"},{"uid":"9a20036d-3","name":"utils.ts"},{"uid":"9a20036d-5","name":"cookie.ts"},{"uid":"9a20036d-7","name":"custom.ts"},{"uid":"9a20036d-9","name":"indexed-db.ts"},{"uid":"9a20036d-11","name":"local.ts"},{"uid":"9a20036d-13","name":"session.ts"},{"uid":"9a20036d-15","name":"clear.ts"},{"uid":"9a20036d-17","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"9a20036d-1":{"renderedLength":1076,"gzipLength":468,"brotliLength":0,"metaUid":"9a20036d-0"},"9a20036d-3":{"renderedLength":1231,"gzipLength":565,"brotliLength":0,"metaUid":"9a20036d-2"},"9a20036d-5":{"renderedLength":3215,"gzipLength":1193,"brotliLength":0,"metaUid":"9a20036d-4"},"9a20036d-7":{"renderedLength":2242,"gzipLength":890,"brotliLength":0,"metaUid":"9a20036d-6"},"9a20036d-9":{"renderedLength":4316,"gzipLength":1411,"brotliLength":0,"metaUid":"9a20036d-8"},"9a20036d-11":{"renderedLength":3217,"gzipLength":1141,"brotliLength":0,"metaUid":"9a20036d-10"},"9a20036d-13":{"renderedLength":932,"gzipLength":464,"brotliLength":0,"metaUid":"9a20036d-12"},"9a20036d-15":{"renderedLength":1489,"gzipLength":604,"brotliLength":0,"metaUid":"9a20036d-14"},"9a20036d-17":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"9a20036d-16"}},"nodeMetas":{"9a20036d-0":{"id":"/src/registry.ts","moduleParts":{"index.js":"9a20036d-1"},"imported":[],"importedBy":[{"uid":"9a20036d-16"},{"uid":"9a20036d-4"},{"uid":"9a20036d-6"},{"uid":"9a20036d-8"},{"uid":"9a20036d-10"},{"uid":"9a20036d-12"},{"uid":"9a20036d-14"}]},"9a20036d-2":{"id":"/src/utils.ts","moduleParts":{"index.js":"9a20036d-3"},"imported":[],"importedBy":[{"uid":"9a20036d-4"},{"uid":"9a20036d-6"},{"uid":"9a20036d-8"},{"uid":"9a20036d-10"},{"uid":"9a20036d-12"},{"uid":"9a20036d-14"}]},"9a20036d-4":{"id":"/src/cookie.ts","moduleParts":{"index.js":"9a20036d-5"},"imported":[{"uid":"9a20036d-18"},{"uid":"9a20036d-0"},{"uid":"9a20036d-2"}],"importedBy":[{"uid":"9a20036d-16"}]},"9a20036d-6":{"id":"/src/custom.ts","moduleParts":{"index.js":"9a20036d-7"},"imported":[{"uid":"9a20036d-18"},{"uid":"9a20036d-0"},{"uid":"9a20036d-2"}],"importedBy":[{"uid":"9a20036d-16"}]},"9a20036d-8":{"id":"/src/indexed-db.ts","moduleParts":{"index.js":"9a20036d-9"},"imported":[{"uid":"9a20036d-18"},{"uid":"9a20036d-0"},{"uid":"9a20036d-2"}],"importedBy":[{"uid":"9a20036d-16"}]},"9a20036d-10":{"id":"/src/local.ts","moduleParts":{"index.js":"9a20036d-11"},"imported":[{"uid":"9a20036d-18"},{"uid":"9a20036d-0"},{"uid":"9a20036d-2"}],"importedBy":[{"uid":"9a20036d-16"},{"uid":"9a20036d-12"}]},"9a20036d-12":{"id":"/src/session.ts","moduleParts":{"index.js":"9a20036d-13"},"imported":[{"uid":"9a20036d-18"},{"uid":"9a20036d-10"},{"uid":"9a20036d-0"},{"uid":"9a20036d-2"}],"importedBy":[{"uid":"9a20036d-16"}]},"9a20036d-14":{"id":"/src/clear.ts","moduleParts":{"index.js":"9a20036d-15"},"imported":[{"uid":"9a20036d-0"},{"uid":"9a20036d-2"}],"importedBy":[{"uid":"9a20036d-16"}]},"9a20036d-16":{"id":"/src/index.ts","moduleParts":{"index.js":"9a20036d-17"},"imported":[{"uid":"9a20036d-4"},{"uid":"9a20036d-6"},{"uid":"9a20036d-8"},{"uid":"9a20036d-10"},{"uid":"9a20036d-12"},{"uid":"9a20036d-14"},{"uid":"9a20036d-0"}],"importedBy":[],"isEntry":true},"9a20036d-18":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"9a20036d-4"},{"uid":"9a20036d-6"},{"uid":"9a20036d-8"},{"uid":"9a20036d-10"},{"uid":"9a20036d-12"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -281,6 +281,7 @@ const useMemoryStorage = createStorage((() => {
281
281
  //#region src/indexed-db.ts
282
282
  const dbCache = /* @__PURE__ */ new Map();
283
283
  function openDB(dbName, storeName) {
284
+ if (typeof indexedDB === "undefined") return Promise.reject(/* @__PURE__ */ new Error("[Pyreon] indexedDB is not available in this environment"));
284
285
  const cacheKey = `${dbName}:${storeName}`;
285
286
  const cached = dbCache.get(cacheKey);
286
287
  if (cached) return cached;
@@ -396,17 +397,44 @@ function _resetDBCache() {
396
397
 
397
398
  //#endregion
398
399
  //#region src/local.ts
399
- let listenerAttached = false;
400
- function attachStorageListener() {
401
- if (listenerAttached || !isBrowser()) return;
402
- listenerAttached = true;
403
- window.addEventListener("storage", (e) => {
404
- if (!e.key) return;
405
- const entry = getEntry("local", e.key);
406
- if (!entry) return;
407
- const newValue = e.newValue !== null ? deserialize(e.newValue, entry.defaultValue) : entry.defaultValue;
408
- entry.signal.set(newValue);
409
- });
400
+ let activeCount = 0;
401
+ let storageHandler = null;
402
+ function onStorageEvent(e) {
403
+ if (!e.key) return;
404
+ const entry = getEntry("local", e.key);
405
+ if (!entry) return;
406
+ const newValue = e.newValue !== null ? deserialize(e.newValue, entry.defaultValue) : entry.defaultValue;
407
+ entry.signal.set(newValue);
408
+ }
409
+ function retainStorageListener() {
410
+ if (!isBrowser()) return;
411
+ activeCount++;
412
+ if (storageHandler === null) {
413
+ storageHandler = onStorageEvent;
414
+ window.addEventListener("storage", storageHandler);
415
+ }
416
+ }
417
+ /**
418
+ * Test-only: force-detach the cross-tab listener and reset the refcount.
419
+ * Used in test teardown to keep `_resetRegistry` and listener state in sync.
420
+ */
421
+ function _resetStorageListener() {
422
+ if (storageHandler !== null && isBrowser()) window.removeEventListener("storage", storageHandler);
423
+ storageHandler = null;
424
+ activeCount = 0;
425
+ }
426
+ /**
427
+ * Release one refcount on the cross-tab listener. Detaches the window-level
428
+ * handler when the count drops to zero. Called from `.remove()`.
429
+ */
430
+ function releaseStorageListener() {
431
+ if (!isBrowser()) return;
432
+ if (activeCount === 0) return;
433
+ activeCount--;
434
+ if (activeCount === 0 && storageHandler !== null) {
435
+ window.removeEventListener("storage", storageHandler);
436
+ storageHandler = null;
437
+ }
410
438
  }
411
439
  /**
412
440
  * Reactive signal backed by localStorage. Automatically syncs across
@@ -431,7 +459,7 @@ function useStorage(key, defaultValue, options) {
431
459
  }
432
460
  const storageSig = createStorageSignal(signal(initialValue), key, defaultValue, "local", options);
433
461
  setEntry("local", key, storageSig, defaultValue);
434
- attachStorageListener();
462
+ retainStorageListener();
435
463
  return storageSig;
436
464
  }
437
465
  /**
@@ -465,6 +493,7 @@ function createStorageSignal(sig, key, defaultValue, backend, options) {
465
493
  sig.set(defaultValue);
466
494
  if (storage) storage.removeItem(key);
467
495
  removeEntry(backend, key);
496
+ if (backend === "local") releaseStorageListener();
468
497
  };
469
498
  return storageSig;
470
499
  }
@@ -548,5 +577,5 @@ function clearBackend(type) {
548
577
  }
549
578
 
550
579
  //#endregion
551
- export { _resetDBCache, _resetRegistry, clearStorage, createStorage, removeStorage, setCookieSource, useCookie, useIndexedDB, useMemoryStorage, useSessionStorage, useStorage };
580
+ export { _resetDBCache, _resetRegistry, _resetStorageListener, clearStorage, createStorage, removeStorage, setCookieSource, useCookie, useIndexedDB, useMemoryStorage, useSessionStorage, useStorage };
552
581
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/registry.ts","../src/utils.ts","../src/cookie.ts","../src/custom.ts","../src/indexed-db.ts","../src/local.ts","../src/session.ts","../src/clear.ts"],"sourcesContent":["import type { StorageSignal } from './types'\n\n// ─── Signal Registry ─────────────────────────────────────────────────────────\n\ninterface RegistryEntry<T = unknown> {\n signal: StorageSignal<T>\n defaultValue: T\n backend: string\n}\n\nconst registry = new Map<string, RegistryEntry>()\n\n/**\n * Build a composite key from backend type + storage key to avoid\n * collisions between different backends using the same key name.\n */\nfunction registryKey(backend: string, key: string): string {\n return `${backend}:${key}`\n}\n\n/**\n * Get an existing signal from the registry.\n */\nexport function getEntry<T>(backend: string, key: string): RegistryEntry<T> | undefined {\n return registry.get(registryKey(backend, key)) as RegistryEntry<T> | undefined\n}\n\n/**\n * Register a new signal in the registry.\n */\nexport function setEntry<T>(\n backend: string,\n key: string,\n signal: StorageSignal<T>,\n defaultValue: T,\n): void {\n registry.set(registryKey(backend, key), { signal, defaultValue, backend })\n}\n\n/**\n * Remove an entry from the registry.\n */\nexport function removeEntry(backend: string, key: string): void {\n registry.delete(registryKey(backend, key))\n}\n\n/**\n * Get all entries for a specific backend.\n */\nexport function getEntriesByBackend(backend: string): RegistryEntry[] {\n const entries: RegistryEntry[] = []\n for (const entry of registry.values()) {\n if (entry.backend === backend) entries.push(entry)\n }\n return entries\n}\n\n/**\n * Clear all entries from the registry. Used for testing.\n */\nexport function _resetRegistry(): void {\n registry.clear()\n}\n","import type { StorageOptions } from './types'\n\n// ─── SSR Detection ───────────────────────────────────────────────────────────\n\n/**\n * Check if we're running in a browser environment.\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\n// ─── Serialization ───────────────────────────────────────────────────────────\n\n/**\n * Serialize a value to a string for storage.\n */\nexport function serialize<T>(value: T, serializer?: StorageOptions<T>['serializer']): string {\n if (serializer) return serializer(value)\n return JSON.stringify(value)\n}\n\n/**\n * Deserialize a raw string from storage back to a typed value.\n * Returns the default value if deserialization fails.\n */\nexport function deserialize<T>(\n raw: string,\n defaultValue: T,\n deserializer?: StorageOptions<T>['deserializer'],\n onError?: StorageOptions<T>['onError'],\n): T {\n try {\n if (deserializer) return deserializer(raw)\n return JSON.parse(raw) as T\n } catch (e) {\n if (onError) {\n const result = onError(e as Error)\n return result !== undefined ? result : defaultValue\n }\n return defaultValue\n }\n}\n\n// ─── Safe Storage Access ─────────────────────────────────────────────────────\n\n/**\n * Safely get a Web Storage instance (localStorage or sessionStorage).\n * Returns null if not available (SSR, security restrictions, etc.).\n */\nexport function getWebStorage(type: 'local' | 'session'): Storage | null {\n if (!isBrowser()) return null\n try {\n const storage = type === 'local' ? window.localStorage : window.sessionStorage\n // Test that it actually works (can throw in private browsing)\n const testKey = '__pyreon_storage_test__'\n storage.setItem(testKey, '1')\n storage.removeItem(testKey)\n return storage\n } catch {\n return null\n }\n}\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { CookieOptions, StorageSignal } from './types'\nimport { deserialize, isBrowser, serialize } from './utils'\n\n// ─── Server-side cookie source ───────────────────────────────────────────────\n\nlet serverCookieString = ''\n\n/**\n * Set the cookie source string for SSR. Call this once per request\n * with the raw Cookie header value.\n *\n * @example\n * ```ts\n * // In your SSR request handler\n * setCookieSource(request.headers.get('cookie') ?? '')\n * ```\n */\nexport function setCookieSource(cookieHeader: string): void {\n serverCookieString = cookieHeader\n}\n\n// ─── Cookie parsing ──────────────────────────────────────────────────────────\n\nfunction parseCookies(cookieString: string): Map<string, string> {\n const cookies = new Map<string, string>()\n if (!cookieString) return cookies\n\n for (const pair of cookieString.split(';')) {\n const eqIndex = pair.indexOf('=')\n if (eqIndex === -1) continue\n const name = pair.slice(0, eqIndex).trim()\n const value = pair.slice(eqIndex + 1).trim()\n if (name) cookies.set(name, decodeURIComponent(value))\n }\n\n return cookies\n}\n\nfunction getCookieString(): string {\n if (isBrowser()) return document.cookie\n return serverCookieString\n}\n\nfunction readCookie(key: string): string | null {\n const cookies = parseCookies(getCookieString())\n return cookies.get(key) ?? null\n}\n\n// ─── Cookie writing ──────────────────────────────────────────────────────────\n\nfunction writeCookie<T>(key: string, value: T, options: CookieOptions<T>): void {\n if (!isBrowser()) return\n\n const serialized = serialize(value, options.serializer)\n let cookie = `${encodeURIComponent(key)}=${encodeURIComponent(serialized)}`\n\n if (options.maxAge !== undefined) {\n cookie += `; max-age=${options.maxAge}`\n }\n if (options.expires) {\n cookie += `; expires=${options.expires.toUTCString()}`\n }\n cookie += `; path=${options.path ?? '/'}`\n if (options.domain) {\n cookie += `; domain=${options.domain}`\n }\n if (options.secure) {\n cookie += '; secure'\n }\n cookie += `; samesite=${options.sameSite ?? 'lax'}`\n\n document.cookie = cookie\n}\n\nfunction deleteCookie<T>(key: string, options: CookieOptions<T>): void {\n if (!isBrowser()) return\n\n let cookie = `${encodeURIComponent(key)}=; max-age=0`\n cookie += `; path=${options.path ?? '/'}`\n if (options.domain) {\n cookie += `; domain=${options.domain}`\n }\n\n document.cookie = cookie\n}\n\n// ─── useCookie ───────────────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by a browser cookie. SSR-compatible when\n * used with setCookieSource().\n *\n * @example\n * ```ts\n * const locale = useCookie('locale', 'en', {\n * maxAge: 60 * 60 * 24 * 365, // 1 year\n * path: '/',\n * sameSite: 'lax',\n * })\n * locale() // 'en'\n * locale.set('de') // sets cookie + updates signal\n * locale.remove() // deletes cookie, resets to default\n * ```\n */\nexport function useCookie<T>(\n key: string,\n defaultValue: T,\n options: CookieOptions<T> = {},\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('cookie', key)\n if (existing) return existing.signal\n\n // Read initial value from cookie\n const raw = readCookie(key)\n const initialValue =\n raw !== null\n ? deserialize(raw, defaultValue, options.deserializer, options.onError)\n : defaultValue\n\n const sig = signal<T>(initialValue)\n\n // Build the storage signal\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n storageSig.set = (value: T) => {\n sig.set(value)\n writeCookie(key, value, options)\n }\n\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n storageSig.remove = () => {\n sig.set(defaultValue)\n deleteCookie(key, options)\n removeEntry('cookie', key)\n }\n\n setEntry('cookie', key, storageSig, defaultValue)\n\n return storageSig\n}\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { StorageBackend, StorageOptions, StorageSignal } from './types'\nimport { deserialize, serialize } from './utils'\n\n// ─── createStorage ───────────────────────────────────────────────────────────\n\n/**\n * Create a custom storage hook backed by any synchronous storage backend.\n * Useful for encrypted storage, in-memory storage, or custom adapters.\n *\n * @example\n * ```ts\n * const useEncrypted = createStorage({\n * get: (key) => decrypt(localStorage.getItem(key)),\n * set: (key, value) => localStorage.setItem(key, encrypt(value)),\n * remove: (key) => localStorage.removeItem(key),\n * })\n *\n * const secret = useEncrypted('api-key', '')\n * ```\n */\nexport function createStorage(\n backend: StorageBackend,\n backendName?: string,\n): <T>(key: string, defaultValue: T, options?: StorageOptions<T>) => StorageSignal<T> {\n const name = backendName ?? 'custom'\n\n return function useCustomStorage<T>(\n key: string,\n defaultValue: T,\n options?: StorageOptions<T>,\n ): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>(name, key)\n if (existing) return existing.signal\n\n // Read initial value\n let initialValue = defaultValue\n try {\n const raw = backend.get(key)\n if (raw !== null) {\n initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)\n }\n } catch {\n // Backend read failed — use default\n }\n\n const sig = signal<T>(initialValue)\n\n // Build the storage signal\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n storageSig.set = (value: T) => {\n sig.set(value)\n try {\n backend.set(key, serialize(value, options?.serializer))\n } catch {\n // Write failed — signal still updates\n }\n }\n\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n storageSig.remove = () => {\n sig.set(defaultValue)\n try {\n backend.remove(key)\n } catch {\n // Remove failed\n }\n removeEntry(name, key)\n }\n\n setEntry(name, key, storageSig, defaultValue)\n\n return storageSig\n }\n}\n\n// ─── Memory storage ──────────────────────────────────────────────────────────\n\n/**\n * In-memory storage backend. Useful for SSR, testing, or ephemeral state.\n * Values are lost on page unload.\n *\n * @example\n * ```ts\n * import { useMemoryStorage } from '@pyreon/storage'\n *\n * const temp = useMemoryStorage('key', 'default')\n * ```\n */\nexport const useMemoryStorage = createStorage(\n (() => {\n const store = new Map<string, string>()\n return {\n get: (key: string) => store.get(key) ?? null,\n set: (key: string, value: string) => store.set(key, value),\n remove: (key: string) => store.delete(key),\n }\n })(),\n 'memory',\n)\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { IndexedDBOptions, StorageSignal } from './types'\nimport { deserialize, isBrowser, serialize } from './utils'\n\n// ─── Database management ─────────────────────────────────────────────────────\n\nconst dbCache = new Map<string, Promise<IDBDatabase>>()\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n const cacheKey = `${dbName}:${storeName}`\n const cached = dbCache.get(cacheKey)\n if (cached) return cached\n\n const promise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName)\n }\n }\n\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n\n dbCache.set(cacheKey, promise)\n return promise\n}\n\nfunction idbGet(db: IDBDatabase, storeName: string, key: string): Promise<string | null> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeName, 'readonly')\n const store = tx.objectStore(storeName)\n const request = store.get(key)\n request.onsuccess = () =>\n resolve(request.result !== undefined ? (request.result as string) : null)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbSet(db: IDBDatabase, storeName: string, key: string, value: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const store = tx.objectStore(storeName)\n const request = store.put(value, key)\n request.onsuccess = () => resolve()\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbDelete(db: IDBDatabase, storeName: string, key: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const store = tx.objectStore(storeName)\n const request = store.delete(key)\n request.onsuccess = () => resolve()\n request.onerror = () => reject(request.error)\n })\n}\n\n// ─── useIndexedDB ────────────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by IndexedDB. Suitable for large or structured\n * data that exceeds localStorage limits. Writes are debounced.\n *\n * The signal starts with `defaultValue` and updates asynchronously\n * when the stored value is read from IndexedDB.\n *\n * @example\n * ```ts\n * const draft = useIndexedDB('article-draft', { title: '', body: '' })\n * draft() // { title: '', body: '' } initially, then stored value\n * draft.set({ title: 'My Post', body: '...' }) // signal updates immediately, IDB write is debounced\n * ```\n */\nexport function useIndexedDB<T>(\n key: string,\n defaultValue: T,\n options: IndexedDBOptions<T> = {},\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('indexeddb', key)\n if (existing) return existing.signal\n\n const dbName = options.dbName ?? 'pyreon-storage'\n const storeName = options.storeName ?? 'kv'\n const debounceMs = options.debounceMs ?? 100\n\n const sig = signal<T>(defaultValue)\n\n // Async initial load\n if (isBrowser() && typeof indexedDB !== 'undefined') {\n openDB(dbName, storeName)\n .then((db) => idbGet(db, storeName, key))\n .then((raw) => {\n if (raw !== null) {\n const value = deserialize(raw, defaultValue, options.deserializer, options.onError)\n sig.set(value)\n }\n })\n .catch(() => {\n // IndexedDB not available — signal keeps defaultValue\n })\n }\n\n // Debounced write\n let writeTimer: ReturnType<typeof setTimeout> | null = null\n let pendingValue: T | undefined\n\n function flushWrite(): void {\n if (pendingValue === undefined) return\n const value = pendingValue\n pendingValue = undefined\n\n if (!isBrowser() || typeof indexedDB === 'undefined') return\n\n openDB(dbName, storeName)\n .then((db) => idbSet(db, storeName, key, serialize(value, options.serializer)))\n .catch(() => {\n // Write failed — signal still has the correct value\n })\n }\n\n function scheduleWrite(value: T): void {\n pendingValue = value\n if (writeTimer !== null) clearTimeout(writeTimer)\n writeTimer = setTimeout(flushWrite, debounceMs)\n }\n\n // Build the storage signal\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n storageSig.set = (value: T) => {\n sig.set(value)\n scheduleWrite(value)\n }\n\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n storageSig.remove = () => {\n sig.set(defaultValue)\n pendingValue = undefined\n if (writeTimer !== null) clearTimeout(writeTimer)\n\n if (isBrowser() && typeof indexedDB !== 'undefined') {\n openDB(dbName, storeName)\n .then((db) => idbDelete(db, storeName, key))\n .catch(() => {\n // Delete failed — signal already reset\n })\n }\n\n removeEntry('indexeddb', key)\n }\n\n setEntry('indexeddb', key, storageSig, defaultValue)\n\n return storageSig\n}\n\n/**\n * Reset the database cache. For testing only.\n */\nexport function _resetDBCache(): void {\n dbCache.clear()\n}\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { StorageOptions, StorageSignal } from './types'\nimport { deserialize, getWebStorage, isBrowser, serialize } from './utils'\n\n// ─── Cross-tab sync ──────────────────────────────────────────────────────────\n\nlet listenerAttached = false\n\nfunction attachStorageListener(): void {\n if (listenerAttached || !isBrowser()) return\n listenerAttached = true\n\n window.addEventListener('storage', (e) => {\n if (!e.key) return\n const entry = getEntry('local', e.key)\n if (!entry) return\n\n const newValue =\n e.newValue !== null ? deserialize(e.newValue, entry.defaultValue) : entry.defaultValue\n\n entry.signal.set(newValue)\n })\n}\n\n// ─── useStorage ──────────────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by localStorage. Automatically syncs across\n * browser tabs via the native `storage` event.\n *\n * @example\n * ```ts\n * const theme = useStorage('theme', 'light')\n * theme() // 'light' (or stored value)\n * theme.set('dark') // updates signal + localStorage\n * theme.remove() // clears storage, resets to default\n * ```\n */\nexport function useStorage<T>(\n key: string,\n defaultValue: T,\n options?: StorageOptions<T>,\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('local', key)\n if (existing) return existing.signal\n\n const storage = getWebStorage('local')\n\n // Read initial value from storage\n let initialValue = defaultValue\n if (storage) {\n const raw = storage.getItem(key)\n if (raw !== null) {\n initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)\n }\n }\n\n const sig = signal<T>(initialValue)\n\n // Create the storage signal by extending the base signal\n const storageSig = createStorageSignal(sig, key, defaultValue, 'local', options)\n\n setEntry('local', key, storageSig, defaultValue)\n attachStorageListener()\n\n return storageSig\n}\n\n// ─── Storage Signal Factory ──────────────────────────────────────────────────\n\n/**\n * Wraps a base signal with storage persistence behavior.\n * Used by both useStorage and useSessionStorage.\n */\nexport function createStorageSignal<T>(\n sig: ReturnType<typeof signal<T>>,\n key: string,\n defaultValue: T,\n backend: 'local' | 'session',\n options?: StorageOptions<T>,\n): StorageSignal<T> {\n const storage = getWebStorage(backend)\n\n // The callable signal function (read)\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n // Delegate all signal methods\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n // Override set to persist\n storageSig.set = (value: T) => {\n sig.set(value)\n if (storage) {\n try {\n storage.setItem(key, serialize(value, options?.serializer))\n } catch {\n // Storage full or blocked — signal still updates\n }\n }\n }\n\n // Override update to persist\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n // Add remove method\n storageSig.remove = () => {\n sig.set(defaultValue)\n if (storage) {\n storage.removeItem(key)\n }\n removeEntry(backend, key)\n }\n\n return storageSig\n}\n","import { signal } from '@pyreon/reactivity'\nimport { createStorageSignal } from './local'\nimport { getEntry, setEntry } from './registry'\nimport type { StorageOptions, StorageSignal } from './types'\nimport { deserialize, getWebStorage } from './utils'\n\n// ─── useSessionStorage ───────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by sessionStorage. Scoped to the current\n * browser tab — does not sync across tabs.\n *\n * @example\n * ```ts\n * const step = useSessionStorage('wizard-step', 0)\n * step() // 0 (or stored value)\n * step.set(3) // updates signal + sessionStorage\n * step.remove() // clears storage, resets to default\n * ```\n */\nexport function useSessionStorage<T>(\n key: string,\n defaultValue: T,\n options?: StorageOptions<T>,\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('session', key)\n if (existing) return existing.signal\n\n const storage = getWebStorage('session')\n\n // Read initial value from storage\n let initialValue = defaultValue\n if (storage) {\n const raw = storage.getItem(key)\n if (raw !== null) {\n initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)\n }\n }\n\n const sig = signal<T>(initialValue)\n const storageSig = createStorageSignal(sig, key, defaultValue, 'session', options)\n\n setEntry('session', key, storageSig, defaultValue)\n\n return storageSig\n}\n","import { getEntriesByBackend, getEntry, removeEntry } from './registry'\nimport { getWebStorage, isBrowser } from './utils'\n\n// ─── Storage type mapping ────────────────────────────────────────────────────\n\ntype StorageType = 'local' | 'session' | 'cookie' | 'indexeddb' | 'all'\n\n// ─── removeStorage ───────────────────────────────────────────────────────────\n\n/**\n * Remove a specific key from storage and reset its signal to the default value.\n *\n * @example\n * ```ts\n * removeStorage('theme') // from localStorage\n * removeStorage('step', { type: 'session' }) // from sessionStorage\n * removeStorage('locale', { type: 'cookie' }) // deletes cookie\n * ```\n */\nexport function removeStorage(\n key: string,\n options?: { type?: 'local' | 'session' | 'cookie' | 'indexeddb' },\n): void {\n const type = options?.type ?? 'local'\n const entry = getEntry(type, key)\n\n if (entry) {\n entry.signal.remove()\n } else {\n // No signal registered — still try to clear the raw storage\n if (type === 'local' || type === 'session') {\n const storage = getWebStorage(type)\n if (storage) storage.removeItem(key)\n } else if (type === 'cookie' && isBrowser()) {\n document.cookie = `${encodeURIComponent(key)}=; max-age=0; path=/`\n }\n removeEntry(type, key)\n }\n}\n\n// ─── clearStorage ────────────────────────────────────────────────────────────\n\n/**\n * Clear all managed storage entries for a specific backend, or all backends.\n *\n * @example\n * ```ts\n * clearStorage() // clear all localStorage entries managed by @pyreon/storage\n * clearStorage('session') // clear all sessionStorage entries\n * clearStorage('cookie') // clear all managed cookies\n * clearStorage('all') // clear everything\n * ```\n */\nexport function clearStorage(type: StorageType = 'local'): void {\n if (type === 'all') {\n clearBackend('local')\n clearBackend('session')\n clearBackend('cookie')\n clearBackend('indexeddb')\n return\n }\n\n clearBackend(type)\n}\n\nfunction clearBackend(type: string): void {\n const entries = getEntriesByBackend(type)\n for (const entry of entries) {\n entry.signal.remove()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/registry.ts","../src/utils.ts","../src/cookie.ts","../src/custom.ts","../src/indexed-db.ts","../src/local.ts","../src/session.ts","../src/clear.ts"],"sourcesContent":["import type { StorageSignal } from './types'\n\n// ─── Signal Registry ─────────────────────────────────────────────────────────\n\ninterface RegistryEntry<T = unknown> {\n signal: StorageSignal<T>\n defaultValue: T\n backend: string\n}\n\nconst registry = new Map<string, RegistryEntry>()\n\n/**\n * Build a composite key from backend type + storage key to avoid\n * collisions between different backends using the same key name.\n */\nfunction registryKey(backend: string, key: string): string {\n return `${backend}:${key}`\n}\n\n/**\n * Get an existing signal from the registry.\n */\nexport function getEntry<T>(backend: string, key: string): RegistryEntry<T> | undefined {\n return registry.get(registryKey(backend, key)) as RegistryEntry<T> | undefined\n}\n\n/**\n * Register a new signal in the registry.\n */\nexport function setEntry<T>(\n backend: string,\n key: string,\n signal: StorageSignal<T>,\n defaultValue: T,\n): void {\n registry.set(registryKey(backend, key), { signal, defaultValue, backend })\n}\n\n/**\n * Remove an entry from the registry.\n */\nexport function removeEntry(backend: string, key: string): void {\n registry.delete(registryKey(backend, key))\n}\n\n/**\n * Get all entries for a specific backend.\n */\nexport function getEntriesByBackend(backend: string): RegistryEntry[] {\n const entries: RegistryEntry[] = []\n for (const entry of registry.values()) {\n if (entry.backend === backend) entries.push(entry)\n }\n return entries\n}\n\n/**\n * Clear all entries from the registry. Used for testing.\n */\nexport function _resetRegistry(): void {\n registry.clear()\n}\n","import type { StorageOptions } from './types'\n\n// ─── SSR Detection ───────────────────────────────────────────────────────────\n\n/**\n * Check if we're running in a browser environment.\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\n// ─── Serialization ───────────────────────────────────────────────────────────\n\n/**\n * Serialize a value to a string for storage.\n */\nexport function serialize<T>(value: T, serializer?: StorageOptions<T>['serializer']): string {\n if (serializer) return serializer(value)\n return JSON.stringify(value)\n}\n\n/**\n * Deserialize a raw string from storage back to a typed value.\n * Returns the default value if deserialization fails.\n */\nexport function deserialize<T>(\n raw: string,\n defaultValue: T,\n deserializer?: StorageOptions<T>['deserializer'],\n onError?: StorageOptions<T>['onError'],\n): T {\n try {\n if (deserializer) return deserializer(raw)\n return JSON.parse(raw) as T\n } catch (e) {\n if (onError) {\n const result = onError(e as Error)\n return result !== undefined ? result : defaultValue\n }\n return defaultValue\n }\n}\n\n// ─── Safe Storage Access ─────────────────────────────────────────────────────\n\n/**\n * Safely get a Web Storage instance (localStorage or sessionStorage).\n * Returns null if not available (SSR, security restrictions, etc.).\n */\nexport function getWebStorage(type: 'local' | 'session'): Storage | null {\n if (!isBrowser()) return null\n try {\n const storage = type === 'local' ? window.localStorage : window.sessionStorage\n // Test that it actually works (can throw in private browsing)\n const testKey = '__pyreon_storage_test__'\n storage.setItem(testKey, '1')\n storage.removeItem(testKey)\n return storage\n } catch {\n return null\n }\n}\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { CookieOptions, StorageSignal } from './types'\nimport { deserialize, isBrowser, serialize } from './utils'\n\n// ─── Server-side cookie source ───────────────────────────────────────────────\n\nlet serverCookieString = ''\n\n/**\n * Set the cookie source string for SSR. Call this once per request\n * with the raw Cookie header value.\n *\n * @example\n * ```ts\n * // In your SSR request handler\n * setCookieSource(request.headers.get('cookie') ?? '')\n * ```\n */\nexport function setCookieSource(cookieHeader: string): void {\n serverCookieString = cookieHeader\n}\n\n// ─── Cookie parsing ──────────────────────────────────────────────────────────\n\nfunction parseCookies(cookieString: string): Map<string, string> {\n const cookies = new Map<string, string>()\n if (!cookieString) return cookies\n\n for (const pair of cookieString.split(';')) {\n const eqIndex = pair.indexOf('=')\n if (eqIndex === -1) continue\n const name = pair.slice(0, eqIndex).trim()\n const value = pair.slice(eqIndex + 1).trim()\n if (name) cookies.set(name, decodeURIComponent(value))\n }\n\n return cookies\n}\n\nfunction getCookieString(): string {\n if (isBrowser()) return document.cookie\n return serverCookieString\n}\n\nfunction readCookie(key: string): string | null {\n const cookies = parseCookies(getCookieString())\n return cookies.get(key) ?? null\n}\n\n// ─── Cookie writing ──────────────────────────────────────────────────────────\n\nfunction writeCookie<T>(key: string, value: T, options: CookieOptions<T>): void {\n if (!isBrowser()) return\n\n const serialized = serialize(value, options.serializer)\n let cookie = `${encodeURIComponent(key)}=${encodeURIComponent(serialized)}`\n\n if (options.maxAge !== undefined) {\n cookie += `; max-age=${options.maxAge}`\n }\n if (options.expires) {\n cookie += `; expires=${options.expires.toUTCString()}`\n }\n cookie += `; path=${options.path ?? '/'}`\n if (options.domain) {\n cookie += `; domain=${options.domain}`\n }\n if (options.secure) {\n cookie += '; secure'\n }\n cookie += `; samesite=${options.sameSite ?? 'lax'}`\n\n document.cookie = cookie\n}\n\nfunction deleteCookie<T>(key: string, options: CookieOptions<T>): void {\n if (!isBrowser()) return\n\n let cookie = `${encodeURIComponent(key)}=; max-age=0`\n cookie += `; path=${options.path ?? '/'}`\n if (options.domain) {\n cookie += `; domain=${options.domain}`\n }\n\n document.cookie = cookie\n}\n\n// ─── useCookie ───────────────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by a browser cookie. SSR-compatible when\n * used with setCookieSource().\n *\n * @example\n * ```ts\n * const locale = useCookie('locale', 'en', {\n * maxAge: 60 * 60 * 24 * 365, // 1 year\n * path: '/',\n * sameSite: 'lax',\n * })\n * locale() // 'en'\n * locale.set('de') // sets cookie + updates signal\n * locale.remove() // deletes cookie, resets to default\n * ```\n */\nexport function useCookie<T>(\n key: string,\n defaultValue: T,\n options: CookieOptions<T> = {},\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('cookie', key)\n if (existing) return existing.signal\n\n // Read initial value from cookie\n const raw = readCookie(key)\n const initialValue =\n raw !== null\n ? deserialize(raw, defaultValue, options.deserializer, options.onError)\n : defaultValue\n\n const sig = signal<T>(initialValue)\n\n // Build the storage signal\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n storageSig.set = (value: T) => {\n sig.set(value)\n writeCookie(key, value, options)\n }\n\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n storageSig.remove = () => {\n sig.set(defaultValue)\n deleteCookie(key, options)\n removeEntry('cookie', key)\n }\n\n setEntry('cookie', key, storageSig, defaultValue)\n\n return storageSig\n}\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { StorageBackend, StorageOptions, StorageSignal } from './types'\nimport { deserialize, serialize } from './utils'\n\n// ─── createStorage ───────────────────────────────────────────────────────────\n\n/**\n * Create a custom storage hook backed by any synchronous storage backend.\n * Useful for encrypted storage, in-memory storage, or custom adapters.\n *\n * @example\n * ```ts\n * const useEncrypted = createStorage({\n * get: (key) => decrypt(localStorage.getItem(key)),\n * set: (key, value) => localStorage.setItem(key, encrypt(value)),\n * remove: (key) => localStorage.removeItem(key),\n * })\n *\n * const secret = useEncrypted('api-key', '')\n * ```\n */\nexport function createStorage(\n backend: StorageBackend,\n backendName?: string,\n): <T>(key: string, defaultValue: T, options?: StorageOptions<T>) => StorageSignal<T> {\n const name = backendName ?? 'custom'\n\n return function useCustomStorage<T>(\n key: string,\n defaultValue: T,\n options?: StorageOptions<T>,\n ): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>(name, key)\n if (existing) return existing.signal\n\n // Read initial value\n let initialValue = defaultValue\n try {\n const raw = backend.get(key)\n if (raw !== null) {\n initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)\n }\n } catch {\n // Backend read failed — use default\n }\n\n const sig = signal<T>(initialValue)\n\n // Build the storage signal\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n storageSig.set = (value: T) => {\n sig.set(value)\n try {\n backend.set(key, serialize(value, options?.serializer))\n } catch {\n // Write failed — signal still updates\n }\n }\n\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n storageSig.remove = () => {\n sig.set(defaultValue)\n try {\n backend.remove(key)\n } catch {\n // Remove failed\n }\n removeEntry(name, key)\n }\n\n setEntry(name, key, storageSig, defaultValue)\n\n return storageSig\n }\n}\n\n// ─── Memory storage ──────────────────────────────────────────────────────────\n\n/**\n * In-memory storage backend. Useful for SSR, testing, or ephemeral state.\n * Values are lost on page unload.\n *\n * @example\n * ```ts\n * import { useMemoryStorage } from '@pyreon/storage'\n *\n * const temp = useMemoryStorage('key', 'default')\n * ```\n */\nexport const useMemoryStorage = createStorage(\n (() => {\n const store = new Map<string, string>()\n return {\n get: (key: string) => store.get(key) ?? null,\n set: (key: string, value: string) => store.set(key, value),\n remove: (key: string) => store.delete(key),\n }\n })(),\n 'memory',\n)\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { IndexedDBOptions, StorageSignal } from './types'\nimport { deserialize, isBrowser, serialize } from './utils'\n\n// ─── Database management ─────────────────────────────────────────────────────\n\nconst dbCache = new Map<string, Promise<IDBDatabase>>()\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n if (typeof indexedDB === 'undefined') {\n return Promise.reject(new Error('[Pyreon] indexedDB is not available in this environment'))\n }\n const cacheKey = `${dbName}:${storeName}`\n const cached = dbCache.get(cacheKey)\n if (cached) return cached\n\n const promise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName)\n }\n }\n\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n\n dbCache.set(cacheKey, promise)\n return promise\n}\n\nfunction idbGet(db: IDBDatabase, storeName: string, key: string): Promise<string | null> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeName, 'readonly')\n const store = tx.objectStore(storeName)\n const request = store.get(key)\n request.onsuccess = () =>\n resolve(request.result !== undefined ? (request.result as string) : null)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbSet(db: IDBDatabase, storeName: string, key: string, value: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const store = tx.objectStore(storeName)\n const request = store.put(value, key)\n request.onsuccess = () => resolve()\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbDelete(db: IDBDatabase, storeName: string, key: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeName, 'readwrite')\n const store = tx.objectStore(storeName)\n const request = store.delete(key)\n request.onsuccess = () => resolve()\n request.onerror = () => reject(request.error)\n })\n}\n\n// ─── useIndexedDB ────────────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by IndexedDB. Suitable for large or structured\n * data that exceeds localStorage limits. Writes are debounced.\n *\n * The signal starts with `defaultValue` and updates asynchronously\n * when the stored value is read from IndexedDB.\n *\n * @example\n * ```ts\n * const draft = useIndexedDB('article-draft', { title: '', body: '' })\n * draft() // { title: '', body: '' } initially, then stored value\n * draft.set({ title: 'My Post', body: '...' }) // signal updates immediately, IDB write is debounced\n * ```\n */\nexport function useIndexedDB<T>(\n key: string,\n defaultValue: T,\n options: IndexedDBOptions<T> = {},\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('indexeddb', key)\n if (existing) return existing.signal\n\n const dbName = options.dbName ?? 'pyreon-storage'\n const storeName = options.storeName ?? 'kv'\n const debounceMs = options.debounceMs ?? 100\n\n const sig = signal<T>(defaultValue)\n\n // Async initial load\n if (isBrowser() && typeof indexedDB !== 'undefined') {\n openDB(dbName, storeName)\n .then((db) => idbGet(db, storeName, key))\n .then((raw) => {\n if (raw !== null) {\n const value = deserialize(raw, defaultValue, options.deserializer, options.onError)\n sig.set(value)\n }\n })\n .catch(() => {\n // IndexedDB not available — signal keeps defaultValue\n })\n }\n\n // Debounced write\n let writeTimer: ReturnType<typeof setTimeout> | null = null\n let pendingValue: T | undefined\n\n function flushWrite(): void {\n if (pendingValue === undefined) return\n const value = pendingValue\n pendingValue = undefined\n\n if (!isBrowser() || typeof indexedDB === 'undefined') return\n\n openDB(dbName, storeName)\n .then((db) => idbSet(db, storeName, key, serialize(value, options.serializer)))\n .catch(() => {\n // Write failed — signal still has the correct value\n })\n }\n\n function scheduleWrite(value: T): void {\n pendingValue = value\n if (writeTimer !== null) clearTimeout(writeTimer)\n writeTimer = setTimeout(flushWrite, debounceMs)\n }\n\n // Build the storage signal\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n storageSig.set = (value: T) => {\n sig.set(value)\n scheduleWrite(value)\n }\n\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n storageSig.remove = () => {\n sig.set(defaultValue)\n pendingValue = undefined\n if (writeTimer !== null) clearTimeout(writeTimer)\n\n if (isBrowser() && typeof indexedDB !== 'undefined') {\n openDB(dbName, storeName)\n .then((db) => idbDelete(db, storeName, key))\n .catch(() => {\n // Delete failed — signal already reset\n })\n }\n\n removeEntry('indexeddb', key)\n }\n\n setEntry('indexeddb', key, storageSig, defaultValue)\n\n return storageSig\n}\n\n/**\n * Reset the database cache. For testing only.\n */\nexport function _resetDBCache(): void {\n dbCache.clear()\n}\n","import { signal } from '@pyreon/reactivity'\nimport { getEntry, removeEntry, setEntry } from './registry'\nimport type { StorageOptions, StorageSignal } from './types'\nimport { deserialize, getWebStorage, isBrowser, serialize } from './utils'\n\n// ─── Cross-tab sync ──────────────────────────────────────────────────────────\n\n// Refcount the active localStorage signals so we can detach the `storage`\n// event listener when nothing subscribes anymore. Before the refcount, the\n// listener was attached on first `useStorage` and NEVER removed, leaking a\n// window-level handler across the lifetime of the page even after all\n// signals disposed via `.remove()`.\nlet activeCount = 0\nlet storageHandler: ((e: StorageEvent) => void) | null = null\n\nfunction onStorageEvent(e: StorageEvent): void {\n if (!e.key) return\n const entry = getEntry('local', e.key)\n if (!entry) return\n\n const newValue =\n e.newValue !== null ? deserialize(e.newValue, entry.defaultValue) : entry.defaultValue\n\n entry.signal.set(newValue)\n}\n\nfunction retainStorageListener(): void {\n if (!isBrowser()) return\n activeCount++\n if (storageHandler === null) {\n storageHandler = onStorageEvent\n window.addEventListener('storage', storageHandler)\n }\n}\n\n/**\n * Test-only: force-detach the cross-tab listener and reset the refcount.\n * Used in test teardown to keep `_resetRegistry` and listener state in sync.\n */\nexport function _resetStorageListener(): void {\n if (storageHandler !== null && isBrowser()) {\n window.removeEventListener('storage', storageHandler)\n }\n storageHandler = null\n activeCount = 0\n}\n\n/**\n * Release one refcount on the cross-tab listener. Detaches the window-level\n * handler when the count drops to zero. Called from `.remove()`.\n */\nexport function releaseStorageListener(): void {\n if (!isBrowser()) return\n if (activeCount === 0) return\n activeCount--\n if (activeCount === 0 && storageHandler !== null) {\n window.removeEventListener('storage', storageHandler)\n storageHandler = null\n }\n}\n\n// ─── useStorage ──────────────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by localStorage. Automatically syncs across\n * browser tabs via the native `storage` event.\n *\n * @example\n * ```ts\n * const theme = useStorage('theme', 'light')\n * theme() // 'light' (or stored value)\n * theme.set('dark') // updates signal + localStorage\n * theme.remove() // clears storage, resets to default\n * ```\n */\nexport function useStorage<T>(\n key: string,\n defaultValue: T,\n options?: StorageOptions<T>,\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('local', key)\n if (existing) return existing.signal\n\n const storage = getWebStorage('local')\n\n // Read initial value from storage\n let initialValue = defaultValue\n if (storage) {\n const raw = storage.getItem(key)\n if (raw !== null) {\n initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)\n }\n }\n\n const sig = signal<T>(initialValue)\n\n // Create the storage signal by extending the base signal\n const storageSig = createStorageSignal(sig, key, defaultValue, 'local', options)\n\n setEntry('local', key, storageSig, defaultValue)\n retainStorageListener()\n\n return storageSig\n}\n\n// ─── Storage Signal Factory ──────────────────────────────────────────────────\n\n/**\n * Wraps a base signal with storage persistence behavior.\n * Used by both useStorage and useSessionStorage.\n */\nexport function createStorageSignal<T>(\n sig: ReturnType<typeof signal<T>>,\n key: string,\n defaultValue: T,\n backend: 'local' | 'session',\n options?: StorageOptions<T>,\n): StorageSignal<T> {\n const storage = getWebStorage(backend)\n\n // The callable signal function (read)\n const storageSig = (() => sig()) as unknown as StorageSignal<T>\n\n // Delegate all signal methods\n storageSig.peek = () => sig.peek()\n storageSig.subscribe = (listener: () => void) => sig.subscribe(listener)\n storageSig.direct = (updater: () => void) => sig.direct(updater)\n storageSig.debug = () => sig.debug()\n\n Object.defineProperty(storageSig, 'label', {\n get: () => sig.label,\n set: (v: string | undefined) => {\n sig.label = v\n },\n })\n\n // Override set to persist\n storageSig.set = (value: T) => {\n sig.set(value)\n if (storage) {\n try {\n storage.setItem(key, serialize(value, options?.serializer))\n } catch {\n // Storage full or blocked — signal still updates\n }\n }\n }\n\n // Override update to persist\n storageSig.update = (fn: (current: T) => T) => {\n const newValue = fn(sig.peek())\n storageSig.set(newValue)\n }\n\n // Add remove method\n storageSig.remove = () => {\n sig.set(defaultValue)\n if (storage) {\n storage.removeItem(key)\n }\n removeEntry(backend, key)\n if (backend === 'local') {\n releaseStorageListener()\n }\n }\n\n return storageSig\n}\n","import { signal } from '@pyreon/reactivity'\nimport { createStorageSignal } from './local'\nimport { getEntry, setEntry } from './registry'\nimport type { StorageOptions, StorageSignal } from './types'\nimport { deserialize, getWebStorage } from './utils'\n\n// ─── useSessionStorage ───────────────────────────────────────────────────────\n\n/**\n * Reactive signal backed by sessionStorage. Scoped to the current\n * browser tab — does not sync across tabs.\n *\n * @example\n * ```ts\n * const step = useSessionStorage('wizard-step', 0)\n * step() // 0 (or stored value)\n * step.set(3) // updates signal + sessionStorage\n * step.remove() // clears storage, resets to default\n * ```\n */\nexport function useSessionStorage<T>(\n key: string,\n defaultValue: T,\n options?: StorageOptions<T>,\n): StorageSignal<T> {\n // Return existing signal if already registered\n const existing = getEntry<T>('session', key)\n if (existing) return existing.signal\n\n const storage = getWebStorage('session')\n\n // Read initial value from storage\n let initialValue = defaultValue\n if (storage) {\n const raw = storage.getItem(key)\n if (raw !== null) {\n initialValue = deserialize(raw, defaultValue, options?.deserializer, options?.onError)\n }\n }\n\n const sig = signal<T>(initialValue)\n const storageSig = createStorageSignal(sig, key, defaultValue, 'session', options)\n\n setEntry('session', key, storageSig, defaultValue)\n\n return storageSig\n}\n","import { getEntriesByBackend, getEntry, removeEntry } from './registry'\nimport { getWebStorage, isBrowser } from './utils'\n\n// ─── Storage type mapping ────────────────────────────────────────────────────\n\ntype StorageType = 'local' | 'session' | 'cookie' | 'indexeddb' | 'all'\n\n// ─── removeStorage ───────────────────────────────────────────────────────────\n\n/**\n * Remove a specific key from storage and reset its signal to the default value.\n *\n * @example\n * ```ts\n * removeStorage('theme') // from localStorage\n * removeStorage('step', { type: 'session' }) // from sessionStorage\n * removeStorage('locale', { type: 'cookie' }) // deletes cookie\n * ```\n */\nexport function removeStorage(\n key: string,\n options?: { type?: 'local' | 'session' | 'cookie' | 'indexeddb' },\n): void {\n const type = options?.type ?? 'local'\n const entry = getEntry(type, key)\n\n if (entry) {\n entry.signal.remove()\n } else {\n // No signal registered — still try to clear the raw storage\n if (type === 'local' || type === 'session') {\n const storage = getWebStorage(type)\n if (storage) storage.removeItem(key)\n } else if (type === 'cookie' && isBrowser()) {\n document.cookie = `${encodeURIComponent(key)}=; max-age=0; path=/`\n }\n removeEntry(type, key)\n }\n}\n\n// ─── clearStorage ────────────────────────────────────────────────────────────\n\n/**\n * Clear all managed storage entries for a specific backend, or all backends.\n *\n * @example\n * ```ts\n * clearStorage() // clear all localStorage entries managed by @pyreon/storage\n * clearStorage('session') // clear all sessionStorage entries\n * clearStorage('cookie') // clear all managed cookies\n * clearStorage('all') // clear everything\n * ```\n */\nexport function clearStorage(type: StorageType = 'local'): void {\n if (type === 'all') {\n clearBackend('local')\n clearBackend('session')\n clearBackend('cookie')\n clearBackend('indexeddb')\n return\n }\n\n clearBackend(type)\n}\n\nfunction clearBackend(type: string): void {\n const entries = getEntriesByBackend(type)\n for (const entry of entries) {\n entry.signal.remove()\n }\n}\n"],"mappings":"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"}
@@ -153,6 +153,11 @@ declare function useIndexedDB<T>(key: string, defaultValue: T, options?: Indexed
153
153
  declare function _resetDBCache(): void;
154
154
  //#endregion
155
155
  //#region src/local.d.ts
156
+ /**
157
+ * Test-only: force-detach the cross-tab listener and reset the refcount.
158
+ * Used in test teardown to keep `_resetRegistry` and listener state in sync.
159
+ */
160
+ declare function _resetStorageListener(): void;
156
161
  /**
157
162
  * Reactive signal backed by localStorage. Automatically syncs across
158
163
  * browser tabs via the native `storage` event.
@@ -216,5 +221,5 @@ declare function clearStorage(type?: StorageType): void;
216
221
  */
217
222
  declare function _resetRegistry(): void;
218
223
  //#endregion
219
- export { type AsyncStorageBackend, type CookieOptions, type IndexedDBOptions, type StorageBackend, type StorageOptions, type StorageSignal, _resetDBCache, _resetRegistry, clearStorage, createStorage, removeStorage, setCookieSource, useCookie, useIndexedDB, useMemoryStorage, useSessionStorage, useStorage };
224
+ export { type AsyncStorageBackend, type CookieOptions, type IndexedDBOptions, type StorageBackend, type StorageOptions, type StorageSignal, _resetDBCache, _resetRegistry, _resetStorageListener, clearStorage, createStorage, removeStorage, setCookieSource, useCookie, useIndexedDB, useMemoryStorage, useSessionStorage, useStorage };
220
225
  //# sourceMappingURL=index2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/cookie.ts","../../../src/custom.ts","../../../src/indexed-db.ts","../../../src/local.ts","../../../src/session.ts","../../../src/clear.ts","../../../src/registry.ts"],"mappings":";;;;;AAQA;;UAAiB,aAAA,YAAyB,MAAA,CAAO,CAAA;EAAD;EAE9C,MAAA;AAAA;;;;UAQe,cAAA;EAAA;EAEf,UAAA,IAAc,KAAA,EAAO,CAAA;EAFQ;EAI7B,YAAA,IAAgB,GAAA,aAAgB,CAAA;EAAA;EAEhC,OAAA,IAAW,KAAA,EAAO,KAAA,KAAU,CAAA;AAAA;;;;UAQb,aAAA,YAAyB,cAAA,CAAe,CAAA;EAZlC;EAcrB,MAAA;EAZA;EAcA,OAAA,GAAU,IAAA;EAdsB;EAgBhC,IAAA;EAdkB;EAgBlB,MAAA;EAhB4B;EAkB5B,MAAA;EAlB6B;EAoB7B,QAAA;AAAA;;;;UAQe,gBAAA,YAA4B,cAAA,CAAe,CAAA;EApBJ;EAsBtD,MAAA;EAtB6B;EAwB7B,SAAA;EAxBuD;EA0BvD,UAAA;AAAA;;;;UAQe,cAAA;EAtBf;EAwBA,GAAA,CAAI,GAAA;EAxBI;EA0BR,GAAA,CAAI,GAAA,UAAa,KAAA;EAlBc;EAoB/B,MAAA,CAAO,GAAA;AAAA;;;;UAMQ,mBAAA;EAtBf;EAwBA,GAAA,CAAI,GAAA,WAAc,OAAA;EAtBR;EAwBV,GAAA,CAAI,GAAA,UAAa,KAAA,WAAgB,OAAA;EAhBlB;EAkBf,MAAA,CAAO,GAAA,WAAc,OAAA;AAAA;;;;;AA5EvB;;;;;;;;iBCWgB,eAAA,CAAgB,YAAA;;ADDhC;;;;;;;;;;;;;;;iBCwFgB,SAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAS,aAAA,CAAc,CAAA,IACtB,aAAA,CAAc,CAAA;;;;;ADtGjB;;;;;;;;;;AAUA;;;iBEIgB,aAAA,CACd,OAAA,EAAS,cAAA,EACT,WAAA,gBACK,GAAA,UAAa,YAAA,EAAc,CAAA,EAAG,OAAA,GAAU,cAAA,CAAe,CAAA,MAAO,aAAA,CAAc,CAAA;;;;;;;;;;;;cAmFtE,gBAAA,MAnFR,GAAA,UAAa,YAAA,EAAgB,CAAA,EAAC,OAAA,GAAY,cAAA,CAAe,CAAA,MAAO,aAAA,CAAc,CAAA;;;;;AFjBnF;;;;;;;;;;AAUA;;iBG6DgB,YAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAS,gBAAA,CAAiB,CAAA,IACzB,aAAA,CAAc,CAAA;;;;iBAmGD,aAAA,CAAA;;;;AH9KhB;;;;;;;;;;AAUA;iBIqBgB,UAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAU,cAAA,CAAe,CAAA,IACxB,aAAA,CAAc,CAAA;;;;;AJnCjB;;;;;;;;;;iBKYgB,iBAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAU,cAAA,CAAe,CAAA,IACxB,aAAA,CAAc,CAAA;;;KCnBZ,WAAA;;;ANGL;;;;;;;;iBMWgB,aAAA,CACd,GAAA,UACA,OAAA;EAAY,IAAA;AAAA;;;;;;;;;;;;iBAgCE,YAAA,CAAa,IAAA,GAAM,WAAA;;;;;;iBCOnB,cAAA,CAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/cookie.ts","../../../src/custom.ts","../../../src/indexed-db.ts","../../../src/local.ts","../../../src/session.ts","../../../src/clear.ts","../../../src/registry.ts"],"mappings":";;;;;AAQA;;UAAiB,aAAA,YAAyB,MAAA,CAAO,CAAA;EAAD;EAE9C,MAAA;AAAA;;;;UAQe,cAAA;EAAA;EAEf,UAAA,IAAc,KAAA,EAAO,CAAA;EAFQ;EAI7B,YAAA,IAAgB,GAAA,aAAgB,CAAA;EAAA;EAEhC,OAAA,IAAW,KAAA,EAAO,KAAA,KAAU,CAAA;AAAA;;;;UAQb,aAAA,YAAyB,cAAA,CAAe,CAAA;EAZlC;EAcrB,MAAA;EAZA;EAcA,OAAA,GAAU,IAAA;EAdsB;EAgBhC,IAAA;EAdkB;EAgBlB,MAAA;EAhB4B;EAkB5B,MAAA;EAlB6B;EAoB7B,QAAA;AAAA;;;;UAQe,gBAAA,YAA4B,cAAA,CAAe,CAAA;EApBJ;EAsBtD,MAAA;EAtB6B;EAwB7B,SAAA;EAxBuD;EA0BvD,UAAA;AAAA;;;;UAQe,cAAA;EAtBf;EAwBA,GAAA,CAAI,GAAA;EAxBI;EA0BR,GAAA,CAAI,GAAA,UAAa,KAAA;EAlBc;EAoB/B,MAAA,CAAO,GAAA;AAAA;;;;UAMQ,mBAAA;EAtBf;EAwBA,GAAA,CAAI,GAAA,WAAc,OAAA;EAtBR;EAwBV,GAAA,CAAI,GAAA,UAAa,KAAA,WAAgB,OAAA;EAhBlB;EAkBf,MAAA,CAAO,GAAA,WAAc,OAAA;AAAA;;;;;AA5EvB;;;;;;;;iBCWgB,eAAA,CAAgB,YAAA;;ADDhC;;;;;;;;;;;;;;;iBCwFgB,SAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAS,aAAA,CAAc,CAAA,IACtB,aAAA,CAAc,CAAA;;;;;ADtGjB;;;;;;;;;;AAUA;;;iBEIgB,aAAA,CACd,OAAA,EAAS,cAAA,EACT,WAAA,gBACK,GAAA,UAAa,YAAA,EAAc,CAAA,EAAG,OAAA,GAAU,cAAA,CAAe,CAAA,MAAO,aAAA,CAAc,CAAA;;;;;;;;;;;;cAmFtE,gBAAA,MAnFR,GAAA,UAAa,YAAA,EAAgB,CAAA,EAAC,OAAA,GAAY,cAAA,CAAe,CAAA,MAAO,aAAA,CAAc,CAAA;;;;;AFjBnF;;;;;;;;;;AAUA;;iBGgEgB,YAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAS,gBAAA,CAAiB,CAAA,IACzB,aAAA,CAAc,CAAA;;;;iBAmGD,aAAA,CAAA;;;;AHjLhB;;;iBI+BgB,qBAAA,CAAA;;AJrBhB;;;;;;;;;;;iBIyDgB,UAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAU,cAAA,CAAe,CAAA,IACxB,aAAA,CAAc,CAAA;;;;;AJvEjB;;;;;;;;;;iBKYgB,iBAAA,GAAA,CACd,GAAA,UACA,YAAA,EAAc,CAAA,EACd,OAAA,GAAU,cAAA,CAAe,CAAA,IACxB,aAAA,CAAc,CAAA;;;KCnBZ,WAAA;;;ANGL;;;;;;;;iBMWgB,aAAA,CACd,GAAA,UACA,OAAA;EAAY,IAAA;AAAA;;;;;;;;;;;;iBAgCE,YAAA,CAAa,IAAA,GAAM,WAAA;;;;;;iBCOnB,cAAA,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/storage",
3
- "version": "0.12.13",
3
+ "version": "0.12.14",
4
4
  "description": "Reactive client-side storage for Pyreon — localStorage, sessionStorage, cookies, IndexedDB",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/storage#readme",
6
6
  "bugs": {
@@ -42,10 +42,10 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@happy-dom/global-registrator": "^20.8.9",
45
- "@pyreon/reactivity": "^0.12.13",
45
+ "@pyreon/reactivity": "^0.12.14",
46
46
  "@vitus-labs/tools-lint": "^1.15.5"
47
47
  },
48
48
  "peerDependencies": {
49
- "@pyreon/reactivity": "^0.12.13"
49
+ "@pyreon/reactivity": "^0.12.14"
50
50
  }
51
51
  }
package/src/index.ts CHANGED
@@ -48,4 +48,5 @@ export type {
48
48
  // ─── Testing ─────────────────────────────────────────────────────────────────
49
49
 
50
50
  export { _resetDBCache } from './indexed-db'
51
+ export { _resetStorageListener } from './local'
51
52
  export { _resetRegistry } from './registry'
package/src/indexed-db.ts CHANGED
@@ -8,6 +8,9 @@ import { deserialize, isBrowser, serialize } from './utils'
8
8
  const dbCache = new Map<string, Promise<IDBDatabase>>()
9
9
 
10
10
  function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
11
+ if (typeof indexedDB === 'undefined') {
12
+ return Promise.reject(new Error('[Pyreon] indexedDB is not available in this environment'))
13
+ }
11
14
  const cacheKey = `${dbName}:${storeName}`
12
15
  const cached = dbCache.get(cacheKey)
13
16
  if (cached) return cached
package/src/local.ts CHANGED
@@ -5,22 +5,58 @@ import { deserialize, getWebStorage, isBrowser, serialize } from './utils'
5
5
 
6
6
  // ─── Cross-tab sync ──────────────────────────────────────────────────────────
7
7
 
8
- let listenerAttached = false
9
-
10
- function attachStorageListener(): void {
11
- if (listenerAttached || !isBrowser()) return
12
- listenerAttached = true
8
+ // Refcount the active localStorage signals so we can detach the `storage`
9
+ // event listener when nothing subscribes anymore. Before the refcount, the
10
+ // listener was attached on first `useStorage` and NEVER removed, leaking a
11
+ // window-level handler across the lifetime of the page even after all
12
+ // signals disposed via `.remove()`.
13
+ let activeCount = 0
14
+ let storageHandler: ((e: StorageEvent) => void) | null = null
15
+
16
+ function onStorageEvent(e: StorageEvent): void {
17
+ if (!e.key) return
18
+ const entry = getEntry('local', e.key)
19
+ if (!entry) return
20
+
21
+ const newValue =
22
+ e.newValue !== null ? deserialize(e.newValue, entry.defaultValue) : entry.defaultValue
23
+
24
+ entry.signal.set(newValue)
25
+ }
13
26
 
14
- window.addEventListener('storage', (e) => {
15
- if (!e.key) return
16
- const entry = getEntry('local', e.key)
17
- if (!entry) return
27
+ function retainStorageListener(): void {
28
+ if (!isBrowser()) return
29
+ activeCount++
30
+ if (storageHandler === null) {
31
+ storageHandler = onStorageEvent
32
+ window.addEventListener('storage', storageHandler)
33
+ }
34
+ }
18
35
 
19
- const newValue =
20
- e.newValue !== null ? deserialize(e.newValue, entry.defaultValue) : entry.defaultValue
36
+ /**
37
+ * Test-only: force-detach the cross-tab listener and reset the refcount.
38
+ * Used in test teardown to keep `_resetRegistry` and listener state in sync.
39
+ */
40
+ export function _resetStorageListener(): void {
41
+ if (storageHandler !== null && isBrowser()) {
42
+ window.removeEventListener('storage', storageHandler)
43
+ }
44
+ storageHandler = null
45
+ activeCount = 0
46
+ }
21
47
 
22
- entry.signal.set(newValue)
23
- })
48
+ /**
49
+ * Release one refcount on the cross-tab listener. Detaches the window-level
50
+ * handler when the count drops to zero. Called from `.remove()`.
51
+ */
52
+ export function releaseStorageListener(): void {
53
+ if (!isBrowser()) return
54
+ if (activeCount === 0) return
55
+ activeCount--
56
+ if (activeCount === 0 && storageHandler !== null) {
57
+ window.removeEventListener('storage', storageHandler)
58
+ storageHandler = null
59
+ }
24
60
  }
25
61
 
26
62
  // ─── useStorage ──────────────────────────────────────────────────────────────
@@ -63,7 +99,7 @@ export function useStorage<T>(
63
99
  const storageSig = createStorageSignal(sig, key, defaultValue, 'local', options)
64
100
 
65
101
  setEntry('local', key, storageSig, defaultValue)
66
- attachStorageListener()
102
+ retainStorageListener()
67
103
 
68
104
  return storageSig
69
105
  }
@@ -124,6 +160,9 @@ export function createStorageSignal<T>(
124
160
  storage.removeItem(key)
125
161
  }
126
162
  removeEntry(backend, key)
163
+ if (backend === 'local') {
164
+ releaseStorageListener()
165
+ }
127
166
  }
128
167
 
129
168
  return storageSig
@@ -0,0 +1,65 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { _resetRegistry, _resetStorageListener, useStorage } from '../index'
3
+
4
+ /**
5
+ * Regression for the cross-tab `storage` listener leak. Before the refcount,
6
+ * the listener was attached on the first `useStorage` call and NEVER
7
+ * removed — even after every signal was disposed via `.remove()`. The
8
+ * listener stayed bound to `window` for the lifetime of the page.
9
+ *
10
+ * With the refcount, `.remove()` decrements and the listener detaches at 0.
11
+ * A later `useStorage` reattaches cleanly.
12
+ */
13
+ describe('useStorage — cross-tab listener refcount (regression)', () => {
14
+ let addSpy: ReturnType<typeof vi.spyOn>
15
+ let removeSpy: ReturnType<typeof vi.spyOn>
16
+
17
+ beforeEach(() => {
18
+ localStorage.clear()
19
+ _resetStorageListener()
20
+ _resetRegistry()
21
+ addSpy = vi.spyOn(window, 'addEventListener')
22
+ removeSpy = vi.spyOn(window, 'removeEventListener')
23
+ })
24
+
25
+ afterEach(() => {
26
+ addSpy.mockRestore()
27
+ removeSpy.mockRestore()
28
+ localStorage.clear()
29
+ _resetStorageListener()
30
+ _resetRegistry()
31
+ })
32
+
33
+ const countCalls = (spy: ReturnType<typeof vi.spyOn>, event: string): number =>
34
+ spy.mock.calls.filter((c: unknown[]) => c[0] === event).length
35
+
36
+ it('attaches the storage listener exactly once when multiple signals exist', () => {
37
+ useStorage('a', 0)
38
+ useStorage('b', 0)
39
+ expect(countCalls(addSpy, 'storage')).toBe(1)
40
+ })
41
+
42
+ it('detaches the storage listener when every signal is disposed', () => {
43
+ const a = useStorage('a', 0)
44
+ const b = useStorage('b', 0)
45
+ expect(countCalls(addSpy, 'storage')).toBe(1)
46
+ expect(countCalls(removeSpy, 'storage')).toBe(0)
47
+
48
+ a.remove()
49
+ expect(countCalls(removeSpy, 'storage')).toBe(0) // still one retained
50
+
51
+ b.remove()
52
+ expect(countCalls(removeSpy, 'storage')).toBe(1) // now detached
53
+ })
54
+
55
+ it('reattaches cleanly when a new signal is created after full disposal', () => {
56
+ const a = useStorage('a', 0)
57
+ a.remove()
58
+ expect(countCalls(addSpy, 'storage')).toBe(1)
59
+ expect(countCalls(removeSpy, 'storage')).toBe(1)
60
+
61
+ useStorage('b', 0)
62
+ expect(countCalls(addSpy, 'storage')).toBe(2) // reattached
63
+ expect(countCalls(removeSpy, 'storage')).toBe(1)
64
+ })
65
+ })