@athenaintel/react 0.41.2 → 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 +6 -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/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/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
|
@@ -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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"}
|