{"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Ctx, ReadSignal } from '@kontsedal/olas-core'\r\nimport { signal } from '@kontsedal/olas-core'\r\n\r\nexport type StorageAdapter = {\r\n  get(key: string): string | null | Promise<string | null>\r\n  set(key: string, value: string): void | Promise<void>\r\n  delete(key: string): void | Promise<void>\r\n  onChange?(handler: (key: string, value: string | null) => void): () => void\r\n  /**\r\n   * Optional — list every key currently in storage. Consumers that need to\r\n   * enumerate keys (e.g. `@kontsedal/olas-mutation-queue` replaying the\r\n   * pending queue on init) require this extension; consumers that only\r\n   * `get` / `set` known keys (the typical `usePersisted` shape) don't need\r\n   * it. Both built-in adapters (`localStorageAdapter`, `indexedDbAdapter`)\r\n   * implement it.\r\n   */\r\n  keys?(): Iterable<string> | Promise<Iterable<string>>\r\n}\r\n\r\n/**\r\n * Where a `PersistOptions.onError` fired. Distinguishes the failing operation\r\n * for routing (e.g. quota-exceeded vs schema-migration-failed vs\r\n * deserialization-corrupted).\r\n */\r\nexport type PersistErrorOp =\r\n  | 'load'\r\n  | 'deserialize'\r\n  | 'serialize'\r\n  | 'write'\r\n  | 'migrate'\r\n  | 'remoteChange'\r\n\r\nexport type PersistOptions<T> = {\r\n  /**\r\n   * Storage backend. When omitted *or explicitly `undefined`* (handy for app\r\n   * code that forwards a deps slot like `ctx.deps.storage`), the browser\r\n   * `localStorageAdapter` is used. SSR-safe — `localStorageAdapter` no-ops\r\n   * when `localStorage` isn't defined.\r\n   */\r\n  storage?: StorageAdapter | undefined\r\n  serialize?: (value: T) => string\r\n  deserialize?: (raw: string) => T\r\n  crossTab?: boolean\r\n  /**\r\n   * Schema version. When the value loaded from storage carries a different\r\n   * `version`, `migrate(raw, fromVersion)` is invoked to bring it forward;\r\n   * the migrated value is written back atomically. When omitted, no version\r\n   * gate runs — payloads are read and written raw (current default).\r\n   *\r\n   * The on-disk shape with versioning enabled is `{\"v\": N, \"d\": <serialized>}`\r\n   * — `usePersisted` wraps every write and reads both shapes (legacy raw and\r\n   * versioned). Versioned writes only happen once `version` is set.\r\n   */\r\n  version?: number\r\n  /**\r\n   * Migrate a raw payload of a prior version. Receives the pre-deserialize\r\n   * string and the version number it was written with (or `undefined` if no\r\n   * version stamp existed, i.e. the legacy raw shape). Return the migrated\r\n   * payload AS A `T` value (post-deserialize); `usePersisted` re-serializes\r\n   * it before writing. Return `undefined` to drop the entry (the source\r\n   * keeps its current value).\r\n   */\r\n  migrate?: (raw: string, fromVersion: number | undefined) => T | undefined | Promise<T | undefined>\r\n  /**\r\n   * Debounce writes by `throttleMs` milliseconds. Useful for high-frequency\r\n   * sources (cursor position, scroll, every-keystroke field) where the\r\n   * default \"write on every change\" is too chatty. Defaults to `0` (no\r\n   * debounce). On `ctx.onDispose`, any pending write is flushed.\r\n   */\r\n  throttleMs?: number\r\n  /**\r\n   * Routed errors from every fallible op: storage `get`/`set` (quota,\r\n   * security, version-conflict), `deserialize`/`serialize` (corrupt JSON,\r\n   * non-serializable T), `migrate` (user-thrown), and `onChange` callbacks\r\n   * (cross-tab payload corruption). Without this, errors are swallowed —\r\n   * matches the historical behavior, but production apps want at least a\r\n   * sentry/console hook.\r\n   */\r\n  onError?: (err: unknown, op: PersistErrorOp, key: string) => void\r\n}\r\n\r\nexport type Persisted = {\r\n  ready: ReadSignal<boolean>\r\n}\r\n\r\nexport type PersistableSource<T> = {\r\n  readonly value: T\r\n  set(value: T): void\r\n  subscribe(handler: (value: T) => void): () => void\r\n}\r\n\r\n/**\r\n * Configuration for `indexedDbAdapter`. All fields optional; sane defaults\r\n * picked for typical app use.\r\n */\r\nexport type IndexedDbAdapterOptions = {\r\n  /** Database name. Defaults to `'olas-persist'`. */\r\n  databaseName?: string\r\n  /** Object store inside the database. Defaults to `'kv'`. */\r\n  storeName?: string\r\n  /**\r\n   * `BroadcastChannel` name used to notify other tabs of writes through this\r\n   * adapter (so `onChange` works cross-tab — IDB itself has no built-in\r\n   * change event). Defaults to `'olas-persist:' + databaseName + '/' +\r\n   * storeName`. Set to `null` to disable cross-tab notifications.\r\n   */\r\n  channelName?: string | null\r\n  /**\r\n   * Override the `IDBFactory` — defaults to `globalThis.indexedDB`. Useful\r\n   * for testing (inject a fake) or runtimes that ship their own IDB\r\n   * implementation. When undefined and no global `indexedDB`, the adapter\r\n   * no-ops (SSR-safe).\r\n   */\r\n  indexedDB?: IDBFactory\r\n  /**\r\n   * Override the `BroadcastChannel` constructor. Defaults to\r\n   * `globalThis.BroadcastChannel`. When undefined and no global, `onChange`\r\n   * subscriptions still register but never fire.\r\n   */\r\n  broadcastChannel?: typeof BroadcastChannel\r\n}\r\n\r\n/**\r\n * IndexedDB-backed `StorageAdapter`. Async on every operation; cross-tab\r\n * change notifications layered via `BroadcastChannel` (IDB has no native\r\n * change event, so external IDB writes by code that doesn't go through\r\n * this adapter are *not* observed). When no `IDBFactory` is available\r\n * (SSR, restricted environments), every method resolves to a no-op.\r\n *\r\n * Storage is a single key/value object store inside a single database;\r\n * fine for the persisted-signal use case `usePersisted` is built around.\r\n * For larger or schema-shaped data, write a custom adapter against your\r\n * own IDB layout.\r\n */\r\nexport function indexedDbAdapter(options?: IndexedDbAdapterOptions): StorageAdapter {\r\n  const dbName = options?.databaseName ?? 'olas-persist'\r\n  const storeName = options?.storeName ?? 'kv'\r\n  const idbFactory = options?.indexedDB ?? getGlobalIndexedDb()\r\n  const bcCtor = options?.broadcastChannel ?? getGlobalBroadcastChannel()\r\n  const channelName =\r\n    options?.channelName === null\r\n      ? null\r\n      : (options?.channelName ?? `olas-persist:${dbName}/${storeName}`)\r\n\r\n  let dbPromise: Promise<IDBDatabase> | null = null\r\n  let channel: BroadcastChannel | null = null\r\n\r\n  const ensureChannel = (): BroadcastChannel | null => {\r\n    if (channel !== null) return channel\r\n    if (bcCtor === undefined || channelName === null) return null\r\n    try {\r\n      channel = new bcCtor(channelName)\r\n      return channel\r\n    } catch {\r\n      return null\r\n    }\r\n  }\r\n\r\n  const openDb = (): Promise<IDBDatabase> | null => {\r\n    if (idbFactory === undefined) return null\r\n    if (dbPromise !== null) return dbPromise\r\n    dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\r\n      const req = idbFactory.open(dbName, 1)\r\n      req.onupgradeneeded = () => {\r\n        const db = req.result\r\n        if (!db.objectStoreNames.contains(storeName)) {\r\n          db.createObjectStore(storeName)\r\n        }\r\n      }\r\n      req.onsuccess = () => {\r\n        const db = req.result\r\n        // Without this, holding the connection open BLOCKS another tab that\r\n        // wants to upgrade or `deleteDatabase` — a permanent silent stall.\r\n        // Close ours and drop the cached promise so the next op re-opens; a\r\n        // failed re-open then REJECTS and routes through the caller's onError\r\n        // instead of no-oping forever (T6.1).\r\n        db.onversionchange = () => {\r\n          db.close()\r\n          dbPromise = null\r\n        }\r\n        resolve(db)\r\n      }\r\n      req.onerror = () => reject(req.error ?? new Error('[olas-persist] IDB open failed'))\r\n    })\r\n    // Lazy connection — if the open fails, future calls retry rather than\r\n    // staying stuck on a poisoned promise.\r\n    dbPromise.catch(() => {\r\n      dbPromise = null\r\n    })\r\n    return dbPromise\r\n  }\r\n\r\n  const runRequest = async <T>(\r\n    mode: IDBTransactionMode,\r\n    build: (store: IDBObjectStore) => IDBRequest<T>,\r\n  ): Promise<T | undefined> => {\r\n    const db = await openDb()\r\n    if (db === null) return undefined\r\n    return new Promise<T | undefined>((resolve, reject) => {\r\n      let settled = false\r\n      const fail = (err: unknown): void => {\r\n        if (settled) return\r\n        settled = true\r\n        reject(err ?? new Error('[olas-persist] IDB request failed'))\r\n      }\r\n      const tx = db.transaction(storeName, mode)\r\n      const store = tx.objectStore(storeName)\r\n      const req = build(store)\r\n      let result: T | undefined\r\n      // Capture the request's result on success, but resolve on the\r\n      // TRANSACTION's commit — a write's `req.onsuccess` fires before the data\r\n      // is durably committed, so quota / disk failures only surface as a\r\n      // `tx.onabort` at commit time. Resolving on `req.onsuccess` (the old\r\n      // behavior) acked writes that never landed (T6.1).\r\n      req.onsuccess = () => {\r\n        result = req.result\r\n      }\r\n      req.onerror = () => fail(req.error)\r\n      tx.oncomplete = () => {\r\n        if (settled) return\r\n        settled = true\r\n        resolve(result)\r\n      }\r\n      tx.onabort = () => fail(tx.error)\r\n      tx.onerror = () => fail(tx.error)\r\n    })\r\n  }\r\n\r\n  return {\r\n    async get(key: string): Promise<string | null> {\r\n      if (idbFactory === undefined) return null\r\n      // A real read error (db closed, corrupt store) REJECTS so the caller's\r\n      // error routing runs (`usePersisted` → `onError('load')`). A missing key\r\n      // is not an error — `req.result` is `undefined`, so we return null.\r\n      const result = await runRequest<unknown>('readonly', (s) => s.get(key))\r\n      return typeof result === 'string' ? result : null\r\n    },\r\n    async set(key: string, value: string): Promise<void> {\r\n      if (idbFactory === undefined) return\r\n      // Do NOT swallow — a rejected write (quota, closed db, aborted commit)\r\n      // propagates so `usePersisted`'s `onError('write')` fires (T6.1). The\r\n      // cross-tab broadcast only runs once the commit actually lands.\r\n      await runRequest('readwrite', (s) => s.put(value, key))\r\n      ensureChannel()?.postMessage({ key, value })\r\n    },\r\n    async delete(key: string): Promise<void> {\r\n      if (idbFactory === undefined) return\r\n      await runRequest('readwrite', (s) => s.delete(key))\r\n      ensureChannel()?.postMessage({ key, value: null })\r\n    },\r\n    onChange(handler: (key: string, value: string | null) => void): () => void {\r\n      const ch = ensureChannel()\r\n      if (ch === null) return () => {}\r\n      const listener = (event: MessageEvent<{ key: string; value: string | null }>) => {\r\n        try {\r\n          handler(event.data.key, event.data.value)\r\n        } catch {\r\n          /* swallow — onChange handlers shouldn't take down the adapter */\r\n        }\r\n      }\r\n      ch.addEventListener('message', listener)\r\n      return () => ch.removeEventListener('message', listener)\r\n    },\r\n    async keys(): Promise<string[]> {\r\n      if (idbFactory === undefined) return []\r\n      try {\r\n        const result = await runRequest<IDBValidKey[]>('readonly', (s) => s.getAllKeys())\r\n        if (!Array.isArray(result)) return []\r\n        return result.filter((k): k is string => typeof k === 'string')\r\n      } catch {\r\n        return []\r\n      }\r\n    },\r\n  }\r\n}\r\n\r\nfunction getGlobalIndexedDb(): IDBFactory | undefined {\r\n  return typeof indexedDB === 'undefined' ? undefined : indexedDB\r\n}\r\n\r\nfunction getGlobalBroadcastChannel(): typeof BroadcastChannel | undefined {\r\n  return typeof BroadcastChannel === 'undefined' ? undefined : BroadcastChannel\r\n}\r\n\r\n/** Default localStorage adapter — only viable in the browser. */\r\nexport const localStorageAdapter: StorageAdapter = {\r\n  get(key: string): string | null {\r\n    if (typeof localStorage === 'undefined') return null\r\n    return localStorage.getItem(key)\r\n  },\r\n  set(key: string, value: string): void {\r\n    if (typeof localStorage === 'undefined') return\r\n    localStorage.setItem(key, value)\r\n  },\r\n  delete(key: string): void {\r\n    if (typeof localStorage === 'undefined') return\r\n    localStorage.removeItem(key)\r\n  },\r\n  onChange(handler) {\r\n    if (typeof window === 'undefined') return () => {}\r\n    const listener = (event: StorageEvent) => {\r\n      if (event.key === null) return\r\n      handler(event.key, event.newValue)\r\n    }\r\n    window.addEventListener('storage', listener)\r\n    return () => window.removeEventListener('storage', listener)\r\n  },\r\n  keys(): string[] {\r\n    if (typeof localStorage === 'undefined') return []\r\n    const out: string[] = []\r\n    for (let i = 0; i < localStorage.length; i++) {\r\n      const k = localStorage.key(i)\r\n      if (k !== null) out.push(k)\r\n    }\r\n    return out\r\n  },\r\n}\r\n\r\n/**\r\n * Persist a signal-like source under `key`. Loads the stored value on\r\n * construction (sync for localStorage, async for any storage that returns a\r\n * promise). Subsequent writes to the source are mirrored to storage.\r\n *\r\n * Cleanup (unsubscribe + cross-tab listener removal) is bound to `ctx`.\r\n */\r\nexport function usePersisted<T>(\r\n  ctx: Ctx,\r\n  key: string,\r\n  source: PersistableSource<T>,\r\n  options?: PersistOptions<T>,\r\n): Persisted {\r\n  const storage = options?.storage ?? localStorageAdapter\r\n  const serialize = options?.serialize ?? JSON.stringify\r\n  const deserialize = options?.deserialize ?? JSON.parse\r\n  const crossTab = options?.crossTab ?? false\r\n  const version = options?.version\r\n  const migrate = options?.migrate\r\n  const throttleMs = options?.throttleMs ?? 0\r\n  const onError = options?.onError\r\n\r\n  const reportError = (err: unknown, op: PersistErrorOp): void => {\r\n    if (onError === undefined) return\r\n    try {\r\n      onError(err, op, key)\r\n    } catch {\r\n      /* an onError handler that itself throws is its own problem. */\r\n    }\r\n  }\r\n\r\n  const ready$ = signal(false)\r\n  let writingFromLoad = false\r\n  // Ready-gate race bookkeeping (T6.1). A source write or a cross-tab change\r\n  // that lands BEFORE the initial async load settles must not be lost or\r\n  // clobbered by `applyLoaded`. We remember the latest of each and reconcile\r\n  // once ready flips true (a local user write wins over both stored + remote).\r\n  let userWroteBeforeReady = false\r\n  let pendingUserValueBeforeReady: T | undefined\r\n  let hasPendingRemote = false\r\n  let pendingRemoteRaw: string | null = null\r\n\r\n  /**\r\n   * On-disk envelope when `version` is set: `{\"v\": N, \"d\": \"<serializedT>\"}`.\r\n   * Without `version`, we read/write raw (legacy shape). Migration takes the\r\n   * raw inner string + the parsed `v` (or `undefined` for legacy) so the\r\n   * consumer's migrator can replay arbitrary historical formats.\r\n   */\r\n  type Envelope = { v: number; d: string }\r\n  const isEnvelope = (raw: unknown): raw is Envelope =>\r\n    typeof raw === 'object' &&\r\n    raw !== null &&\r\n    typeof (raw as { v?: unknown }).v === 'number' &&\r\n    typeof (raw as { d?: unknown }).d === 'string'\r\n\r\n  const encodeForStorage = (value: T): string => {\r\n    const inner = serialize(value)\r\n    if (version === undefined) return inner\r\n    return JSON.stringify({ v: version, d: inner })\r\n  }\r\n\r\n  // Apply a cross-tab raw value to the source (a null → `undefined` delete;\r\n  // otherwise parse/deserialize, honoring the version envelope). Shared by the\r\n  // live `onChange` path and the buffered-until-ready replay (T6.1).\r\n  const applyRemote = (rawValue: string | null): void => {\r\n    if (rawValue == null) {\r\n      writingFromLoad = true\r\n      try {\r\n        source.set(undefined as T)\r\n      } finally {\r\n        writingFromLoad = false\r\n      }\r\n      return\r\n    }\r\n    try {\r\n      let parsed: unknown\r\n      try {\r\n        parsed = JSON.parse(rawValue)\r\n      } catch {\r\n        parsed = undefined\r\n      }\r\n      let value: T\r\n      if (version !== undefined && isEnvelope(parsed)) {\r\n        if (parsed.v !== version) return // peer on a different schema; ignore.\r\n        value = deserialize(parsed.d) as T\r\n      } else {\r\n        value = deserialize(rawValue) as T\r\n      }\r\n      writingFromLoad = true\r\n      try {\r\n        source.set(value)\r\n      } finally {\r\n        writingFromLoad = false\r\n      }\r\n    } catch (err) {\r\n      reportError(err, 'remoteChange')\r\n    }\r\n  }\r\n\r\n  // Flip `ready` and reconcile anything that raced the initial load: a local\r\n  // user write wins outright (and is flushed to storage); otherwise a buffered\r\n  // cross-tab change (the freshest one) is applied. `scheduleWrite` is only\r\n  // reached in the async-load path, where it is already defined below.\r\n  const settleReady = (): void => {\r\n    ready$.set(true)\r\n    if (userWroteBeforeReady) {\r\n      userWroteBeforeReady = false\r\n      hasPendingRemote = false\r\n      scheduleWrite(pendingUserValueBeforeReady as T)\r\n      return\r\n    }\r\n    if (hasPendingRemote) {\r\n      hasPendingRemote = false\r\n      applyRemote(pendingRemoteRaw)\r\n    }\r\n  }\r\n\r\n  // Load initial value.\r\n  const loaded = storage.get(key)\r\n  const applyLoaded = async (raw: string | null): Promise<void> => {\r\n    // A local write already raced the load — it wins; don't apply storage.\r\n    // `settleReady` flushes the user's value.\r\n    if (userWroteBeforeReady) {\r\n      settleReady()\r\n      return\r\n    }\r\n    if (raw == null) {\r\n      settleReady()\r\n      return\r\n    }\r\n    let value: T | undefined\r\n    let needsRewrite = false\r\n    try {\r\n      // Try the envelope shape first (for version-aware reads). If it isn't\r\n      // an envelope, treat the raw string as a legacy v=undefined payload.\r\n      let parsedEnvelope: unknown\r\n      try {\r\n        parsedEnvelope = JSON.parse(raw)\r\n      } catch {\r\n        parsedEnvelope = undefined\r\n      }\r\n      if (version !== undefined && isEnvelope(parsedEnvelope)) {\r\n        if (parsedEnvelope.v === version) {\r\n          value = deserialize(parsedEnvelope.d) as T\r\n        } else if (migrate !== undefined) {\r\n          try {\r\n            const migrated = await migrate(parsedEnvelope.d, parsedEnvelope.v)\r\n            if (migrated === undefined) {\r\n              settleReady()\r\n              return\r\n            }\r\n            value = migrated\r\n            needsRewrite = true\r\n          } catch (err) {\r\n            reportError(err, 'migrate')\r\n            settleReady()\r\n            return\r\n          }\r\n        } else {\r\n          // Version mismatch with no migrator — discard.\r\n          settleReady()\r\n          return\r\n        }\r\n      } else if (version !== undefined && migrate !== undefined) {\r\n        // Legacy raw payload but we now require versioning — invoke migrator\r\n        // with `fromVersion: undefined`.\r\n        try {\r\n          const migrated = await migrate(raw, undefined)\r\n          if (migrated === undefined) {\r\n            settleReady()\r\n            return\r\n          }\r\n          value = migrated\r\n          needsRewrite = true\r\n        } catch (err) {\r\n          reportError(err, 'migrate')\r\n          settleReady()\r\n          return\r\n        }\r\n      } else {\r\n        value = deserialize(raw) as T\r\n      }\r\n    } catch (err) {\r\n      reportError(err, 'deserialize')\r\n      settleReady()\r\n      return\r\n    }\r\n    // A write may have landed while we awaited an async migrate — it wins.\r\n    if (userWroteBeforeReady) {\r\n      settleReady()\r\n      return\r\n    }\r\n    writingFromLoad = true\r\n    try {\r\n      source.set(value as T)\r\n    } finally {\r\n      writingFromLoad = false\r\n    }\r\n    settleReady()\r\n    if (needsRewrite) {\r\n      // Persist the migrated value so the next load doesn't re-migrate. Split\r\n      // serialize vs write so a storage-quota throw isn't mislabeled (T6.1).\r\n      let encoded: string\r\n      try {\r\n        encoded = encodeForStorage(value as T)\r\n      } catch (err) {\r\n        reportError(err, 'serialize')\r\n        return\r\n      }\r\n      try {\r\n        const writeResult = storage.set(key, encoded)\r\n        if (writeResult instanceof Promise) writeResult.catch((e) => reportError(e, 'write'))\r\n      } catch (err) {\r\n        reportError(err, 'write')\r\n      }\r\n    }\r\n  }\r\n\r\n  if (loaded instanceof Promise) {\r\n    loaded.then(\r\n      (raw) => applyLoaded(raw),\r\n      (err) => {\r\n        reportError(err, 'load')\r\n        settleReady()\r\n      },\r\n    )\r\n  } else {\r\n    applyLoaded(loaded)\r\n  }\r\n\r\n  // Optional throttled writer. State is captured per-`usePersisted` call so\r\n  // multiple persisted signals in the same controller don't interfere.\r\n  let pendingWriteValue: T | undefined\r\n  let hasPendingWrite = false\r\n  let writeTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\n  const flushWrite = (): void => {\r\n    if (!hasPendingWrite) return\r\n    const value = pendingWriteValue as T\r\n    hasPendingWrite = false\r\n    pendingWriteValue = undefined\r\n    writeTimer = null\r\n    // Encode and write are separate failure domains: encoding is a 'serialize'\r\n    // error; `storage.set` (sync for localStorage — quota throws here) is a\r\n    // 'write' error. The old single try mislabeled every write throw as\r\n    // 'serialize' (T6.1).\r\n    let raw: string\r\n    try {\r\n      raw = encodeForStorage(value)\r\n    } catch (err) {\r\n      reportError(err, 'serialize')\r\n      return\r\n    }\r\n    try {\r\n      const writeResult = storage.set(key, raw)\r\n      if (writeResult instanceof Promise) writeResult.catch((e) => reportError(e, 'write'))\r\n    } catch (err) {\r\n      reportError(err, 'write')\r\n    }\r\n  }\r\n\r\n  const scheduleWrite = (value: T): void => {\r\n    if (throttleMs <= 0) {\r\n      pendingWriteValue = value\r\n      hasPendingWrite = true\r\n      flushWrite()\r\n      return\r\n    }\r\n    pendingWriteValue = value\r\n    hasPendingWrite = true\r\n    if (writeTimer === null) {\r\n      writeTimer = setTimeout(flushWrite, throttleMs)\r\n    }\r\n  }\r\n\r\n  // Persist on every CHANGE. The signal's subscribe fires immediately with\r\n  // the current value — skip that initial call so we don't write back what\r\n  // we just loaded (or the source's default before load).\r\n  let skipFirstDelivery = true\r\n  const unsub = source.subscribe((value) => {\r\n    if (skipFirstDelivery) {\r\n      skipFirstDelivery = false\r\n      return\r\n    }\r\n    if (writingFromLoad) return\r\n    if (!ready$.peek()) {\r\n      // A real user write before the initial load settled — remember it so\r\n      // `settleReady` flushes it and `applyLoaded` doesn't clobber the source.\r\n      // The old code dropped it, then the load overwrote what the user typed\r\n      // (T6.1).\r\n      userWroteBeforeReady = true\r\n      pendingUserValueBeforeReady = value\r\n      return\r\n    }\r\n    scheduleWrite(value)\r\n  })\r\n\r\n  // Cross-tab sync.\r\n  let unsubChange: (() => void) | null = null\r\n  if (crossTab && storage.onChange) {\r\n    unsubChange = storage.onChange((changedKey, rawValue) => {\r\n      if (changedKey !== key) return\r\n      if (!ready$.peek()) {\r\n        // Buffer the freshest cross-tab change until the initial load settles;\r\n        // applying it now would race the load and get clobbered by\r\n        // `applyLoaded` (T6.1). A local user write still takes precedence in\r\n        // `settleReady`.\r\n        hasPendingRemote = true\r\n        pendingRemoteRaw = rawValue\r\n        return\r\n      }\r\n      // A null value is a cross-tab delete (`localStorage.removeItem`) — mirror\r\n      // it locally as `undefined`; see `applyRemote`.\r\n      applyRemote(rawValue)\r\n    })\r\n  }\r\n\r\n  ctx.onDispose(() => {\r\n    // Flush any pending throttled write before tearing down so we never lose\r\n    // the last value the user produced. Synchronous in localStorage; the\r\n    // Promise return from IDB resolves shortly after dispose returns.\r\n    if (hasPendingWrite) {\r\n      if (writeTimer !== null) clearTimeout(writeTimer)\r\n      flushWrite()\r\n    }\r\n    unsub()\r\n    unsubChange?.()\r\n  })\r\n\r\n  return { ready: ready$ }\r\n}\r\n\r\n/**\r\n * Clear every key under a `prefix` (default: clear all). Useful for \"log out\"\r\n * flows that want to drop persisted state without enumerating consumers.\r\n * Errors are routed through the optional `onError` (e.g. quota or security\r\n * exceptions on `delete`).\r\n */\r\nexport async function clearPersisted(\r\n  storage: StorageAdapter = localStorageAdapter,\r\n  prefix?: string,\r\n  onError?: (err: unknown, key: string) => void,\r\n): Promise<void> {\r\n  if (storage.keys === undefined) return\r\n  let keys: Iterable<string>\r\n  try {\r\n    const result = storage.keys()\r\n    keys = result instanceof Promise ? await result : result\r\n  } catch (err) {\r\n    onError?.(err, '<keys>')\r\n    return\r\n  }\r\n  for (const key of keys) {\r\n    if (prefix !== undefined && !key.startsWith(prefix)) continue\r\n    try {\r\n      const r = storage.delete(key)\r\n      if (r instanceof Promise) await r\r\n    } catch (err) {\r\n      onError?.(err, key)\r\n    }\r\n  }\r\n}\r\n"],"mappings":"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"}