@athenaintel/react 0.41.1 → 0.41.3
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.
- package/README.md +12 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs +158 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs.map +1 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.js +158 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.js.map +1 -0
- package/dist/agent-runtime/src/protocol.cjs +15 -0
- package/dist/agent-runtime/src/protocol.cjs.map +1 -1
- package/dist/agent-runtime/src/protocol.js +16 -1
- package/dist/agent-runtime/src/protocol.js.map +1 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.cjs +2 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.cjs.map +1 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.js +2 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.js.map +1 -1
- package/dist/agent-runtime/src/thread-snapshot-mirror.cjs +400 -0
- package/dist/agent-runtime/src/thread-snapshot-mirror.cjs.map +1 -0
- package/dist/agent-runtime/src/thread-snapshot-mirror.js +400 -0
- package/dist/agent-runtime/src/thread-snapshot-mirror.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +5 -1
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +5 -1
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +4 -3
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +7 -1
- package/dist/chat/super-grouping/activity-timeline.js +4 -3
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/offline/offline-history.cjs +77 -0
- package/dist/offline/offline-history.cjs.map +1 -0
- package/dist/offline/offline-history.d.ts +60 -0
- package/dist/offline/offline-history.js +77 -0
- package/dist/offline/offline-history.js.map +1 -0
- package/dist/provider/AthenaProvider.cjs +46 -3
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.d.ts +8 -1
- package/dist/provider/AthenaProvider.js +46 -3
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.cjs +4 -14
- package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.d.ts +5 -4
- package/dist/runtime/useAthenaStatewireRuntime.js +4 -14
- package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
- package/dist/threads/offline-session-list.cjs +75 -0
- package/dist/threads/offline-session-list.cjs.map +1 -0
- package/dist/threads/offline-session-list.d.ts +27 -0
- package/dist/threads/offline-session-list.js +75 -0
- package/dist/threads/offline-session-list.js.map +1 -0
- package/dist/threads/statewire-thread-list.cjs +63 -4
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.d.ts +21 -2
- package/dist/threads/statewire-thread-list.js +64 -5
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-snapshot-cache.js","sources":["../../../../agent-runtime/src/thread-snapshot-cache.ts"],"sourcesContent":["import type { DeepAgentStreamValues } from './types';\n\n/**\n * Per-tab LRU of the last wire state seen per thread, so a switch back to a\n * recently-visited thread can mount with that snapshot instead of an empty\n * \"loading\" state (see `UseDeepAgentThreadOptions.snapshotCache`). Entries are\n * references into states the wire already produced — remembering is O(1) and\n * copies nothing; the bound is `maxThreads` retained states.\n */\nexport interface DeepAgentThreadSnapshotCache {\n /** Record the latest hydrated wire state for a thread. */\n remember(threadId: string, state: DeepAgentStreamValues): void;\n /** The freshest remembered state for a thread, or `undefined` when none is\n * held or the entry aged out. */\n take(threadId: string): DeepAgentStreamValues | undefined;\n /**\n * Drop a single thread's entry. Called when the server refuses\n * authorization for the thread — a refused thread must not warm-paint on\n * its next mount.\n */\n forget(threadId: string): void;\n /**\n * Drop every entry. Called when the acting identity changes — a snapshot\n * captured under one account must never seed a mount under another.\n */\n clear(): void;\n /**\n * Whether a fresh entry is held for the thread, without consuming or\n * touching it. Hosts read it at selection time to decide whether the switch\n * can paint from a seed (no loading treatment) or must cover the attach\n * wait. Optional so hand-rolled caches keep type-checking; the built-in\n * cache implements it.\n */\n peek?(threadId: string): boolean;\n}\n\n/** A write observed on the cache — the durable-mirror seam (see `onChange`). */\nexport type DeepAgentThreadSnapshotCacheChange =\n | { type: 'remember'; threadId: string; state: DeepAgentStreamValues; at: number }\n | { type: 'forget'; threadId: string }\n | { type: 'clear' };\n\nexport interface DeepAgentThreadSnapshotCacheOptions {\n /** Threads retained; the least-recently written entry evicts beyond this. */\n maxThreads?: number;\n /**\n * Entries older than this are dropped at read time. The seed's value is\n * \"the thread you just left\" — a snapshot from long ago is more likely to\n * flash content the server has since rewritten than to save a paint.\n */\n maxAgeMs?: number;\n /**\n * Observer of every write (`remember`, `forget`, `clear`). Hosts mirror\n * entries into durable storage from here so a reload can seed too; the\n * observer is never invoked for `restore`, which re-admits mirrored entries.\n * Exceptions are swallowed — a mirror fault must not break the wire path.\n */\n onChange?: (change: DeepAgentThreadSnapshotCacheChange) => void;\n /** Injectable clock (tests). */\n now?: () => number;\n}\n\n/** The built-in cache: the host contract plus the durable-mirror re-admission. */\nexport interface CreatedDeepAgentThreadSnapshotCache extends DeepAgentThreadSnapshotCache {\n peek(threadId: string): boolean;\n /**\n * Re-admit an entry captured earlier — a mirrored snapshot after a reload —\n * under its ORIGINAL capture time, so the age bound still applies. Never\n * displaces a fresher in-memory entry and never notifies `onChange`.\n */\n restore(threadId: string, state: DeepAgentStreamValues, at: number): void;\n}\n\nconst DEFAULT_MAX_THREADS = 30;\nconst DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;\n\nexport function createDeepAgentThreadSnapshotCache(\n options: DeepAgentThreadSnapshotCacheOptions = {}\n): CreatedDeepAgentThreadSnapshotCache {\n const maxThreads = options.maxThreads ?? DEFAULT_MAX_THREADS;\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n const now = options.now ?? (() => Date.now());\n const notify = (change: DeepAgentThreadSnapshotCacheChange): void => {\n try {\n options.onChange?.(change);\n } catch {\n // The mirror is best-effort; the in-memory cache stays authoritative.\n }\n };\n // Map iteration order is insertion order; delete-then-set on every write\n // keeps the first key the least-recently written.\n const entries = new Map<string, { state: DeepAgentStreamValues; at: number }>();\n const evict = (): void => {\n while (entries.size > maxThreads) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n };\n const fresh = (threadId: string): { state: DeepAgentStreamValues; at: number } | undefined => {\n const entry = entries.get(threadId);\n if (!entry) return undefined;\n if (now() - entry.at > maxAgeMs) {\n entries.delete(threadId);\n return undefined;\n }\n return entry;\n };\n return {\n remember(threadId, state) {\n const at = now();\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n notify({ type: 'remember', threadId, state, at });\n },\n take(threadId) {\n return fresh(threadId)?.state;\n },\n peek(threadId) {\n return fresh(threadId) !== undefined;\n },\n restore(threadId, state, at) {\n if (now() - at > maxAgeMs) return;\n const existing = entries.get(threadId);\n if (existing && existing.at >= at) return;\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n },\n forget(threadId) {\n entries.delete(threadId);\n notify({ type: 'forget', threadId });\n },\n clear() {\n entries.clear();\n notify({ type: 'clear' });\n },\n };\n}\n\n/**\n * Shape a remembered wire state for use as a mount's `initialState`.\n *\n * Messages/todos and the other app-owned keys seed as captured. The run lane\n * seeds by what the user left: a thread whose run was `running` when the\n * snapshot was taken seeds a running entry (see {@link seedRunsLane}) so the\n * switch back reads Running with a Stop control on its first frame — the same\n * frame `runs: []` would render as \"ready\" with a Send button over a thread the\n * server is still mid-run on. Every other live-lifecycle lane normalizes to\n * resting: a seeded input request could collect a decision the server may have\n * already resolved, so an `input-required` entry seeds as no run at all.\n * `__interrupt__` is dropped for the same reason, and so are `ptc_subagents`\n * (the live in-eval dispatch lane — a seeded \"running\" dispatch card under a\n * resting thread would render active with no eval behind it) and\n * `outOfBandRunActive` (the host's out-of-band-run-in-progress flag — seeding a\n * finished run's `true` would hide the composer until the attach corrected\n * it). Durable thread properties (`runConfig`, `sessionPersisted`, …) seed as\n * captured; the attach snapshot then replaces the whole seed.\n */\nexport function toThreadSnapshotSeed(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const {\n __interrupt__: _interrupt,\n ptc_subagents: _ptcSubagents,\n outOfBandRunActive: _outOfBandRunActive,\n ...rest\n } = state;\n return { ...rest, runs: seedRunsLane(state.runs) };\n}\n\n/**\n * The run entries a seed carries.\n *\n * Only a `running` entry survives, and only its replicated identity and its\n * durable lanes: `runId` (the target of a Stop sent during the attach\n * round-trip — the live run's id if it is still running, and a refused stop\n * if it finished meanwhile), `epoch` (rides that Stop so a replayed stop cannot\n * end a later leg) and the `queue`/`steerQueue` entries (server lanes the tray\n * renders). The in-flight records rest until the snapshot: `dispatching` and\n * `nextDispatch` are batches the materializer would re-apply onto messages\n * that already contain them, `stopping` is a rewind the host may have\n * finished, `inputRequests` is a parked decision, and `error` is a failure the\n * next user run clears. An entry in any other status — `stopped`, `error`,\n * `input-required` — seeds as no run.\n */\nfunction seedRunsLane(\n runs: DeepAgentStreamValues['runs']\n): NonNullable<DeepAgentStreamValues['runs']> {\n const live = runs?.[0];\n if (live === undefined || live.status !== 'running') return [];\n return [\n {\n runId: live.runId,\n status: 'running',\n queue: live.queue,\n steerQueue: live.steerQueue,\n ...(live.epoch !== undefined && { epoch: live.epoch }),\n },\n ];\n}\n"],"names":[],"mappings":"AAyEA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB,KAAK,KAAK;AAE9B,SAAS,mCACd,UAA+C,IACV;AACrC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAA;AACvC,QAAM,SAAS,CAAC,WAAqD;AATvE;AAUI,QAAI;AACF,oBAAQ,aAAR,iCAAmB;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,8BAAc,IAAA;AACpB,QAAM,QAAQ,MAAY;AACxB,WAAO,QAAQ,OAAO,YAAY;AAChC,YAAM,SAAS,QAAQ,KAAA,EAAO,OAAO;AACrC,UAAI,WAAW,OAAW;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACvB;AAAA,EACF;AACA,QAAM,QAAQ,CAAC,aAA+E;AAC5F,UAAM,QAAQ,QAAQ,IAAI,QAAQ;AAClC,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,IAAA,IAAQ,MAAM,KAAK,UAAU;AAC/B,cAAQ,OAAO,QAAQ;AACvB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,SAAS,UAAU,OAAO;AACxB,YAAM,KAAK,IAAA;AACX,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AACA,aAAO,EAAE,MAAM,YAAY,UAAU,OAAO,IAAI;AAAA,IAClD;AAAA,IACA,KAAK,UAAU;AA3CnB;AA4CM,cAAO,WAAM,QAAQ,MAAd,mBAAiB;AAAA,IAC1B;AAAA,IACA,KAAK,UAAU;AACb,aAAO,MAAM,QAAQ,MAAM;AAAA,IAC7B;AAAA,IACA,QAAQ,UAAU,OAAO,IAAI;AAC3B,UAAI,IAAA,IAAQ,KAAK,SAAU;AAC3B,YAAM,WAAW,QAAQ,IAAI,QAAQ;AACrC,UAAI,YAAY,SAAS,MAAM,GAAI;AACnC,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AAAA,IACF;AAAA,IACA,OAAO,UAAU;AACf,cAAQ,OAAO,QAAQ;AACvB,aAAO,EAAE,MAAM,UAAU,SAAA,CAAU;AAAA,IACrC;AAAA,IACA,QAAQ;AACN,cAAQ,MAAA;AACR,aAAO,EAAE,MAAM,SAAS;AAAA,IAC1B;AAAA,EAAA;AAEJ;AAqBO,SAAS,qBAAqB,OAAqD;AACxF,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,GAAG;AAAA,EAAA,IACD;AACJ,SAAO,EAAE,GAAG,MAAM,MAAM,aAAa,MAAM,IAAI,EAAA;AACjD;AAiBA,SAAS,aACP,MAC4C;AAC5C,QAAM,OAAO,6BAAO;AACpB,MAAI,SAAS,UAAa,KAAK,WAAW,kBAAkB,CAAA;AAC5D,SAAO;AAAA,IACL;AAAA,MACE,OAAO,KAAK;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,GAAI,KAAK,UAAU,UAAa,EAAE,OAAO,KAAK,MAAA;AAAA,IAAM;AAAA,EACtD;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"thread-snapshot-cache.js","sources":["../../../../agent-runtime/src/thread-snapshot-cache.ts"],"sourcesContent":["import { withSeedProtocolLane } from './protocol';\nimport type { DeepAgentStreamValues } from './types';\n\n/**\n * Per-tab LRU of the last wire state seen per thread, so a switch back to a\n * recently-visited thread can mount with that snapshot instead of an empty\n * \"loading\" state (see `UseDeepAgentThreadOptions.snapshotCache`). Entries are\n * references into states the wire already produced — remembering is O(1) and\n * copies nothing; the bound is `maxThreads` retained states.\n */\nexport interface DeepAgentThreadSnapshotCache {\n /** Record the latest hydrated wire state for a thread. */\n remember(threadId: string, state: DeepAgentStreamValues): void;\n /** The freshest remembered state for a thread, or `undefined` when none is\n * held or the entry aged out. */\n take(threadId: string): DeepAgentStreamValues | undefined;\n /**\n * Drop a single thread's entry. Called when the server refuses\n * authorization for the thread — a refused thread must not warm-paint on\n * its next mount.\n */\n forget(threadId: string): void;\n /**\n * Drop every entry. Called when the acting identity changes — a snapshot\n * captured under one account must never seed a mount under another.\n */\n clear(): void;\n /**\n * Whether a fresh entry is held for the thread, without consuming or\n * touching it. Hosts read it at selection time to decide whether the switch\n * can paint from a seed (no loading treatment) or must cover the attach\n * wait. Optional so hand-rolled caches keep type-checking; the built-in\n * cache implements it.\n */\n peek?(threadId: string): boolean;\n}\n\n/** A write observed on the cache — the durable-mirror seam (see `onChange`). */\nexport type DeepAgentThreadSnapshotCacheChange =\n | { type: 'remember'; threadId: string; state: DeepAgentStreamValues; at: number }\n | { type: 'forget'; threadId: string }\n | { type: 'clear' };\n\nexport interface DeepAgentThreadSnapshotCacheOptions {\n /** Threads retained; the least-recently written entry evicts beyond this. */\n maxThreads?: number;\n /**\n * Entries older than this are dropped at read time. The seed's value is\n * \"the thread you just left\" — a snapshot from long ago is more likely to\n * flash content the server has since rewritten than to save a paint.\n */\n maxAgeMs?: number;\n /**\n * Observer of every write (`remember`, `forget`, `clear`). Hosts mirror\n * entries into durable storage from here so a reload can seed too; the\n * observer is never invoked for `restore`, which re-admits mirrored entries.\n * Exceptions are swallowed — a mirror fault must not break the wire path.\n */\n onChange?: (change: DeepAgentThreadSnapshotCacheChange) => void;\n /** Injectable clock (tests). */\n now?: () => number;\n}\n\n/** The built-in cache: the host contract plus the durable-mirror re-admission. */\nexport interface CreatedDeepAgentThreadSnapshotCache extends DeepAgentThreadSnapshotCache {\n peek(threadId: string): boolean;\n /**\n * Re-admit an entry captured earlier — a mirrored snapshot after a reload —\n * under its ORIGINAL capture time, so the age bound still applies. Never\n * displaces a fresher in-memory entry and never notifies `onChange`.\n */\n restore(threadId: string, state: DeepAgentStreamValues, at: number): void;\n}\n\nconst DEFAULT_MAX_THREADS = 30;\nconst DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;\n\nexport function createDeepAgentThreadSnapshotCache(\n options: DeepAgentThreadSnapshotCacheOptions = {}\n): CreatedDeepAgentThreadSnapshotCache {\n const maxThreads = options.maxThreads ?? DEFAULT_MAX_THREADS;\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n const now = options.now ?? (() => Date.now());\n const notify = (change: DeepAgentThreadSnapshotCacheChange): void => {\n try {\n options.onChange?.(change);\n } catch {\n // The mirror is best-effort; the in-memory cache stays authoritative.\n }\n };\n // Map iteration order is insertion order; delete-then-set on every write\n // keeps the first key the least-recently written.\n const entries = new Map<string, { state: DeepAgentStreamValues; at: number }>();\n const evict = (): void => {\n while (entries.size > maxThreads) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n };\n const fresh = (threadId: string): { state: DeepAgentStreamValues; at: number } | undefined => {\n const entry = entries.get(threadId);\n if (!entry) return undefined;\n if (now() - entry.at > maxAgeMs) {\n entries.delete(threadId);\n return undefined;\n }\n return entry;\n };\n return {\n remember(threadId, state) {\n const at = now();\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n notify({ type: 'remember', threadId, state, at });\n },\n take(threadId) {\n return fresh(threadId)?.state;\n },\n peek(threadId) {\n return fresh(threadId) !== undefined;\n },\n restore(threadId, state, at) {\n if (now() - at > maxAgeMs) return;\n const existing = entries.get(threadId);\n if (existing && existing.at >= at) return;\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n },\n forget(threadId) {\n entries.delete(threadId);\n notify({ type: 'forget', threadId });\n },\n clear() {\n entries.clear();\n notify({ type: 'clear' });\n },\n };\n}\n\n/**\n * Shape a remembered wire state for use as a mount's `initialState`.\n *\n * Messages/todos and the other app-owned keys seed as captured. The run lane\n * seeds by what the user left: a thread whose run was `running` when the\n * snapshot was taken seeds a running entry (see {@link seedRunsLane}) so the\n * switch back reads Running with a Stop control on its first frame — the same\n * frame `runs: []` would render as \"ready\" with a Send button over a thread the\n * server is still mid-run on. Every other live-lifecycle lane normalizes to\n * resting: a seeded input request could collect a decision the server may have\n * already resolved, so an `input-required` entry seeds as no run at all.\n * `__interrupt__` is dropped for the same reason, and so are `ptc_subagents`\n * (the live in-eval dispatch lane — a seeded \"running\" dispatch card under a\n * resting thread would render active with no eval behind it) and\n * `outOfBandRunActive` (the host's out-of-band-run-in-progress flag — seeding a\n * finished run's `true` would hide the composer until the attach corrected\n * it). Durable thread properties (`runConfig`, `config`, `sessionPersisted`, …)\n * seed as captured; the attach snapshot then replaces the whole seed. The\n * protocol lane seeds as a seed's own copy (`withSeedProtocolLane`), so\n * `isDeepAgentHostState` never reads the seed, or a state folded over it, as\n * what the host holds now.\n */\nexport function toThreadSnapshotSeed(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const {\n __interrupt__: _interrupt,\n ptc_subagents: _ptcSubagents,\n outOfBandRunActive: _outOfBandRunActive,\n ...rest\n } = state;\n return withSeedProtocolLane({ ...rest, runs: seedRunsLane(state.runs) });\n}\n\n/**\n * The run entries a seed carries.\n *\n * Only a `running` entry survives, and only its replicated identity and its\n * durable lanes: `runId` (the target of a Stop sent during the attach\n * round-trip — the live run's id if it is still running, and a refused stop\n * if it finished meanwhile), `epoch` (rides that Stop so a replayed stop cannot\n * end a later leg) and the `queue`/`steerQueue` entries (server lanes the tray\n * renders). The in-flight records rest until the snapshot: `dispatching` and\n * `nextDispatch` are batches the materializer would re-apply onto messages\n * that already contain them, `stopping` is a rewind the host may have\n * finished, `inputRequests` is a parked decision, and `error` is a failure the\n * next user run clears. An entry in any other status — `stopped`, `error`,\n * `input-required` — seeds as no run.\n */\nfunction seedRunsLane(\n runs: DeepAgentStreamValues['runs']\n): NonNullable<DeepAgentStreamValues['runs']> {\n const live = runs?.[0];\n if (live === undefined || live.status !== 'running') return [];\n return [\n {\n runId: live.runId,\n status: 'running',\n queue: live.queue,\n steerQueue: live.steerQueue,\n ...(live.epoch !== undefined && { epoch: live.epoch }),\n },\n ];\n}\n"],"names":[],"mappings":";AA0EA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB,KAAK,KAAK;AAE9B,SAAS,mCACd,UAA+C,IACV;AACrC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAA;AACvC,QAAM,SAAS,CAAC,WAAqD;;AACnE,QAAI;AACF,oBAAQ,aAAR,iCAAmB;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,8BAAc,IAAA;AACpB,QAAM,QAAQ,MAAY;AACxB,WAAO,QAAQ,OAAO,YAAY;AAChC,YAAM,SAAS,QAAQ,KAAA,EAAO,OAAO;AACrC,UAAI,WAAW,OAAW;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACvB;AAAA,EACF;AACA,QAAM,QAAQ,CAAC,aAA+E;AAC5F,UAAM,QAAQ,QAAQ,IAAI,QAAQ;AAClC,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,IAAA,IAAQ,MAAM,KAAK,UAAU;AAC/B,cAAQ,OAAO,QAAQ;AACvB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,SAAS,UAAU,OAAO;AACxB,YAAM,KAAK,IAAA;AACX,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AACA,aAAO,EAAE,MAAM,YAAY,UAAU,OAAO,IAAI;AAAA,IAClD;AAAA,IACA,KAAK,UAAU;;AACb,cAAO,WAAM,QAAQ,MAAd,mBAAiB;AAAA,IAC1B;AAAA,IACA,KAAK,UAAU;AACb,aAAO,MAAM,QAAQ,MAAM;AAAA,IAC7B;AAAA,IACA,QAAQ,UAAU,OAAO,IAAI;AAC3B,UAAI,IAAA,IAAQ,KAAK,SAAU;AAC3B,YAAM,WAAW,QAAQ,IAAI,QAAQ;AACrC,UAAI,YAAY,SAAS,MAAM,GAAI;AACnC,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AAAA,IACF;AAAA,IACA,OAAO,UAAU;AACf,cAAQ,OAAO,QAAQ;AACvB,aAAO,EAAE,MAAM,UAAU,SAAA,CAAU;AAAA,IACrC;AAAA,IACA,QAAQ;AACN,cAAQ,MAAA;AACR,aAAO,EAAE,MAAM,SAAS;AAAA,IAC1B;AAAA,EAAA;AAEJ;AAwBO,SAAS,qBAAqB,OAAqD;AACxF,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,GAAG;AAAA,EAAA,IACD;AACJ,SAAO,qBAAqB,EAAE,GAAG,MAAM,MAAM,aAAa,MAAM,IAAI,GAAG;AACzE;AAiBA,SAAS,aACP,MAC4C;AAC5C,QAAM,OAAO,6BAAO;AACpB,MAAI,SAAS,UAAa,KAAK,WAAW,kBAAkB,CAAA;AAC5D,SAAO;AAAA,IACL;AAAA,MACE,OAAO,KAAK;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,GAAI,KAAK,UAAU,UAAa,EAAE,OAAO,KAAK,MAAA;AAAA,IAAM;AAAA,EACtD;AAEJ;"}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const THREAD_SNAPSHOT_MIRROR_DATABASE = "athena-thread-snapshots:v2";
|
|
4
|
+
const THREAD_SNAPSHOT_MIRROR_FORMAT = 1;
|
|
5
|
+
const MAX_MIRRORED_SNAPSHOTS_PER_OWNER = 30;
|
|
6
|
+
const MAX_MIRRORED_SNAPSHOT_MESSAGES = 150;
|
|
7
|
+
const SNAPSHOT_MIRROR_DEBOUNCE_MS = 1500;
|
|
8
|
+
const DATABASE_VERSION = 1;
|
|
9
|
+
const SNAPSHOTS_STORE = "snapshots";
|
|
10
|
+
const LISTS_STORE = "lists";
|
|
11
|
+
const BY_OWNER = "byOwner";
|
|
12
|
+
const BY_OWNER_AT = "byOwnerAt";
|
|
13
|
+
const IV_BYTES = 12;
|
|
14
|
+
const idbGlobals = globalThis;
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
async function sha256Hex(value) {
|
|
18
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
|
|
19
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
20
|
+
}
|
|
21
|
+
async function sealJson({ key, value, aad }) {
|
|
22
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
23
|
+
const ciphertext = new Uint8Array(
|
|
24
|
+
await crypto.subtle.encrypt(
|
|
25
|
+
{ name: "AES-GCM", iv, additionalData: encoder.encode(aad) },
|
|
26
|
+
key,
|
|
27
|
+
encoder.encode(JSON.stringify(value))
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
const sealed = new Uint8Array(IV_BYTES + ciphertext.byteLength);
|
|
31
|
+
sealed.set(iv, 0);
|
|
32
|
+
sealed.set(ciphertext, IV_BYTES);
|
|
33
|
+
return sealed;
|
|
34
|
+
}
|
|
35
|
+
async function openJson({ key, sealed, aad }) {
|
|
36
|
+
const iv = sealed.subarray(0, IV_BYTES);
|
|
37
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
38
|
+
{ name: "AES-GCM", iv, additionalData: encoder.encode(aad) },
|
|
39
|
+
key,
|
|
40
|
+
sealed.subarray(IV_BYTES)
|
|
41
|
+
);
|
|
42
|
+
return JSON.parse(decoder.decode(plaintext));
|
|
43
|
+
}
|
|
44
|
+
function requestToPromise(request) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
request.onsuccess = () => resolve(request.result);
|
|
47
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function transactionDone(tx) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
tx.oncomplete = () => resolve();
|
|
53
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
54
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB transaction failed"));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function openDatabase(factory, name) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
try {
|
|
60
|
+
const request = factory.open(name, DATABASE_VERSION);
|
|
61
|
+
request.onupgradeneeded = () => {
|
|
62
|
+
const db = request.result;
|
|
63
|
+
if (!db.objectStoreNames.contains(SNAPSHOTS_STORE)) {
|
|
64
|
+
const store = db.createObjectStore(SNAPSHOTS_STORE, { keyPath: "key" });
|
|
65
|
+
store.createIndex(BY_OWNER, "owner", { unique: false });
|
|
66
|
+
store.createIndex(BY_OWNER_AT, ["owner", "at"], { unique: false });
|
|
67
|
+
}
|
|
68
|
+
if (!db.objectStoreNames.contains(LISTS_STORE)) {
|
|
69
|
+
const lists = db.createObjectStore(LISTS_STORE, { keyPath: "key" });
|
|
70
|
+
lists.createIndex(BY_OWNER, "owner", { unique: false });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
request.onsuccess = () => {
|
|
74
|
+
const db = request.result;
|
|
75
|
+
db.onversionchange = () => db.close();
|
|
76
|
+
resolve(db);
|
|
77
|
+
};
|
|
78
|
+
request.onerror = () => resolve(null);
|
|
79
|
+
request.onblocked = () => resolve(null);
|
|
80
|
+
} catch {
|
|
81
|
+
resolve(null);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function isRestingThreadSnapshot(state) {
|
|
86
|
+
var _a;
|
|
87
|
+
const run = (_a = state.runs) == null ? void 0 : _a[0];
|
|
88
|
+
if (run && run.status === "running") return false;
|
|
89
|
+
if (state.outOfBandRunActive === true) return false;
|
|
90
|
+
return Array.isArray(state.messages);
|
|
91
|
+
}
|
|
92
|
+
function classifyThreadSnapshotForMirror(state, maxMessages = MAX_MIRRORED_SNAPSHOT_MESSAGES) {
|
|
93
|
+
var _a;
|
|
94
|
+
if (!isRestingThreadSnapshot(state)) return "skip";
|
|
95
|
+
return (((_a = state.messages) == null ? void 0 : _a.length) ?? 0) <= maxMessages ? "persist" : "invalidate";
|
|
96
|
+
}
|
|
97
|
+
function isOwnFormat(record) {
|
|
98
|
+
if (typeof record !== "object" || record === null) return true;
|
|
99
|
+
const format = record.format;
|
|
100
|
+
return typeof format !== "number" || format <= THREAD_SNAPSHOT_MIRROR_FORMAT;
|
|
101
|
+
}
|
|
102
|
+
function isRecord(value) {
|
|
103
|
+
return typeof value === "object" && value !== null;
|
|
104
|
+
}
|
|
105
|
+
function createThreadSnapshotMirror({
|
|
106
|
+
getKey,
|
|
107
|
+
factory = idbGlobals.indexedDB,
|
|
108
|
+
databaseName = THREAD_SNAPSHOT_MIRROR_DATABASE,
|
|
109
|
+
debounceMs = SNAPSHOT_MIRROR_DEBOUNCE_MS,
|
|
110
|
+
maxPerOwner = MAX_MIRRORED_SNAPSHOTS_PER_OWNER,
|
|
111
|
+
maxMessages = MAX_MIRRORED_SNAPSHOT_MESSAGES
|
|
112
|
+
}) {
|
|
113
|
+
let databasePromise = null;
|
|
114
|
+
const database = () => {
|
|
115
|
+
if (!factory || typeof crypto === "undefined" || !crypto.subtle) return Promise.resolve(null);
|
|
116
|
+
databasePromise ?? (databasePromise = openDatabase(factory, databaseName).then((db) => {
|
|
117
|
+
if (db) {
|
|
118
|
+
const forget = () => {
|
|
119
|
+
databasePromise = null;
|
|
120
|
+
};
|
|
121
|
+
db.addEventListener("versionchange", forget);
|
|
122
|
+
db.addEventListener("close", forget);
|
|
123
|
+
} else {
|
|
124
|
+
databasePromise = null;
|
|
125
|
+
}
|
|
126
|
+
return db;
|
|
127
|
+
}));
|
|
128
|
+
return databasePromise;
|
|
129
|
+
};
|
|
130
|
+
const keyOf = async (ownerId, purpose) => {
|
|
131
|
+
try {
|
|
132
|
+
return await getKey(ownerId, purpose);
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const aadOf = (kind, owner, key) => `athena-thread-snapshot|${THREAD_SNAPSHOT_MIRROR_FORMAT}|${kind}|${owner}|${key}`;
|
|
138
|
+
const pending = /* @__PURE__ */ new Map();
|
|
139
|
+
const pendingLists = /* @__PURE__ */ new Map();
|
|
140
|
+
let queue = Promise.resolve();
|
|
141
|
+
const enqueue = (task) => {
|
|
142
|
+
queue = queue.then(task, task).catch(() => {
|
|
143
|
+
});
|
|
144
|
+
return queue;
|
|
145
|
+
};
|
|
146
|
+
const pruneOwner = async (db, owner) => {
|
|
147
|
+
const KeyRange = idbGlobals.IDBKeyRange;
|
|
148
|
+
if (!KeyRange) return;
|
|
149
|
+
const tx = db.transaction(SNAPSHOTS_STORE, "readwrite");
|
|
150
|
+
const store = tx.objectStore(SNAPSHOTS_STORE);
|
|
151
|
+
const index = store.index(BY_OWNER_AT);
|
|
152
|
+
const range = KeyRange.bound([owner, Number.NEGATIVE_INFINITY], [owner, Number.POSITIVE_INFINITY]);
|
|
153
|
+
const beyond = [];
|
|
154
|
+
await new Promise((resolve, reject) => {
|
|
155
|
+
let kept = 0;
|
|
156
|
+
const request = index.openKeyCursor(range, "prev");
|
|
157
|
+
request.onsuccess = () => {
|
|
158
|
+
const cursor = request.result;
|
|
159
|
+
if (!cursor) {
|
|
160
|
+
resolve();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
kept += 1;
|
|
164
|
+
if (kept > maxPerOwner) beyond.push(cursor.primaryKey);
|
|
165
|
+
cursor.continue();
|
|
166
|
+
};
|
|
167
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB cursor failed"));
|
|
168
|
+
});
|
|
169
|
+
for (const key of beyond) {
|
|
170
|
+
if (isOwnFormat(await requestToPromise(store.get(key)))) store.delete(key);
|
|
171
|
+
}
|
|
172
|
+
await transactionDone(tx);
|
|
173
|
+
};
|
|
174
|
+
const put = (input) => enqueue(async () => {
|
|
175
|
+
const db = await database();
|
|
176
|
+
if (!db) return;
|
|
177
|
+
const cryptoKey = await keyOf(input.ownerId, "write");
|
|
178
|
+
if (!cryptoKey) return;
|
|
179
|
+
const owner = await sha256Hex(input.ownerId);
|
|
180
|
+
const key = await sha256Hex(`${input.ownerId}|${input.threadId}`);
|
|
181
|
+
const sealed = await sealJson({
|
|
182
|
+
key: cryptoKey,
|
|
183
|
+
value: { threadId: input.threadId, state: input.state },
|
|
184
|
+
aad: aadOf("snapshot", owner, key)
|
|
185
|
+
});
|
|
186
|
+
const tx = db.transaction(SNAPSHOTS_STORE, "readwrite");
|
|
187
|
+
const store = tx.objectStore(SNAPSHOTS_STORE);
|
|
188
|
+
if (isOwnFormat(await requestToPromise(store.get(key)))) {
|
|
189
|
+
store.put({
|
|
190
|
+
key,
|
|
191
|
+
owner,
|
|
192
|
+
at: input.at,
|
|
193
|
+
format: THREAD_SNAPSHOT_MIRROR_FORMAT,
|
|
194
|
+
sealed
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
await transactionDone(tx);
|
|
198
|
+
await pruneOwner(db, owner);
|
|
199
|
+
});
|
|
200
|
+
const deleteSnapshot = (input) => enqueue(async () => {
|
|
201
|
+
const db = await database();
|
|
202
|
+
if (!db) return;
|
|
203
|
+
const key = await sha256Hex(`${input.ownerId}|${input.threadId}`);
|
|
204
|
+
const tx = db.transaction(SNAPSHOTS_STORE, "readwrite");
|
|
205
|
+
const store = tx.objectStore(SNAPSHOTS_STORE);
|
|
206
|
+
if (isOwnFormat(await requestToPromise(store.get(key)))) store.delete(key);
|
|
207
|
+
await transactionDone(tx);
|
|
208
|
+
});
|
|
209
|
+
const pendingKey = (ownerId, threadId) => `${ownerId}\0${threadId}`;
|
|
210
|
+
const flushPending = (key) => {
|
|
211
|
+
const entry = pending.get(key);
|
|
212
|
+
if (!entry) return Promise.resolve();
|
|
213
|
+
pending.delete(key);
|
|
214
|
+
clearTimeout(entry.timer);
|
|
215
|
+
return put(entry.input);
|
|
216
|
+
};
|
|
217
|
+
const putList = (input) => enqueue(async () => {
|
|
218
|
+
const db = await database();
|
|
219
|
+
if (!db) return;
|
|
220
|
+
const cryptoKey = await keyOf(input.ownerId, "write");
|
|
221
|
+
if (!cryptoKey) return;
|
|
222
|
+
const owner = await sha256Hex(input.ownerId);
|
|
223
|
+
const key = await sha256Hex(`${input.ownerId}|list|${input.listKey}`);
|
|
224
|
+
const sealed = await sealJson({ key: cryptoKey, value: input.value, aad: aadOf("list", owner, key) });
|
|
225
|
+
const tx = db.transaction(LISTS_STORE, "readwrite");
|
|
226
|
+
const store = tx.objectStore(LISTS_STORE);
|
|
227
|
+
if (isOwnFormat(await requestToPromise(store.get(key)))) {
|
|
228
|
+
store.put({
|
|
229
|
+
key,
|
|
230
|
+
owner,
|
|
231
|
+
at: Date.now(),
|
|
232
|
+
format: THREAD_SNAPSHOT_MIRROR_FORMAT,
|
|
233
|
+
sealed
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
await transactionDone(tx);
|
|
237
|
+
});
|
|
238
|
+
const flushList = (key) => {
|
|
239
|
+
const entry = pendingLists.get(key);
|
|
240
|
+
if (!entry) return Promise.resolve();
|
|
241
|
+
pendingLists.delete(key);
|
|
242
|
+
clearTimeout(entry.timer);
|
|
243
|
+
return putList(entry.input);
|
|
244
|
+
};
|
|
245
|
+
const dropPendingOf = (ownerId) => {
|
|
246
|
+
for (const [key, entry] of pending) {
|
|
247
|
+
if (ownerId === null || entry.input.ownerId === ownerId) {
|
|
248
|
+
clearTimeout(entry.timer);
|
|
249
|
+
pending.delete(key);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const [key, entry] of pendingLists) {
|
|
253
|
+
if (ownerId === null || entry.input.ownerId === ownerId) {
|
|
254
|
+
clearTimeout(entry.timer);
|
|
255
|
+
pendingLists.delete(key);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
async load(ownerId) {
|
|
261
|
+
try {
|
|
262
|
+
const db = await database();
|
|
263
|
+
if (!db) return [];
|
|
264
|
+
const cryptoKey = await keyOf(ownerId, "read");
|
|
265
|
+
if (!cryptoKey) return [];
|
|
266
|
+
const owner = await sha256Hex(ownerId);
|
|
267
|
+
const records = await requestToPromise(
|
|
268
|
+
db.transaction(SNAPSHOTS_STORE, "readonly").objectStore(SNAPSHOTS_STORE).index(BY_OWNER).getAll(owner)
|
|
269
|
+
);
|
|
270
|
+
const loaded = [];
|
|
271
|
+
const unreadable = [];
|
|
272
|
+
for (const record of records) {
|
|
273
|
+
if (!record || record.format > THREAD_SNAPSHOT_MIRROR_FORMAT) continue;
|
|
274
|
+
try {
|
|
275
|
+
const value = await openJson({
|
|
276
|
+
key: cryptoKey,
|
|
277
|
+
sealed: record.sealed,
|
|
278
|
+
aad: aadOf("snapshot", owner, record.key)
|
|
279
|
+
});
|
|
280
|
+
if (isRecord(value) && typeof value.threadId === "string" && isRecord(value.state) && typeof record.at === "number") {
|
|
281
|
+
loaded.push({ threadId: value.threadId, state: value.state, at: record.at });
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
unreadable.push(record.key);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (unreadable.length > 0) {
|
|
288
|
+
void enqueue(async () => {
|
|
289
|
+
const tx = db.transaction(SNAPSHOTS_STORE, "readwrite");
|
|
290
|
+
for (const key of unreadable) tx.objectStore(SNAPSHOTS_STORE).delete(key);
|
|
291
|
+
await transactionDone(tx);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return loaded.sort((a, b) => a.at - b.at);
|
|
295
|
+
} catch {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
write(input) {
|
|
300
|
+
const key = pendingKey(input.ownerId, input.threadId);
|
|
301
|
+
const verdict = classifyThreadSnapshotForMirror(input.state, maxMessages);
|
|
302
|
+
if (verdict === "invalidate") {
|
|
303
|
+
const existing2 = pending.get(key);
|
|
304
|
+
if (existing2) clearTimeout(existing2.timer);
|
|
305
|
+
pending.delete(key);
|
|
306
|
+
void deleteSnapshot(input);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (verdict === "skip") return;
|
|
310
|
+
const existing = pending.get(key);
|
|
311
|
+
if (existing) clearTimeout(existing.timer);
|
|
312
|
+
pending.set(key, {
|
|
313
|
+
input,
|
|
314
|
+
timer: setTimeout(() => {
|
|
315
|
+
void flushPending(key);
|
|
316
|
+
}, debounceMs)
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
remove(input) {
|
|
320
|
+
const key = pendingKey(input.ownerId, input.threadId);
|
|
321
|
+
const existing = pending.get(key);
|
|
322
|
+
if (existing) clearTimeout(existing.timer);
|
|
323
|
+
pending.delete(key);
|
|
324
|
+
void deleteSnapshot(input);
|
|
325
|
+
},
|
|
326
|
+
writeList(input) {
|
|
327
|
+
const key = pendingKey(input.ownerId, `list:${input.listKey}`);
|
|
328
|
+
const existing = pendingLists.get(key);
|
|
329
|
+
if (existing) clearTimeout(existing.timer);
|
|
330
|
+
pendingLists.set(key, {
|
|
331
|
+
input,
|
|
332
|
+
timer: setTimeout(() => {
|
|
333
|
+
void flushList(key);
|
|
334
|
+
}, debounceMs)
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
async loadList({ ownerId, listKey }) {
|
|
338
|
+
try {
|
|
339
|
+
const db = await database();
|
|
340
|
+
if (!db) return null;
|
|
341
|
+
const cryptoKey = await keyOf(ownerId, "read");
|
|
342
|
+
if (!cryptoKey) return null;
|
|
343
|
+
const owner = await sha256Hex(ownerId);
|
|
344
|
+
const key = await sha256Hex(`${ownerId}|list|${listKey}`);
|
|
345
|
+
const record = await requestToPromise(
|
|
346
|
+
db.transaction(LISTS_STORE, "readonly").objectStore(LISTS_STORE).get(key)
|
|
347
|
+
);
|
|
348
|
+
if (!record || record.format > THREAD_SNAPSHOT_MIRROR_FORMAT || record.owner !== owner) return null;
|
|
349
|
+
try {
|
|
350
|
+
const value = await openJson({ key: cryptoKey, sealed: record.sealed, aad: aadOf("list", owner, key) });
|
|
351
|
+
return { value, at: record.at };
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
clearOwner(ownerId) {
|
|
360
|
+
dropPendingOf(ownerId);
|
|
361
|
+
return enqueue(async () => {
|
|
362
|
+
const db = await database();
|
|
363
|
+
if (!db) return;
|
|
364
|
+
const owner = await sha256Hex(ownerId);
|
|
365
|
+
const tx = db.transaction([SNAPSHOTS_STORE, LISTS_STORE], "readwrite");
|
|
366
|
+
for (const storeName of [SNAPSHOTS_STORE, LISTS_STORE]) {
|
|
367
|
+
const store = tx.objectStore(storeName);
|
|
368
|
+
const keys = await requestToPromise(store.index(BY_OWNER).getAllKeys(owner));
|
|
369
|
+
for (const key of keys) store.delete(key);
|
|
370
|
+
}
|
|
371
|
+
await transactionDone(tx);
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
clearAll() {
|
|
375
|
+
dropPendingOf(null);
|
|
376
|
+
return enqueue(async () => {
|
|
377
|
+
const db = await database();
|
|
378
|
+
if (!db) return;
|
|
379
|
+
const tx = db.transaction([SNAPSHOTS_STORE, LISTS_STORE], "readwrite");
|
|
380
|
+
tx.objectStore(SNAPSHOTS_STORE).clear();
|
|
381
|
+
tx.objectStore(LISTS_STORE).clear();
|
|
382
|
+
await transactionDone(tx);
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
async flush() {
|
|
386
|
+
await Promise.all([...pending.keys()].map(flushPending));
|
|
387
|
+
await Promise.all([...pendingLists.keys()].map(flushList));
|
|
388
|
+
await queue;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
exports.MAX_MIRRORED_SNAPSHOTS_PER_OWNER = MAX_MIRRORED_SNAPSHOTS_PER_OWNER;
|
|
393
|
+
exports.MAX_MIRRORED_SNAPSHOT_MESSAGES = MAX_MIRRORED_SNAPSHOT_MESSAGES;
|
|
394
|
+
exports.SNAPSHOT_MIRROR_DEBOUNCE_MS = SNAPSHOT_MIRROR_DEBOUNCE_MS;
|
|
395
|
+
exports.THREAD_SNAPSHOT_MIRROR_DATABASE = THREAD_SNAPSHOT_MIRROR_DATABASE;
|
|
396
|
+
exports.THREAD_SNAPSHOT_MIRROR_FORMAT = THREAD_SNAPSHOT_MIRROR_FORMAT;
|
|
397
|
+
exports.classifyThreadSnapshotForMirror = classifyThreadSnapshotForMirror;
|
|
398
|
+
exports.createThreadSnapshotMirror = createThreadSnapshotMirror;
|
|
399
|
+
exports.isRestingThreadSnapshot = isRestingThreadSnapshot;
|
|
400
|
+
//# sourceMappingURL=thread-snapshot-mirror.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-snapshot-mirror.cjs","sources":["../../../../agent-runtime/src/thread-snapshot-mirror.ts"],"sourcesContent":["import type { DeepAgentStreamValues } from \"./types\";\n\n/**\n * A durable, encrypted mirror of the thread snapshot cache (`thread-snapshot-cache.ts`) in the\n * browser's IndexedDB, so a reload paints a recently visited thread from its last-known messages,\n * and a thread can be read while the transport cannot connect at all.\n *\n * - **Sealed.** Each record is AES-GCM-256 with a fresh IV under the host's key for its owner\n * (`getKey`: Olympus a non-extractable key in its own keystore, Midas a key main keeps with the\n * OS keychain). The owner and the thread are hashed for the index and bound to the record as\n * additional data, so the database names no one and a record moved to another owner or thread\n * fails to open (and is dropped). No key, no mirror: every write and read is then a no-op.\n * - **Bounded.** Writes are debounced per thread and never taken mid-stream (a state with a live\n * run is replaced by its resting successor within seconds), transcripts longer than\n * `maxMessages` are not kept (an older, shorter record is removed rather than left to seed),\n * and an owner keeps its `maxPerOwner` most recent threads.\n * - **Never in the way.** Every failure degrades to \"no mirror\": a blocked or missing IndexedDB\n * (private browsing, quota, SSR) must never break the wire path.\n * - **Versioned.** A record a newer build wrote is never read, overwritten or pruned by this one;\n * only removing the person's records altogether (an identity change, a sign-out) removes it.\n */\n\nexport const THREAD_SNAPSHOT_MIRROR_DATABASE = \"athena-thread-snapshots:v2\";\n/** The sealed record's format; older builds leave a newer one alone. */\nexport const THREAD_SNAPSHOT_MIRROR_FORMAT = 1;\n/** Threads mirrored per owner; the least recently captured beyond this is pruned after each write. */\nexport const MAX_MIRRORED_SNAPSHOTS_PER_OWNER = 30;\n/** Transcripts longer than this are not mirrored — sealing them would cost more than the seed saves. */\nexport const MAX_MIRRORED_SNAPSHOT_MESSAGES = 150;\n/** Trailing debounce per thread: a streaming thread remembers a state per token. */\nexport const SNAPSHOT_MIRROR_DEBOUNCE_MS = 1_500;\n\nconst DATABASE_VERSION = 1;\nconst SNAPSHOTS_STORE = \"snapshots\";\nconst LISTS_STORE = \"lists\";\nconst BY_OWNER = \"byOwner\";\nconst BY_OWNER_AT = \"byOwnerAt\";\nconst IV_BYTES = 12;\n\nexport interface MirroredThreadSnapshot {\n threadId: string;\n state: DeepAgentStreamValues;\n /** Capture time (epoch ms). */\n at: number;\n}\n\nexport interface MirroredList {\n value: unknown;\n /** When it was written (epoch ms). */\n at: number;\n}\n\nexport interface ThreadSnapshotMirror {\n /** The owner's mirrored snapshots, oldest first. Resolves `[]` on any fault. */\n load(ownerId: string): Promise<MirroredThreadSnapshot[]>;\n /** Mirror a remembered state (debounced per thread; mid-stream states are skipped). */\n write(input: { ownerId: string; threadId: string; state: DeepAgentStreamValues; at: number }): void;\n remove(input: { ownerId: string; threadId: string }): void;\n /** A session list's page as the host last read it, for reading it offline. */\n writeList(input: { ownerId: string; listKey: string; value: unknown }): void;\n loadList(input: { ownerId: string; listKey: string }): Promise<MirroredList | null>;\n /** Drop every record of one owner (an identity change). */\n clearOwner(ownerId: string): Promise<void>;\n /** Drop every record (a sign-out, a workspace switch). */\n clearAll(): Promise<void>;\n /** Write debounced records now (tests, page hide). */\n flush(): Promise<void>;\n}\n\n/**\n * What a key is asked for: reading what is already kept (`read`) or keeping something new\n * (`write`). A host may let what it kept be read where it would keep nothing new, e.g. while its\n * deployment's policy for device copies is not known yet.\n */\nexport type ThreadSnapshotKeyPurpose = \"read\" | \"write\";\n\nexport interface ThreadSnapshotMirrorOptions {\n /** The owner's AES-GCM key, or `null` when the host keeps no mirror for it (no key, policy off). */\n getKey: (ownerId: string, purpose: ThreadSnapshotKeyPurpose) => Promise<CryptoKey | null>;\n /** An `IDBFactory` (the browser's `indexedDB` by default; a fake one in tests). */\n factory?: object;\n databaseName?: string;\n debounceMs?: number;\n maxPerOwner?: number;\n maxMessages?: number;\n}\n\n// This package compiles without the DOM lib (React Native honesty, `globals.d.ts`), so the part of\n// IndexedDB the mirror uses is described structurally here; the mirror runs only where it exists.\ntype IdbListener = ((event?: unknown) => void) | null;\ninterface IdbRequest<T> {\n readonly result: T;\n readonly error: unknown;\n onsuccess: IdbListener;\n onerror: IdbListener;\n}\ninterface IdbOpenRequest extends IdbRequest<IdbDatabase> {\n onupgradeneeded: IdbListener;\n onblocked: IdbListener;\n}\ninterface IdbFactory {\n open(name: string, version?: number): IdbOpenRequest;\n}\ninterface IdbCursor {\n readonly primaryKey: unknown;\n continue(): void;\n}\ninterface IdbIndex {\n getAll(query?: unknown): IdbRequest<unknown[]>;\n getAllKeys(query?: unknown): IdbRequest<unknown[]>;\n openKeyCursor(range?: unknown, direction?: string): IdbRequest<IdbCursor | null>;\n}\ninterface IdbObjectStore {\n put(value: unknown): IdbRequest<unknown>;\n get(key: unknown): IdbRequest<unknown>;\n delete(key: unknown): IdbRequest<unknown>;\n clear(): IdbRequest<unknown>;\n index(name: string): IdbIndex;\n createIndex(name: string, keyPath: string | string[], options?: { unique?: boolean }): unknown;\n}\ninterface IdbTransaction {\n objectStore(name: string): IdbObjectStore;\n readonly error: unknown;\n oncomplete: IdbListener;\n onabort: IdbListener;\n onerror: IdbListener;\n}\ninterface IdbDatabase {\n readonly objectStoreNames: { contains(name: string): boolean };\n transaction(stores: string | string[], mode?: \"readonly\" | \"readwrite\"): IdbTransaction;\n createObjectStore(name: string, options?: { keyPath: string }): IdbObjectStore;\n onversionchange: IdbListener;\n addEventListener(type: string, listener: () => void): void;\n close(): void;\n}\ninterface IdbKeyRangeStatic {\n bound(lower: unknown, upper: unknown): unknown;\n}\n\nconst idbGlobals = globalThis as { indexedDB?: object; IDBKeyRange?: IdbKeyRangeStatic };\n\ninterface SnapshotRecord {\n key: string;\n owner: string;\n at: number;\n format: number;\n sealed: Uint8Array;\n}\n\ninterface ListRecord {\n key: string;\n owner: string;\n at: number;\n format: number;\n sealed: Uint8Array;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nasync function sha256Hex(value: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", encoder.encode(value));\n return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nasync function sealJson({ key, value, aad }: { key: CryptoKey; value: unknown; aad: string }) {\n const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv, additionalData: encoder.encode(aad) },\n key,\n encoder.encode(JSON.stringify(value)),\n ),\n );\n const sealed = new Uint8Array(IV_BYTES + ciphertext.byteLength);\n sealed.set(iv, 0);\n sealed.set(ciphertext, IV_BYTES);\n return sealed;\n}\n\nasync function openJson({ key, sealed, aad }: { key: CryptoKey; sealed: Uint8Array; aad: string }) {\n const iv = sealed.subarray(0, IV_BYTES) as Uint8Array<ArrayBuffer>;\n const plaintext = await crypto.subtle.decrypt(\n { name: \"AES-GCM\", iv, additionalData: encoder.encode(aad) },\n key,\n sealed.subarray(IV_BYTES) as Uint8Array<ArrayBuffer>,\n );\n return JSON.parse(decoder.decode(plaintext)) as unknown;\n}\n\nfunction requestToPromise<T>(request: IdbRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error ?? new Error(\"IndexedDB request failed\"));\n });\n}\n\nfunction transactionDone(tx: IdbTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"IndexedDB transaction aborted\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB transaction failed\"));\n });\n}\n\nfunction openDatabase(factory: IdbFactory, name: string): Promise<IdbDatabase | null> {\n return new Promise((resolve) => {\n try {\n const request = factory.open(name, DATABASE_VERSION);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(SNAPSHOTS_STORE)) {\n const store = db.createObjectStore(SNAPSHOTS_STORE, { keyPath: \"key\" });\n store.createIndex(BY_OWNER, \"owner\", { unique: false });\n store.createIndex(BY_OWNER_AT, [\"owner\", \"at\"], { unique: false });\n }\n if (!db.objectStoreNames.contains(LISTS_STORE)) {\n const lists = db.createObjectStore(LISTS_STORE, { keyPath: \"key\" });\n lists.createIndex(BY_OWNER, \"owner\", { unique: false });\n }\n };\n request.onsuccess = () => {\n const db = request.result;\n // Another page deleting or upgrading the database: step aside.\n db.onversionchange = () => db.close();\n resolve(db);\n };\n // A database a newer build upgraded past this version is left alone.\n request.onerror = () => resolve(null);\n request.onblocked = () => resolve(null);\n } catch {\n resolve(null);\n }\n });\n}\n\n/**\n * Whether a remembered state is a resting transcript: no run in flight (its own or an out-of-band\n * one) and its messages present. Only such a state is kept, and read while Athena can't be reached;\n * a mid-stream state is a partial transcript.\n */\nexport function isRestingThreadSnapshot(state: DeepAgentStreamValues): boolean {\n const run = state.runs?.[0];\n if (run && run.status === \"running\") return false;\n if (state.outOfBandRunActive === true) return false;\n return Array.isArray(state.messages);\n}\n\n/**\n * What the mirror does with a remembered state: `persist` a resting transcript within the size\n * bound; `skip` a mid-stream state (it arrives per token and its resting successor follows within\n * seconds — the last resting record stays); `invalidate` when the transcript has outgrown the\n * mirror — a stale, shorter record would otherwise seed the next reload until the hello.\n */\nexport function classifyThreadSnapshotForMirror(\n state: DeepAgentStreamValues,\n maxMessages: number = MAX_MIRRORED_SNAPSHOT_MESSAGES,\n): \"persist\" | \"skip\" | \"invalidate\" {\n if (!isRestingThreadSnapshot(state)) return \"skip\";\n return (state.messages?.length ?? 0) <= maxMessages ? \"persist\" : \"invalidate\";\n}\n\n/** A record this build may replace or delete: none, or one in a format it writes itself. */\nfunction isOwnFormat(record: unknown): boolean {\n if (typeof record !== \"object\" || record === null) return true;\n const format = (record as { format?: unknown }).format;\n return typeof format !== \"number\" || format <= THREAD_SNAPSHOT_MIRROR_FORMAT;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nexport function createThreadSnapshotMirror({\n getKey,\n factory = idbGlobals.indexedDB,\n databaseName = THREAD_SNAPSHOT_MIRROR_DATABASE,\n debounceMs = SNAPSHOT_MIRROR_DEBOUNCE_MS,\n maxPerOwner = MAX_MIRRORED_SNAPSHOTS_PER_OWNER,\n maxMessages = MAX_MIRRORED_SNAPSHOT_MESSAGES,\n}: ThreadSnapshotMirrorOptions): ThreadSnapshotMirror {\n let databasePromise: Promise<IdbDatabase | null> | null = null;\n const database = (): Promise<IdbDatabase | null> => {\n if (!factory || typeof crypto === \"undefined\" || !crypto.subtle) return Promise.resolve(null);\n databasePromise ??= openDatabase(factory as IdbFactory, databaseName).then((db) => {\n if (db) {\n const forget = () => {\n databasePromise = null;\n };\n db.addEventListener(\"versionchange\", forget);\n db.addEventListener(\"close\", forget);\n } else {\n databasePromise = null;\n }\n return db;\n });\n return databasePromise;\n };\n const keyOf = async (ownerId: string, purpose: ThreadSnapshotKeyPurpose): Promise<CryptoKey | null> => {\n try {\n return await getKey(ownerId, purpose);\n } catch {\n return null;\n }\n };\n const aadOf = (kind: string, owner: string, key: string): string =>\n `athena-thread-snapshot|${THREAD_SNAPSHOT_MIRROR_FORMAT}|${kind}|${owner}|${key}`;\n\n const pending = new Map<\n string,\n { input: { ownerId: string; threadId: string; state: DeepAgentStreamValues; at: number }; timer: ReturnType<typeof setTimeout> }\n >();\n const pendingLists = new Map<\n string,\n { input: { ownerId: string; listKey: string; value: unknown }; timer: ReturnType<typeof setTimeout> }\n >();\n // Writes are serialized so a flush never races a clear.\n let queue: Promise<void> = Promise.resolve();\n const enqueue = (task: () => Promise<void>): Promise<void> => {\n queue = queue.then(task, task).catch(() => {});\n return queue;\n };\n\n const pruneOwner = async (db: IdbDatabase, owner: string): Promise<void> => {\n const KeyRange = idbGlobals.IDBKeyRange;\n if (!KeyRange) return;\n const tx = db.transaction(SNAPSHOTS_STORE, \"readwrite\");\n const store = tx.objectStore(SNAPSHOTS_STORE);\n const index = store.index(BY_OWNER_AT);\n // Key cursor, newest first: bookkeeping never loads the sealed payloads of the records it keeps.\n const range = KeyRange.bound([owner, Number.NEGATIVE_INFINITY], [owner, Number.POSITIVE_INFINITY]);\n const beyond: unknown[] = [];\n await new Promise<void>((resolve, reject) => {\n let kept = 0;\n const request = index.openKeyCursor(range, \"prev\");\n request.onsuccess = () => {\n const cursor = request.result;\n if (!cursor) {\n resolve();\n return;\n }\n kept += 1;\n if (kept > maxPerOwner) beyond.push(cursor.primaryKey);\n cursor.continue();\n };\n request.onerror = () => reject(request.error ?? new Error(\"IndexedDB cursor failed\"));\n });\n // A newer build's record is left to that build's own bound.\n for (const key of beyond) {\n if (isOwnFormat(await requestToPromise(store.get(key)))) store.delete(key);\n }\n await transactionDone(tx);\n };\n\n const put = (input: { ownerId: string; threadId: string; state: DeepAgentStreamValues; at: number }) =>\n enqueue(async () => {\n const db = await database();\n if (!db) return;\n const cryptoKey = await keyOf(input.ownerId, \"write\");\n if (!cryptoKey) return;\n const owner = await sha256Hex(input.ownerId);\n const key = await sha256Hex(`${input.ownerId}|${input.threadId}`);\n const sealed = await sealJson({\n key: cryptoKey,\n value: { threadId: input.threadId, state: input.state },\n aad: aadOf(\"snapshot\", owner, key),\n });\n const tx = db.transaction(SNAPSHOTS_STORE, \"readwrite\");\n const store = tx.objectStore(SNAPSHOTS_STORE);\n // A newer build's record of the thread stays as that build wrote it.\n if (isOwnFormat(await requestToPromise(store.get(key)))) {\n store.put({\n key,\n owner,\n at: input.at,\n format: THREAD_SNAPSHOT_MIRROR_FORMAT,\n sealed,\n } satisfies SnapshotRecord);\n }\n await transactionDone(tx);\n await pruneOwner(db, owner);\n });\n\n const deleteSnapshot = (input: { ownerId: string; threadId: string }) =>\n enqueue(async () => {\n const db = await database();\n if (!db) return;\n const key = await sha256Hex(`${input.ownerId}|${input.threadId}`);\n const tx = db.transaction(SNAPSHOTS_STORE, \"readwrite\");\n const store = tx.objectStore(SNAPSHOTS_STORE);\n if (isOwnFormat(await requestToPromise(store.get(key)))) store.delete(key);\n await transactionDone(tx);\n });\n\n const pendingKey = (ownerId: string, threadId: string) => `${ownerId}\\u0000${threadId}`;\n\n const flushPending = (key: string): Promise<void> => {\n const entry = pending.get(key);\n if (!entry) return Promise.resolve();\n pending.delete(key);\n clearTimeout(entry.timer);\n return put(entry.input);\n };\n\n const putList = (input: { ownerId: string; listKey: string; value: unknown }) =>\n enqueue(async () => {\n const db = await database();\n if (!db) return;\n const cryptoKey = await keyOf(input.ownerId, \"write\");\n if (!cryptoKey) return;\n const owner = await sha256Hex(input.ownerId);\n const key = await sha256Hex(`${input.ownerId}|list|${input.listKey}`);\n const sealed = await sealJson({ key: cryptoKey, value: input.value, aad: aadOf(\"list\", owner, key) });\n const tx = db.transaction(LISTS_STORE, \"readwrite\");\n const store = tx.objectStore(LISTS_STORE);\n if (isOwnFormat(await requestToPromise(store.get(key)))) {\n store.put({\n key,\n owner,\n at: Date.now(),\n format: THREAD_SNAPSHOT_MIRROR_FORMAT,\n sealed,\n } satisfies ListRecord);\n }\n await transactionDone(tx);\n });\n\n const flushList = (key: string): Promise<void> => {\n const entry = pendingLists.get(key);\n if (!entry) return Promise.resolve();\n pendingLists.delete(key);\n clearTimeout(entry.timer);\n return putList(entry.input);\n };\n\n const dropPendingOf = (ownerId: string | null): void => {\n for (const [key, entry] of pending) {\n if (ownerId === null || entry.input.ownerId === ownerId) {\n clearTimeout(entry.timer);\n pending.delete(key);\n }\n }\n for (const [key, entry] of pendingLists) {\n if (ownerId === null || entry.input.ownerId === ownerId) {\n clearTimeout(entry.timer);\n pendingLists.delete(key);\n }\n }\n };\n\n return {\n async load(ownerId) {\n try {\n const db = await database();\n if (!db) return [];\n const cryptoKey = await keyOf(ownerId, \"read\");\n if (!cryptoKey) return [];\n const owner = await sha256Hex(ownerId);\n const records = (await requestToPromise(\n db.transaction(SNAPSHOTS_STORE, \"readonly\").objectStore(SNAPSHOTS_STORE).index(BY_OWNER).getAll(owner),\n )) as SnapshotRecord[];\n const loaded: MirroredThreadSnapshot[] = [];\n const unreadable: string[] = [];\n for (const record of records) {\n if (!record || record.format > THREAD_SNAPSHOT_MIRROR_FORMAT) continue;\n try {\n const value = await openJson({\n key: cryptoKey,\n sealed: record.sealed,\n aad: aadOf(\"snapshot\", owner, record.key),\n });\n if (\n isRecord(value) &&\n typeof value.threadId === \"string\" &&\n isRecord(value.state) &&\n typeof record.at === \"number\"\n ) {\n loaded.push({ threadId: value.threadId, state: value.state as DeepAgentStreamValues, at: record.at });\n }\n } catch {\n unreadable.push(record.key);\n }\n }\n if (unreadable.length > 0) {\n void enqueue(async () => {\n const tx = db.transaction(SNAPSHOTS_STORE, \"readwrite\");\n for (const key of unreadable) tx.objectStore(SNAPSHOTS_STORE).delete(key);\n await transactionDone(tx);\n });\n }\n return loaded.sort((a, b) => a.at - b.at);\n } catch {\n return [];\n }\n },\n write(input) {\n const key = pendingKey(input.ownerId, input.threadId);\n const verdict = classifyThreadSnapshotForMirror(input.state, maxMessages);\n if (verdict === \"invalidate\") {\n const existing = pending.get(key);\n if (existing) clearTimeout(existing.timer);\n pending.delete(key);\n void deleteSnapshot(input);\n return;\n }\n if (verdict === \"skip\") return;\n const existing = pending.get(key);\n if (existing) clearTimeout(existing.timer);\n pending.set(key, {\n input,\n timer: setTimeout(() => {\n void flushPending(key);\n }, debounceMs),\n });\n },\n remove(input) {\n const key = pendingKey(input.ownerId, input.threadId);\n const existing = pending.get(key);\n if (existing) clearTimeout(existing.timer);\n pending.delete(key);\n void deleteSnapshot(input);\n },\n writeList(input) {\n const key = pendingKey(input.ownerId, `list:${input.listKey}`);\n const existing = pendingLists.get(key);\n if (existing) clearTimeout(existing.timer);\n pendingLists.set(key, {\n input,\n timer: setTimeout(() => {\n void flushList(key);\n }, debounceMs),\n });\n },\n async loadList({ ownerId, listKey }) {\n try {\n const db = await database();\n if (!db) return null;\n const cryptoKey = await keyOf(ownerId, \"read\");\n if (!cryptoKey) return null;\n const owner = await sha256Hex(ownerId);\n const key = await sha256Hex(`${ownerId}|list|${listKey}`);\n const record = (await requestToPromise(\n db.transaction(LISTS_STORE, \"readonly\").objectStore(LISTS_STORE).get(key),\n )) as ListRecord | undefined;\n if (!record || record.format > THREAD_SNAPSHOT_MIRROR_FORMAT || record.owner !== owner) return null;\n try {\n const value = await openJson({ key: cryptoKey, sealed: record.sealed, aad: aadOf(\"list\", owner, key) });\n return { value, at: record.at };\n } catch {\n return null;\n }\n } catch {\n return null;\n }\n },\n clearOwner(ownerId) {\n dropPendingOf(ownerId);\n return enqueue(async () => {\n const db = await database();\n if (!db) return;\n const owner = await sha256Hex(ownerId);\n const tx = db.transaction([SNAPSHOTS_STORE, LISTS_STORE], \"readwrite\");\n for (const storeName of [SNAPSHOTS_STORE, LISTS_STORE]) {\n const store = tx.objectStore(storeName);\n const keys = await requestToPromise(store.index(BY_OWNER).getAllKeys(owner));\n for (const key of keys) store.delete(key);\n }\n await transactionDone(tx);\n });\n },\n clearAll() {\n dropPendingOf(null);\n return enqueue(async () => {\n const db = await database();\n if (!db) return;\n const tx = db.transaction([SNAPSHOTS_STORE, LISTS_STORE], \"readwrite\");\n tx.objectStore(SNAPSHOTS_STORE).clear();\n tx.objectStore(LISTS_STORE).clear();\n await transactionDone(tx);\n });\n },\n async flush() {\n await Promise.all([...pending.keys()].map(flushPending));\n await Promise.all([...pendingLists.keys()].map(flushList));\n await queue;\n },\n };\n}\n"],"names":["existing"],"mappings":"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"}
|