@opendatalabs/personal-server-ts-lite 0.0.1-canary.00328bf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/bridge.d.ts +23 -0
  2. package/dist/bridge.d.ts.map +1 -0
  3. package/dist/bridge.js +53 -0
  4. package/dist/bridge.js.map +1 -0
  5. package/dist/browser-runtime.d.ts +58 -0
  6. package/dist/browser-runtime.d.ts.map +1 -0
  7. package/dist/browser-runtime.js +214 -0
  8. package/dist/browser-runtime.js.map +1 -0
  9. package/dist/browser-tls-rustls/browser_tls_rustls.d.ts +49 -0
  10. package/dist/browser-tls-rustls/browser_tls_rustls.js +401 -0
  11. package/dist/browser-tls-rustls/browser_tls_rustls_bg.wasm +0 -0
  12. package/dist/client.d.ts +22 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +408 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/derivatives.d.ts +51 -0
  17. package/dist/derivatives.d.ts.map +1 -0
  18. package/dist/derivatives.js +87 -0
  19. package/dist/derivatives.js.map +1 -0
  20. package/dist/diagnostics.d.ts +166 -0
  21. package/dist/diagnostics.d.ts.map +1 -0
  22. package/dist/diagnostics.js +265 -0
  23. package/dist/diagnostics.js.map +1 -0
  24. package/dist/index.d.ts +18 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +19 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/mcp-store.d.ts +35 -0
  29. package/dist/mcp-store.d.ts.map +1 -0
  30. package/dist/mcp-store.js +208 -0
  31. package/dist/mcp-store.js.map +1 -0
  32. package/dist/owner-binding.d.ts +7 -0
  33. package/dist/owner-binding.d.ts.map +1 -0
  34. package/dist/owner-binding.js +18 -0
  35. package/dist/owner-binding.js.map +1 -0
  36. package/dist/persistence.d.ts +69 -0
  37. package/dist/persistence.d.ts.map +1 -0
  38. package/dist/persistence.js +58 -0
  39. package/dist/persistence.js.map +1 -0
  40. package/dist/relay-tls.d.ts +61 -0
  41. package/dist/relay-tls.d.ts.map +1 -0
  42. package/dist/relay-tls.js +316 -0
  43. package/dist/relay-tls.js.map +1 -0
  44. package/dist/relay.d.ts +149 -0
  45. package/dist/relay.d.ts.map +1 -0
  46. package/dist/relay.js +646 -0
  47. package/dist/relay.js.map +1 -0
  48. package/dist/runtime.d.ts +188 -0
  49. package/dist/runtime.d.ts.map +1 -0
  50. package/dist/runtime.js +1178 -0
  51. package/dist/runtime.js.map +1 -0
  52. package/dist/state.d.ts +72 -0
  53. package/dist/state.d.ts.map +1 -0
  54. package/dist/state.js +366 -0
  55. package/dist/state.js.map +1 -0
  56. package/dist/storage-utils.d.ts +8 -0
  57. package/dist/storage-utils.d.ts.map +1 -0
  58. package/dist/storage-utils.js +67 -0
  59. package/dist/storage-utils.js.map +1 -0
  60. package/dist/storage.d.ts +61 -0
  61. package/dist/storage.d.ts.map +1 -0
  62. package/dist/storage.js +718 -0
  63. package/dist/storage.js.map +1 -0
  64. package/dist/sync.d.ts +58 -0
  65. package/dist/sync.d.ts.map +1 -0
  66. package/dist/sync.js +216 -0
  67. package/dist/sync.js.map +1 -0
  68. package/package.json +40 -0
@@ -0,0 +1,718 @@
1
+ import { buildDataBlocksAsync } from "@opendatalabs/personal-server-ts-core/storage/blocks";
2
+ import { DataBlockStorageError, encodeDataBlockCursor, selectScopeBlocksByIds, validateDataBlockCursor, } from "@opendatalabs/personal-server-ts-core/storage/blocks";
3
+ import { createStorageReadMethods, sortEntries } from "./storage-utils.js";
4
+ import { previewEnvelopeValue, previewJsonEnvelopePrefix, } from "@opendatalabs/personal-server-ts-core/storage/preview";
5
+ const DEFAULT_INDEXED_DB_NAME = "personal-server-lite-storage";
6
+ const DEFAULT_INDEXED_DB_STORE = "state";
7
+ const DEFAULT_INDEXED_DB_KEY = "data-storage-v1";
8
+ const TEXT_PAGE_MEDIA_TYPE = "text/plain; charset=utf-8";
9
+ const textEncoder = new TextEncoder();
10
+ const textDecoder = new TextDecoder();
11
+ function schemaIdFromEnvelope(envelope) {
12
+ const schemaId = envelope.schemaId;
13
+ return typeof schemaId === "string" ? schemaId : undefined;
14
+ }
15
+ function initialState() {
16
+ return {
17
+ version: 1,
18
+ nextId: 1,
19
+ entries: [],
20
+ envelopes: [],
21
+ };
22
+ }
23
+ function envelopePath(scope, collectedAt) {
24
+ return `data/${scope}/${collectedAt}.json`;
25
+ }
26
+ function blockTreePath(scope, collectedAt) {
27
+ return `blocks/${scope}/${collectedAt}`;
28
+ }
29
+ function blockManifestPath(scope, collectedAt) {
30
+ return `${blockTreePath(scope, collectedAt)}/manifest.json`;
31
+ }
32
+ function blockPayloadPath(scope, collectedAt, blockId) {
33
+ return `${blockTreePath(scope, collectedAt)}/${encodeURIComponent(blockId)}.json`;
34
+ }
35
+ function normalizeState(state) {
36
+ if (!state || state.version !== 1) {
37
+ return initialState();
38
+ }
39
+ return {
40
+ version: 1,
41
+ nextId: Math.max(state.nextId, 1),
42
+ entries: state.entries.map((entry) => ({
43
+ ...entry,
44
+ schemaId: entry.schemaId ?? null,
45
+ })),
46
+ envelopes: state.envelopes,
47
+ blockManifests: state.blockManifests ?? [],
48
+ blockPayloads: state.blockPayloads ?? [],
49
+ };
50
+ }
51
+ export function createIndexedDbFallbackDataFileStore(envelopes, blockManifests = new Map(), blockPayloads = new Map()) {
52
+ return {
53
+ kind: "indexeddb",
54
+ async readEnvelope(path) {
55
+ return envelopes.get(path) ?? null;
56
+ },
57
+ async readEnvelopePreview(path, { maxBytes }) {
58
+ const envelope = envelopes.get(path);
59
+ if (!envelope)
60
+ return null;
61
+ return previewEnvelopeValue(envelope, maxBytes);
62
+ },
63
+ async writeEnvelope(path, envelope) {
64
+ envelopes.set(path, envelope);
65
+ return new TextEncoder().encode(JSON.stringify(envelope)).length;
66
+ },
67
+ async deleteEnvelope(path) {
68
+ envelopes.delete(path);
69
+ },
70
+ async readBlockManifest(path) {
71
+ return blockManifests.get(path) ?? null;
72
+ },
73
+ async writeBlockManifest(path, manifest) {
74
+ blockManifests.set(path, manifest);
75
+ },
76
+ async readBlockPayload(path) {
77
+ return blockPayloads.get(path) ?? null;
78
+ },
79
+ async writeBlockPayload(path, block) {
80
+ blockPayloads.set(path, block);
81
+ },
82
+ async deleteBlockTree(pathPrefix) {
83
+ deleteMapPrefix(blockManifests, pathPrefix);
84
+ deleteMapPrefix(blockPayloads, pathPrefix);
85
+ },
86
+ };
87
+ }
88
+ function deleteMapPrefix(map, pathPrefix) {
89
+ const prefix = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`;
90
+ for (const path of map.keys()) {
91
+ if (path === pathPrefix || path.startsWith(prefix)) {
92
+ map.delete(path);
93
+ }
94
+ }
95
+ }
96
+ function describeStorageError(error) {
97
+ if (error instanceof Error) {
98
+ return { errorClass: error.name, message: error.message };
99
+ }
100
+ return { errorClass: typeof error, message: String(error) };
101
+ }
102
+ async function getOrCreateOpfsDirectory(root, parts) {
103
+ let dir = root;
104
+ for (const part of parts) {
105
+ dir = await dir.getDirectoryHandle(part, { create: true });
106
+ }
107
+ return dir;
108
+ }
109
+ async function getOpfsFileHandle(root, path, options) {
110
+ const parts = path.split("/").filter(Boolean);
111
+ const fileName = parts.pop();
112
+ if (!fileName) {
113
+ throw new Error("OPFS path must include a file name");
114
+ }
115
+ const dir = await getOrCreateOpfsDirectory(root, parts);
116
+ return dir.getFileHandle(fileName, options);
117
+ }
118
+ export async function isOpfsAvailable() {
119
+ return (typeof navigator !== "undefined" &&
120
+ typeof navigator.storage?.getDirectory === "function");
121
+ }
122
+ export async function createOpfsPsLiteDataFileStore() {
123
+ if (!(await isOpfsAvailable())) {
124
+ throw new Error("OPFS is not available in this runtime");
125
+ }
126
+ const root = await navigator.storage.getDirectory();
127
+ return {
128
+ kind: "opfs",
129
+ async readEnvelope(path) {
130
+ try {
131
+ const handle = await getOpfsFileHandle(root, path);
132
+ const file = await handle.getFile();
133
+ return JSON.parse(await file.text());
134
+ }
135
+ catch (err) {
136
+ if (err instanceof DOMException && err.name === "NotFoundError") {
137
+ return null;
138
+ }
139
+ throw err;
140
+ }
141
+ },
142
+ async readEnvelopePreview(path, { maxBytes }) {
143
+ try {
144
+ const handle = await getOpfsFileHandle(root, path);
145
+ const file = await handle.getFile();
146
+ return previewJsonEnvelopePrefix(await file.slice(0, maxBytes).text(), maxBytes, {
147
+ sourceTruncated: file.size > maxBytes,
148
+ });
149
+ }
150
+ catch (err) {
151
+ if (err instanceof DOMException && err.name === "NotFoundError") {
152
+ return null;
153
+ }
154
+ throw err;
155
+ }
156
+ },
157
+ async writeEnvelope(path, envelope) {
158
+ const encoded = JSON.stringify(envelope);
159
+ const handle = await getOpfsFileHandle(root, path, { create: true });
160
+ const writable = await handle.createWritable();
161
+ await writable.write(encoded);
162
+ await writable.close();
163
+ return new TextEncoder().encode(encoded).length;
164
+ },
165
+ async deleteEnvelope(path) {
166
+ const parts = path.split("/").filter(Boolean);
167
+ const fileName = parts.pop();
168
+ if (!fileName)
169
+ return;
170
+ try {
171
+ const dir = await getOrCreateOpfsDirectory(root, parts);
172
+ await dir.removeEntry(fileName);
173
+ }
174
+ catch (err) {
175
+ if (err instanceof DOMException && err.name === "NotFoundError") {
176
+ return;
177
+ }
178
+ throw err;
179
+ }
180
+ },
181
+ async readBlockManifest(path) {
182
+ return readJsonOpfsFile(root, path);
183
+ },
184
+ async writeBlockManifest(path, manifest) {
185
+ await writeJsonOpfsFile(root, path, manifest);
186
+ },
187
+ async readBlockPayload(path) {
188
+ return readJsonOpfsFile(root, path);
189
+ },
190
+ async writeBlockPayload(path, block) {
191
+ await writeJsonOpfsFile(root, path, block);
192
+ },
193
+ async deleteBlockTree(pathPrefix) {
194
+ await removeOpfsDirectoryTree(root, pathPrefix);
195
+ },
196
+ };
197
+ }
198
+ async function readJsonOpfsFile(root, path) {
199
+ try {
200
+ const handle = await getOpfsFileHandle(root, path);
201
+ const file = await handle.getFile();
202
+ return JSON.parse(await file.text());
203
+ }
204
+ catch (err) {
205
+ if (err instanceof DOMException && err.name === "NotFoundError") {
206
+ return null;
207
+ }
208
+ throw err;
209
+ }
210
+ }
211
+ async function writeJsonOpfsFile(root, path, value) {
212
+ const handle = await getOpfsFileHandle(root, path, { create: true });
213
+ const writable = await handle.createWritable();
214
+ try {
215
+ await writable.write(JSON.stringify(value));
216
+ }
217
+ finally {
218
+ await writable.close();
219
+ }
220
+ }
221
+ async function removeOpfsDirectoryTree(root, pathPrefix) {
222
+ const parts = pathPrefix.split("/").filter(Boolean);
223
+ const directoryName = parts.pop();
224
+ if (!directoryName)
225
+ return;
226
+ try {
227
+ const parent = await getOrCreateOpfsDirectory(root, parts);
228
+ await parent.removeEntry(directoryName, { recursive: true });
229
+ }
230
+ catch (err) {
231
+ if (err instanceof DOMException && err.name === "NotFoundError") {
232
+ return;
233
+ }
234
+ throw err;
235
+ }
236
+ }
237
+ function openIndexedDb(dbName, storeName) {
238
+ if (typeof indexedDB === "undefined") {
239
+ throw new Error("IndexedDB is not available in this runtime");
240
+ }
241
+ return new Promise((resolve, reject) => {
242
+ const request = indexedDB.open(dbName, 1);
243
+ request.onupgradeneeded = () => {
244
+ const db = request.result;
245
+ if (!db.objectStoreNames.contains(storeName)) {
246
+ db.createObjectStore(storeName);
247
+ }
248
+ };
249
+ request.onsuccess = () => resolve(request.result);
250
+ request.onerror = () => reject(request.error);
251
+ });
252
+ }
253
+ function runIndexedDbTransaction(options, mode, callback) {
254
+ return openIndexedDb(options.dbName, options.storeName).then((db) => new Promise((resolve, reject) => {
255
+ const transaction = db.transaction(options.storeName, mode);
256
+ const store = transaction.objectStore(options.storeName);
257
+ const request = callback(store);
258
+ request.onsuccess = () => resolve(request.result);
259
+ request.onerror = () => reject(request.error);
260
+ transaction.oncomplete = () => db.close();
261
+ transaction.onerror = () => {
262
+ db.close();
263
+ reject(transaction.error);
264
+ };
265
+ }));
266
+ }
267
+ export function createIndexedDbPsLitePersistence(options = {}) {
268
+ const resolved = {
269
+ dbName: options.dbName ?? DEFAULT_INDEXED_DB_NAME,
270
+ storeName: options.storeName ?? DEFAULT_INDEXED_DB_STORE,
271
+ key: options.key ?? DEFAULT_INDEXED_DB_KEY,
272
+ };
273
+ return {
274
+ async read() {
275
+ const state = await runIndexedDbTransaction(resolved, "readonly", (store) => store.get(resolved.key));
276
+ return state ?? null;
277
+ },
278
+ async write(state) {
279
+ await runIndexedDbTransaction(resolved, "readwrite", (store) => store.put(state, resolved.key));
280
+ },
281
+ };
282
+ }
283
+ export async function createPersistentPsLiteStorage(adapter, persistence = createIndexedDbPsLitePersistence(), dataFileStore) {
284
+ let state = normalizeState(await persistence.read());
285
+ const fallbackEnvelopes = new Map(state.envelopes.map((envelope) => [
286
+ envelopePath(envelope.scope, envelope.collectedAt),
287
+ envelope,
288
+ ]));
289
+ const fallbackBlockManifests = new Map((state.blockManifests ?? []).map(({ path, manifest }) => [path, manifest]));
290
+ const fallbackBlockPayloads = new Map((state.blockPayloads ?? []).map(({ path, block }) => [path, block]));
291
+ const fallbackStore = createIndexedDbFallbackDataFileStore(fallbackEnvelopes, fallbackBlockManifests, fallbackBlockPayloads);
292
+ const fileStore = dataFileStore ??
293
+ (adapter.kind !== "custom" && (await isOpfsAvailable())
294
+ ? await createOpfsPsLiteDataFileStore()
295
+ : fallbackStore);
296
+ const capabilities = {
297
+ metadata: adapter.kind === "custom" ? "custom" : "indexeddb",
298
+ files: fileStore.kind,
299
+ opfsAvailable: await isOpfsAvailable(),
300
+ };
301
+ let persistQueue = Promise.resolve();
302
+ async function persist() {
303
+ const snapshot = {
304
+ ...state,
305
+ envelopes: fileStore.kind === "indexeddb"
306
+ ? Array.from(fallbackEnvelopes.values())
307
+ : [],
308
+ blockManifests: Array.from(fallbackBlockManifests.entries()).map(([path, manifest]) => ({ path, manifest })),
309
+ blockPayloads: Array.from(fallbackBlockPayloads.entries()).map(([path, block]) => ({ path, block })),
310
+ };
311
+ const write = persistQueue.then(() => persistence.write(snapshot));
312
+ persistQueue = write.catch(() => undefined);
313
+ await write;
314
+ }
315
+ function entriesForScope(scope) {
316
+ return sortEntries(state.entries.filter((entry) => entry.scope === scope));
317
+ }
318
+ const storagePort = {
319
+ kind: adapter.kind === "custom" ? "custom" : "browser-indexeddb-opfs",
320
+ capabilities,
321
+ ...createStorageReadMethods(() => state.entries, entriesForScope),
322
+ findByFileId(fileId) {
323
+ return state.entries.find((entry) => entry.fileId === fileId);
324
+ },
325
+ findByDataPointId(dataPointId) {
326
+ return state.entries.find((entry) => entry.dataPointId === dataPointId);
327
+ },
328
+ findUnsynced(options) {
329
+ const entries = state.entries
330
+ .filter((entry) => entry.dataPointId === null)
331
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
332
+ return options?.limit === undefined
333
+ ? entries
334
+ : entries.slice(0, options.limit);
335
+ },
336
+ async readEnvelope(scope, collectedAt) {
337
+ const path = envelopePath(scope, collectedAt);
338
+ const envelope = (await fileStore.readEnvelope(path)) ??
339
+ (await fallbackStore.readEnvelope(path));
340
+ if (!envelope) {
341
+ throw new Error("Envelope not found");
342
+ }
343
+ return envelope;
344
+ },
345
+ async writeEnvelope(envelope) {
346
+ const path = envelopePath(envelope.scope, envelope.collectedAt);
347
+ const sizeBytes = await fileStore.writeEnvelope(path, envelope);
348
+ if (fileStore.kind !== "indexeddb") {
349
+ await fallbackStore.deleteEnvelope(path);
350
+ }
351
+ await persist();
352
+ return {
353
+ path,
354
+ relativePath: path,
355
+ sizeBytes,
356
+ };
357
+ },
358
+ async readScopeBlocks(scope, collectedAt, options) {
359
+ const manifestPath = blockManifestPath(scope, collectedAt);
360
+ let manifest = (await fileStore.readBlockManifest?.(manifestPath)) ??
361
+ (fileStore === fallbackStore
362
+ ? null
363
+ : await fallbackStore.readBlockManifest?.(manifestPath)) ??
364
+ null;
365
+ let inMemoryBlocks = null;
366
+ if (!manifest) {
367
+ const envelope = await storagePort.readEnvelope(scope, collectedAt);
368
+ const built = await buildDataBlocksAsync({
369
+ scope,
370
+ collectedAt,
371
+ schemaId: schemaIdFromEnvelope(envelope),
372
+ content: envelope,
373
+ });
374
+ manifest = built.manifest;
375
+ inMemoryBlocks = built.blocks;
376
+ try {
377
+ await storagePort.writeBlockManifest?.(scope, collectedAt, built.manifest, built.blocks);
378
+ }
379
+ catch (error) {
380
+ console.warn({
381
+ scope,
382
+ collectedAt,
383
+ ...describeStorageError(error),
384
+ }, "Block sidecar cache write failed; serving read from local envelope");
385
+ }
386
+ }
387
+ if (options.blockIds?.length) {
388
+ const selection = await selectScopeBlocksByIds(manifest, options.blockIds, { maxBytes: options.maxBytes }, async (blockId) => inMemoryBlocks?.find((candidate) => candidate.id === blockId) ??
389
+ (await readBlockPayloadFromStores(fileStore, fallbackStore, blockPayloadPath(scope, collectedAt, blockId))));
390
+ return {
391
+ scope: manifest.scope,
392
+ collectedAt: manifest.collectedAt,
393
+ ...(manifest.schemaId ? { schemaId: manifest.schemaId } : {}),
394
+ contentKind: manifest.contentKind,
395
+ blocks: selection.blocks,
396
+ warnings: [...manifest.warnings, ...selection.warnings],
397
+ };
398
+ }
399
+ const cursorResult = options.cursor
400
+ ? validateDataBlockCursor(options.cursor, { scope, collectedAt })
401
+ : { ok: true, cursor: null };
402
+ if (!cursorResult.ok) {
403
+ throw new DataBlockStorageError("cursor_invalid", cursorResult.error.message);
404
+ }
405
+ const maxBytes = Math.max(1, options.maxBytes);
406
+ const startIndex = cursorResult.cursor?.blockIndex ?? 0;
407
+ const startOffset = cursorResult.cursor?.intraBlockOffset ?? 0;
408
+ const blocks = [];
409
+ let bytes = 0;
410
+ let nextIndex = startIndex;
411
+ let nextOffset;
412
+ while (nextIndex < manifest.blocks.length) {
413
+ const ref = manifest.blocks[nextIndex];
414
+ if (!ref) {
415
+ nextIndex += 1;
416
+ continue;
417
+ }
418
+ const offset = nextIndex === startIndex ? startOffset : 0;
419
+ if (offset >= ref.sizeBytes) {
420
+ nextIndex += 1;
421
+ continue;
422
+ }
423
+ if (blocks.length > 0 &&
424
+ offset === 0 &&
425
+ bytes + ref.sizeBytes > maxBytes) {
426
+ break;
427
+ }
428
+ const block = inMemoryBlocks?.find((candidate) => candidate.id === ref.id) ??
429
+ (await readBlockPayloadFromStores(fileStore, fallbackStore, blockPayloadPath(scope, collectedAt, ref.id)));
430
+ if (!block) {
431
+ const envelope = await storagePort.readEnvelope(scope, collectedAt);
432
+ const rebuilt = await buildDataBlocksAsync({
433
+ scope,
434
+ collectedAt,
435
+ schemaId: schemaIdFromEnvelope(envelope),
436
+ content: envelope,
437
+ });
438
+ manifest = rebuilt.manifest;
439
+ inMemoryBlocks = rebuilt.blocks;
440
+ nextIndex = startIndex;
441
+ nextOffset = undefined;
442
+ bytes = 0;
443
+ blocks.length = 0;
444
+ continue;
445
+ }
446
+ const page = pageBlock(block, offset, maxBytes - bytes);
447
+ blocks.push(page.block);
448
+ bytes += page.block.sizeBytes;
449
+ if (page.nextOffset !== undefined) {
450
+ nextOffset = page.nextOffset;
451
+ break;
452
+ }
453
+ nextIndex += 1;
454
+ }
455
+ return {
456
+ scope: manifest.scope,
457
+ collectedAt: manifest.collectedAt,
458
+ ...(manifest.schemaId ? { schemaId: manifest.schemaId } : {}),
459
+ contentKind: manifest.contentKind,
460
+ blocks,
461
+ ...(nextOffset !== undefined || nextIndex < manifest.blocks.length
462
+ ? {
463
+ nextCursor: encodeDataBlockCursor({
464
+ scope,
465
+ collectedAt,
466
+ blockIndex: nextIndex,
467
+ ...(nextOffset === undefined
468
+ ? {}
469
+ : { intraBlockOffset: nextOffset }),
470
+ }),
471
+ }
472
+ : {}),
473
+ warnings: manifest.warnings,
474
+ };
475
+ },
476
+ async readBlockManifest(scope, collectedAt) {
477
+ const manifestPath = blockManifestPath(scope, collectedAt);
478
+ return ((await fileStore.readBlockManifest?.(manifestPath)) ??
479
+ (fileStore === fallbackStore
480
+ ? null
481
+ : await fallbackStore.readBlockManifest?.(manifestPath)) ??
482
+ null);
483
+ },
484
+ async hasScopeBlocks(scope, collectedAt) {
485
+ return Boolean(await storagePort.readBlockManifest?.(scope, collectedAt));
486
+ },
487
+ async canReadScopeBlocks(scope, collectedAt) {
488
+ if (await storagePort.hasScopeBlocks?.(scope, collectedAt)) {
489
+ return true;
490
+ }
491
+ const path = envelopePath(scope, collectedAt);
492
+ const preview = (await readPreviewFromFileStore(fileStore, path, 1)) ??
493
+ (fileStore === fallbackStore
494
+ ? null
495
+ : await readPreviewFromFileStore(fallbackStore, path, 1));
496
+ if (preview)
497
+ return true;
498
+ const envelope = (await fileStore.readEnvelope(path)) ??
499
+ (fileStore === fallbackStore
500
+ ? null
501
+ : await fallbackStore.readEnvelope(path));
502
+ return Boolean(envelope);
503
+ },
504
+ async writeBlockManifest(scope, collectedAt, manifest, blocks) {
505
+ const writeTree = async (store) => {
506
+ if (!store.writeBlockManifest || !store.writeBlockPayload) {
507
+ throw new Error("Block sidecar storage is not available");
508
+ }
509
+ await store.deleteBlockTree?.(blockTreePath(scope, collectedAt));
510
+ for (const block of blocks) {
511
+ await store.writeBlockPayload(blockPayloadPath(scope, collectedAt, block.id), block);
512
+ }
513
+ await store.writeBlockManifest(blockManifestPath(scope, collectedAt), manifest);
514
+ };
515
+ let wrotePrimary = false;
516
+ try {
517
+ await writeTree(fileStore);
518
+ wrotePrimary = true;
519
+ }
520
+ catch (error) {
521
+ if (fileStore === fallbackStore) {
522
+ throw error;
523
+ }
524
+ console.warn({
525
+ scope,
526
+ collectedAt,
527
+ fileStoreKind: fileStore.kind,
528
+ fallbackStoreKind: fallbackStore.kind,
529
+ ...describeStorageError(error),
530
+ }, "Primary block sidecar write failed; using IndexedDB fallback");
531
+ await writeTree(fallbackStore);
532
+ }
533
+ if (wrotePrimary && fileStore.kind !== "indexeddb") {
534
+ await fallbackStore.deleteBlockTree?.(blockTreePath(scope, collectedAt));
535
+ }
536
+ await persist();
537
+ },
538
+ async insertEntry(entry) {
539
+ const version = entry.version ??
540
+ state.entries.reduce((max, e) => e.scope === entry.scope && e.version > max ? e.version : max, 0) + 1;
541
+ const indexed = {
542
+ ...entry,
543
+ schemaId: entry.schemaId ?? null,
544
+ version,
545
+ dataPointId: entry.dataPointId ?? null,
546
+ afterTombstoneVersion: entry.afterTombstoneVersion ?? null,
547
+ id: state.nextId,
548
+ createdAt: new Date().toISOString(),
549
+ };
550
+ state = {
551
+ ...state,
552
+ nextId: state.nextId + 1,
553
+ entries: [
554
+ ...state.entries.filter((item) => item.path !== entry.path),
555
+ indexed,
556
+ ],
557
+ };
558
+ await persist();
559
+ return indexed;
560
+ },
561
+ async updateFileId(path, fileId) {
562
+ let updated = false;
563
+ state = {
564
+ ...state,
565
+ entries: state.entries.map((entry) => {
566
+ if (entry.path !== path)
567
+ return entry;
568
+ updated = true;
569
+ return { ...entry, fileId };
570
+ }),
571
+ };
572
+ if (updated) {
573
+ await persist();
574
+ }
575
+ return updated;
576
+ },
577
+ findLatestVersionByScope(scope) {
578
+ return state.entries.reduce((max, e) => (e.scope === scope && e.version > max ? e.version : max), 0);
579
+ },
580
+ async updateDataPointId(path, dataPointId) {
581
+ let updated = false;
582
+ state = {
583
+ ...state,
584
+ entries: state.entries.map((entry) => {
585
+ if (entry.path !== path)
586
+ return entry;
587
+ updated = true;
588
+ return { ...entry, dataPointId };
589
+ }),
590
+ };
591
+ if (updated) {
592
+ await persist();
593
+ }
594
+ return updated;
595
+ },
596
+ async updateEntryVersion(path, version) {
597
+ let updated = false;
598
+ state = {
599
+ ...state,
600
+ entries: state.entries.map((entry) => {
601
+ if (entry.path !== path)
602
+ return entry;
603
+ updated = true;
604
+ return { ...entry, version };
605
+ }),
606
+ };
607
+ if (updated) {
608
+ await persist();
609
+ }
610
+ return updated;
611
+ },
612
+ async deleteScope(scope) {
613
+ let deleted = 0;
614
+ const deletedPaths = [];
615
+ const deletedBlockTrees = [];
616
+ state = {
617
+ ...state,
618
+ entries: state.entries.filter((entry) => {
619
+ if (entry.scope !== scope)
620
+ return true;
621
+ deleted += 1;
622
+ deletedPaths.push(envelopePath(entry.scope, entry.collectedAt));
623
+ deletedBlockTrees.push(blockTreePath(entry.scope, entry.collectedAt));
624
+ return false;
625
+ }),
626
+ };
627
+ await Promise.all([
628
+ ...deletedPaths.flatMap((path) => [
629
+ fileStore.deleteEnvelope(path),
630
+ fallbackStore.deleteEnvelope(path),
631
+ ]),
632
+ ...deletedBlockTrees.flatMap((path) => [
633
+ fileStore.deleteBlockTree?.(path),
634
+ fallbackStore.deleteBlockTree?.(path),
635
+ ]),
636
+ ].filter((promise) => promise !== undefined));
637
+ await persist();
638
+ return deleted;
639
+ },
640
+ async deleteVersion(scope, collectedAt) {
641
+ const entry = state.entries.find((e) => e.scope === scope && e.collectedAt === collectedAt);
642
+ if (!entry)
643
+ return false;
644
+ return removeEntry(entry);
645
+ },
646
+ async deleteByFileId(fileId) {
647
+ const entry = state.entries.find((e) => e.fileId === fileId);
648
+ if (!entry)
649
+ return false;
650
+ return removeEntry(entry);
651
+ },
652
+ };
653
+ async function removeEntry(entry) {
654
+ const blobPath = envelopePath(entry.scope, entry.collectedAt);
655
+ // Delete the blob FIRST (both stores tolerate a missing blob); only drop the index row once
656
+ // the blob is gone. If blob deletion throws for a real reason, the row is preserved so the
657
+ // next sync retry re-attempts instead of orphaning the local blob.
658
+ await Promise.all([
659
+ fileStore.deleteEnvelope(blobPath),
660
+ fallbackStore.deleteEnvelope(blobPath),
661
+ fileStore.deleteBlockTree?.(blockTreePath(entry.scope, entry.collectedAt)) ?? Promise.resolve(),
662
+ fallbackStore.deleteBlockTree?.(blockTreePath(entry.scope, entry.collectedAt)) ?? Promise.resolve(),
663
+ ]);
664
+ state = {
665
+ ...state,
666
+ entries: state.entries.filter((e) => e !== entry),
667
+ };
668
+ await persist();
669
+ return true;
670
+ }
671
+ if (fileStore.readEnvelopePreview) {
672
+ storagePort.readEnvelopePreview = async (scope, collectedAt, { maxBytes }) => {
673
+ const path = envelopePath(scope, collectedAt);
674
+ const primaryPreview = await readPreviewFromFileStore(fileStore, path, maxBytes);
675
+ const preview = primaryPreview ??
676
+ (fileStore === fallbackStore
677
+ ? null
678
+ : await readPreviewFromFileStore(fallbackStore, path, maxBytes));
679
+ if (!preview) {
680
+ throw new Error("Envelope not found");
681
+ }
682
+ return preview;
683
+ };
684
+ }
685
+ return storagePort;
686
+ }
687
+ async function readPreviewFromFileStore(store, path, maxBytes) {
688
+ return (await store.readEnvelopePreview?.(path, { maxBytes })) ?? null;
689
+ }
690
+ function pageBlock(block, offsetBytes, maxBytes) {
691
+ const text = typeof block.value === "string" ? block.value : JSON.stringify(block.value);
692
+ const bytes = textEncoder.encode(text);
693
+ if (offsetBytes <= 0 && bytes.length <= maxBytes) {
694
+ return { block };
695
+ }
696
+ const start = Math.min(Math.max(0, offsetBytes), bytes.length);
697
+ const end = Math.min(bytes.length, start + Math.max(1, maxBytes));
698
+ const value = textDecoder.decode(bytes.slice(start, end));
699
+ return {
700
+ block: {
701
+ ...block,
702
+ path: `${block.path}[bytes ${start}:${end}]`,
703
+ mediaType: block.mediaType.startsWith("text/")
704
+ ? block.mediaType
705
+ : TEXT_PAGE_MEDIA_TYPE,
706
+ value,
707
+ sizeBytes: end - start,
708
+ truncated: end < bytes.length,
709
+ },
710
+ ...(end < bytes.length ? { nextOffset: end } : {}),
711
+ };
712
+ }
713
+ async function readBlockPayloadFromStores(primary, fallback, path) {
714
+ return ((await primary.readBlockPayload?.(path)) ??
715
+ (primary === fallback ? null : await fallback.readBlockPayload?.(path)) ??
716
+ null);
717
+ }
718
+ //# sourceMappingURL=storage.js.map