@dxos/teleport-extension-object-sync 0.10.0 → 0.11.1

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.
@@ -0,0 +1,688 @@
1
+ import { DeferredTask, Mutex, Trigger, sleep, synchronized, trackLeaks } from "@dxos/async";
2
+ import { Context, cancelWithContext } from "@dxos/context";
3
+ import { invariant } from "@dxos/invariant";
4
+ import { log } from "@dxos/log";
5
+ import { RpcClosedError } from "@dxos/protocols";
6
+ import { schema } from "@dxos/protocols/proto";
7
+ import { RpcExtension } from "@dxos/teleport";
8
+ import { BitField, ComplexMap, arrayToBuffer } from "@dxos/util";
9
+ import { PublicKey } from "@dxos/keys";
10
+ import { BlobMeta } from "@dxos/protocols/proto/dxos/echo/blob";
11
+ import * as EffectContext from "effect/Context";
12
+ import path from "@dxos/node-std/path";
13
+ import { subtleCrypto } from "@dxos/crypto";
14
+ import * as SqlClient from "@effect/sql/SqlClient";
15
+ import * as Effect from "effect/Effect";
16
+ import * as Layer from "effect/Layer";
17
+ import { RuntimeProvider } from "@dxos/effect";
18
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
19
+ function __decorate(decorators, target, key, desc) {
20
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
21
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
22
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
23
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
24
+ }
25
+ //#endregion
26
+ //#region src/blob-sync-extension.ts
27
+ var __dxlog_file$3 = "/__w/dxos/dxos/packages/core/mesh/teleport-extension-object-sync/src/blob-sync-extension.ts";
28
+ var MIN_WANT_LIST_UPDATE_INTERVAL = process.env.NODE_ENV === "test" ? 5 : 500;
29
+ var MAX_CONCURRENT_UPLOADS = 20;
30
+ /**
31
+ * Manages replication between a set of feeds for a single teleport session.
32
+ */
33
+ var BlobSyncExtension = class extends RpcExtension {
34
+ _params;
35
+ _ctx = new Context({ onError: (err) => log.catch(err, void 0, {
36
+ "~LogMeta": "~LogMeta",
37
+ F: __dxlog_file$3,
38
+ L: 35,
39
+ S: this
40
+ }) }, {
41
+ "~LogMeta": "~LogMeta",
42
+ F: __dxlog_file$3,
43
+ L: 35
44
+ });
45
+ _lastWantListUpdate = 0;
46
+ _localWantList = { blobs: [] };
47
+ _updateWantList = new DeferredTask(this._ctx, async () => {
48
+ if (this._lastWantListUpdate + MIN_WANT_LIST_UPDATE_INTERVAL > Date.now()) {
49
+ await sleep(this._lastWantListUpdate + MIN_WANT_LIST_UPDATE_INTERVAL - Date.now());
50
+ if (this._ctx.disposed) return;
51
+ }
52
+ log("want", { list: this._localWantList }, {
53
+ "~LogMeta": "~LogMeta",
54
+ F: __dxlog_file$3,
55
+ L: 49,
56
+ S: this
57
+ });
58
+ await this.rpc.BlobSyncService.want(this._localWantList);
59
+ this._lastWantListUpdate = Date.now();
60
+ });
61
+ _currentUploads = 0;
62
+ _upload = new DeferredTask(this._ctx, async () => {
63
+ if (this._currentUploads >= MAX_CONCURRENT_UPLOADS) return;
64
+ const blobChunks = await this._pickBlobChunks(MAX_CONCURRENT_UPLOADS - this._currentUploads);
65
+ if (!blobChunks) return;
66
+ for (const blobChunk of blobChunks) {
67
+ if (this._ctx.disposed) break;
68
+ this._currentUploads++;
69
+ this.push(blobChunk).catch((err) => {
70
+ if (err instanceof RpcClosedError) return;
71
+ log.warn("push failed", { err }, {
72
+ "~LogMeta": "~LogMeta",
73
+ F: __dxlog_file$3,
74
+ L: 76,
75
+ S: this
76
+ });
77
+ }).finally(() => {
78
+ this._currentUploads--;
79
+ this.reconcileUploads();
80
+ });
81
+ }
82
+ });
83
+ /**
84
+ * Set of id's remote peer wants.
85
+ */
86
+ remoteWantList = { blobs: [] };
87
+ constructor(_params) {
88
+ super({
89
+ exposed: { BlobSyncService: schema.getService("dxos.mesh.teleport.blobsync.BlobSyncService") },
90
+ requested: { BlobSyncService: schema.getService("dxos.mesh.teleport.blobsync.BlobSyncService") },
91
+ timeout: 2e4,
92
+ encodingOptions: { preserveAny: true }
93
+ });
94
+ this._params = _params;
95
+ }
96
+ async onOpen(context) {
97
+ log("open", void 0, {
98
+ "~LogMeta": "~LogMeta",
99
+ F: __dxlog_file$3,
100
+ L: 108,
101
+ S: this
102
+ });
103
+ await super.onOpen(context);
104
+ await this._params.onOpen();
105
+ }
106
+ async onClose(err) {
107
+ log("close", void 0, {
108
+ "~LogMeta": "~LogMeta",
109
+ F: __dxlog_file$3,
110
+ L: 114,
111
+ S: this
112
+ });
113
+ await this._ctx.dispose();
114
+ await this._params.onClose();
115
+ await super.onClose(err);
116
+ }
117
+ async onAbort(err) {
118
+ log("abort", void 0, {
119
+ "~LogMeta": "~LogMeta",
120
+ F: __dxlog_file$3,
121
+ L: 121,
122
+ S: this
123
+ });
124
+ await this._ctx.dispose();
125
+ await this._params.onAbort();
126
+ await super.onAbort(err);
127
+ }
128
+ async getHandlers() {
129
+ return { BlobSyncService: {
130
+ want: async (wantList) => {
131
+ log("remote want", { remoteWantList: wantList }, {
132
+ "~LogMeta": "~LogMeta",
133
+ F: __dxlog_file$3,
134
+ L: 131,
135
+ S: this
136
+ });
137
+ this.remoteWantList = wantList;
138
+ this.reconcileUploads();
139
+ },
140
+ push: async (data) => {
141
+ log("received", { data }, {
142
+ "~LogMeta": "~LogMeta",
143
+ F: __dxlog_file$3,
144
+ L: 136,
145
+ S: this
146
+ });
147
+ await this._params.onPush(data);
148
+ }
149
+ } };
150
+ }
151
+ async push(data) {
152
+ if (this._ctx.disposed) return;
153
+ log("push", { data }, {
154
+ "~LogMeta": "~LogMeta",
155
+ F: __dxlog_file$3,
156
+ L: 148,
157
+ S: this
158
+ });
159
+ await this.rpc.BlobSyncService.push(data);
160
+ }
161
+ updateWantList(wantList) {
162
+ if (this._ctx.disposed) return;
163
+ this._localWantList = wantList;
164
+ this._updateWantList.schedule();
165
+ }
166
+ reconcileUploads() {
167
+ if (this._ctx.disposed) return;
168
+ this._upload.schedule();
169
+ }
170
+ async _pickBlobChunks(amount = 1) {
171
+ if (this._ctx.disposed) return;
172
+ if (!this.remoteWantList.blobs || this.remoteWantList.blobs?.length === 0) return;
173
+ const shuffled = [...this.remoteWantList.blobs].sort(() => Math.random() - .5);
174
+ const chunks = [];
175
+ for (const header of shuffled) {
176
+ const meta = await this._params.blobStore.getMeta(header.id);
177
+ if (!meta) continue;
178
+ invariant(meta.bitfield, void 0, {
179
+ "~LogMeta": "~LogMeta",
180
+ F: __dxlog_file$3,
181
+ L: 187,
182
+ S: this,
183
+ A: ["meta.bitfield", ""]
184
+ });
185
+ invariant(meta.chunkSize, void 0, {
186
+ "~LogMeta": "~LogMeta",
187
+ F: __dxlog_file$3,
188
+ L: 188,
189
+ S: this,
190
+ A: ["meta.chunkSize", ""]
191
+ });
192
+ invariant(meta.length, void 0, {
193
+ "~LogMeta": "~LogMeta",
194
+ F: __dxlog_file$3,
195
+ L: 189,
196
+ S: this,
197
+ A: ["meta.length", ""]
198
+ });
199
+ if (header.chunkSize && header.chunkSize !== meta.chunkSize) {
200
+ log.warn("Invalid chunk size", {
201
+ header,
202
+ meta
203
+ }, {
204
+ "~LogMeta": "~LogMeta",
205
+ F: __dxlog_file$3,
206
+ L: 192,
207
+ S: this
208
+ });
209
+ continue;
210
+ }
211
+ const requestBitfield = header.bitfield ?? BitField.ones(meta.length / meta.chunkSize);
212
+ const presentData = BitField.and(requestBitfield, meta.bitfield);
213
+ const chunkIndices = BitField.findIndexes(presentData).sort(() => Math.random() - .5);
214
+ for (const idx of chunkIndices) {
215
+ const chunkData = await this._params.blobStore.get(header.id, {
216
+ offset: idx * meta.chunkSize,
217
+ length: Math.min(meta.chunkSize, meta.length - idx * meta.chunkSize)
218
+ });
219
+ chunks.push({
220
+ id: header.id,
221
+ totalLength: meta.length,
222
+ chunkSize: meta.chunkSize,
223
+ chunkOffset: idx * meta.chunkSize,
224
+ payload: chunkData
225
+ });
226
+ if (chunks.length >= amount) return chunks;
227
+ }
228
+ }
229
+ return chunks;
230
+ }
231
+ };
232
+ __decorate([synchronized], BlobSyncExtension.prototype, "push", null);
233
+ //#endregion
234
+ //#region src/blob-sync.ts
235
+ var __dxlog_file$2 = "/__w/dxos/dxos/packages/core/mesh/teleport-extension-object-sync/src/blob-sync.ts";
236
+ var BlobSync = class BlobSync {
237
+ _params;
238
+ _ctx = new Context(void 0, {
239
+ "~LogMeta": "~LogMeta",
240
+ F: __dxlog_file$2,
241
+ L: 30
242
+ });
243
+ _mutex = new Mutex();
244
+ _downloadRequests = new ComplexMap((key) => PublicKey.from(key).toHex());
245
+ _extensions = /* @__PURE__ */ new Set();
246
+ constructor(_params) {
247
+ this._params = _params;
248
+ }
249
+ async open() {}
250
+ async close() {
251
+ await this._ctx.dispose();
252
+ }
253
+ /**
254
+ * Resolves when the object with the given id is fully downloaded in the blob store.
255
+ *
256
+ * @param id hex-encoded id of the object to download.
257
+ */
258
+ async download(ctx, id) {
259
+ log("download", { id }, {
260
+ "~LogMeta": "~LogMeta",
261
+ F: __dxlog_file$2,
262
+ L: 53,
263
+ S: this
264
+ });
265
+ const request = await this._mutex.executeSynchronized(async () => {
266
+ const existingRequest = this._downloadRequests.get(id);
267
+ if (existingRequest) {
268
+ existingRequest.counter++;
269
+ return existingRequest;
270
+ }
271
+ const meta = await this._params.blobStore.getMeta(id);
272
+ const request = {
273
+ trigger: new Trigger(),
274
+ counter: 1,
275
+ want: {
276
+ id,
277
+ chunkSize: meta?.chunkSize,
278
+ bitfield: meta?.bitfield && Uint8Array.from(BitField.invert(meta.bitfield))
279
+ }
280
+ };
281
+ if (meta?.state === BlobMeta.State.FULLY_PRESENT) request.trigger.wake();
282
+ else {
283
+ this._downloadRequests.set(id, request);
284
+ this._updateExtensionsWantList();
285
+ }
286
+ return request;
287
+ });
288
+ ctx?.onDispose(() => this._mutex.executeSynchronized(async () => {
289
+ const request = this._downloadRequests.get(id);
290
+ if (!request) return;
291
+ if (--request.counter === 0) this._downloadRequests.delete(id);
292
+ this._updateExtensionsWantList();
293
+ }));
294
+ return ctx ? cancelWithContext(ctx, request.trigger.wait()) : request.trigger.wait();
295
+ }
296
+ createExtension() {
297
+ const extension = new BlobSyncExtension({
298
+ blobStore: this._params.blobStore,
299
+ onOpen: async () => {
300
+ log("extension opened", void 0, {
301
+ "~LogMeta": "~LogMeta",
302
+ F: __dxlog_file$2,
303
+ L: 105,
304
+ S: this
305
+ });
306
+ this._extensions.add(extension);
307
+ extension.updateWantList(this._getWantList());
308
+ },
309
+ onClose: async () => {
310
+ log("extension closed", void 0, {
311
+ "~LogMeta": "~LogMeta",
312
+ F: __dxlog_file$2,
313
+ L: 110,
314
+ S: this
315
+ });
316
+ this._extensions.delete(extension);
317
+ },
318
+ onAbort: async () => {
319
+ log("extension aborted", void 0, {
320
+ "~LogMeta": "~LogMeta",
321
+ F: __dxlog_file$2,
322
+ L: 114,
323
+ S: this
324
+ });
325
+ this._extensions.delete(extension);
326
+ },
327
+ onPush: async (blobChunk) => {
328
+ if (!this._downloadRequests.has(blobChunk.id)) return;
329
+ log("received", { blobChunk }, {
330
+ "~LogMeta": "~LogMeta",
331
+ F: __dxlog_file$2,
332
+ L: 121,
333
+ S: this
334
+ });
335
+ const meta = await this._params.blobStore.setChunk(blobChunk);
336
+ if (meta.state === BlobMeta.State.FULLY_PRESENT) {
337
+ this._downloadRequests.get(blobChunk.id)?.trigger.wake();
338
+ this._downloadRequests.delete(blobChunk.id);
339
+ } else {
340
+ invariant(meta.bitfield, void 0, {
341
+ "~LogMeta": "~LogMeta",
342
+ F: __dxlog_file$2,
343
+ L: 127,
344
+ S: this,
345
+ A: ["meta.bitfield", ""]
346
+ });
347
+ this._downloadRequests.get(blobChunk.id).want.bitfield = BitField.invert(meta.bitfield);
348
+ }
349
+ this._updateExtensionsWantList();
350
+ this._reconcileUploads();
351
+ }
352
+ });
353
+ return extension;
354
+ }
355
+ /**
356
+ * Notify extensions that a blob with the given id was added to the blob store.
357
+ */
358
+ async notifyBlobAdded(_id) {
359
+ this._reconcileUploads();
360
+ }
361
+ _getWantList() {
362
+ return { blobs: Array.from(this._downloadRequests.values()).map((request) => request.want) };
363
+ }
364
+ _reconcileUploads() {
365
+ for (const extension of this._extensions) extension.reconcileUploads();
366
+ }
367
+ _updateExtensionsWantList() {
368
+ for (const extension of this._extensions) extension.updateWantList(this._getWantList());
369
+ }
370
+ };
371
+ BlobSync = __decorate([trackLeaks("open", "close")], BlobSync);
372
+ //#endregion
373
+ //#region src/blob-store.ts
374
+ var __dxlog_file$1 = "/__w/dxos/dxos/packages/core/mesh/teleport-extension-object-sync/src/blob-store.ts";
375
+ var DEFAULT_CHUNK_SIZE = 4096;
376
+ /**
377
+ * Effect service tag for {@link BlobStoreApi}.
378
+ */
379
+ var BlobStoreApiService = class extends EffectContext.Tag("@dxos/teleport-extension-object-sync/BlobStoreApi")() {};
380
+ var BlobMetaCodec$1 = schema.getCodecForType("dxos.echo.blob.BlobMeta");
381
+ var BlobStore = class {
382
+ _directory;
383
+ constructor(_directory) {
384
+ this._directory = _directory;
385
+ }
386
+ async getMeta(id) {
387
+ return this._getMeta(id);
388
+ }
389
+ /**
390
+ * @throws If range is not available.
391
+ */
392
+ async get(id, options = {}) {
393
+ const metadata = await this._getMeta(id);
394
+ if (!metadata) throw new Error("Blob not available");
395
+ const { offset = 0, length = metadata.length } = options;
396
+ if (offset + length > metadata.length) throw new Error("Invalid range");
397
+ if (metadata.state === BlobMeta.State.FULLY_PRESENT) return this._getDataFile(id).read(offset, length);
398
+ else if (options.offset === void 0 && options.length === void 0) throw new Error("Blob not available");
399
+ const beginChunk = Math.floor(offset / metadata.chunkSize);
400
+ const endChunk = Math.ceil((offset + length) / metadata.chunkSize);
401
+ invariant(metadata.bitfield, "Bitfield not present", {
402
+ "~LogMeta": "~LogMeta",
403
+ F: __dxlog_file$1,
404
+ L: 81,
405
+ S: this,
406
+ A: ["metadata.bitfield", "'Bitfield not present'"]
407
+ });
408
+ invariant(metadata.bitfield.length * 8 >= endChunk, "Invalid bitfield length", {
409
+ "~LogMeta": "~LogMeta",
410
+ F: __dxlog_file$1,
411
+ L: 82,
412
+ S: this,
413
+ A: ["metadata.bitfield.length * 8 >= endChunk", "'Invalid bitfield length'"]
414
+ });
415
+ if (!(BitField.count(metadata.bitfield, beginChunk, endChunk) === endChunk - beginChunk)) throw new Error("Blob not available");
416
+ return this._getDataFile(id).read(offset, length);
417
+ }
418
+ async list() {
419
+ const files = new Set((await this._directory.list()).map((f) => f.split("_")[0]));
420
+ const res = [];
421
+ for (const file of files) {
422
+ const id = PublicKey.from(file).asUint8Array();
423
+ const meta = await this._getMeta(id);
424
+ if (meta) res.push(meta);
425
+ }
426
+ return res;
427
+ }
428
+ async set(data) {
429
+ const id = new Uint8Array(await subtleCrypto.digest("SHA-256", data));
430
+ const bitfield = BitField.ones(data.length / DEFAULT_CHUNK_SIZE);
431
+ const meta = {
432
+ id,
433
+ state: BlobMeta.State.FULLY_PRESENT,
434
+ length: data.length,
435
+ chunkSize: DEFAULT_CHUNK_SIZE,
436
+ bitfield,
437
+ created: /* @__PURE__ */ new Date(),
438
+ updated: /* @__PURE__ */ new Date()
439
+ };
440
+ await this._getDataFile(id).write(0, arrayToBuffer(data));
441
+ await this._writeMeta(id, meta);
442
+ return meta;
443
+ }
444
+ async setChunk(chunk) {
445
+ let meta = await this._getMeta(chunk.id);
446
+ if (!meta) {
447
+ invariant(chunk.totalLength, "totalLength is not present", {
448
+ "~LogMeta": "~LogMeta",
449
+ F: __dxlog_file$1,
450
+ L: 144,
451
+ S: this,
452
+ A: ["chunk.totalLength", "'totalLength is not present'"]
453
+ });
454
+ meta = {
455
+ id: chunk.id,
456
+ state: BlobMeta.State.PARTIALLY_PRESENT,
457
+ length: chunk.totalLength,
458
+ chunkSize: chunk.chunkSize ?? 4096,
459
+ created: /* @__PURE__ */ new Date()
460
+ };
461
+ meta.bitfield = BitField.zeros(meta.length / meta.chunkSize);
462
+ }
463
+ if (chunk.chunkSize && chunk.chunkSize !== meta.chunkSize) throw new Error("Invalid chunk size");
464
+ invariant(meta.bitfield, "Bitfield not present", {
465
+ "~LogMeta": "~LogMeta",
466
+ F: __dxlog_file$1,
467
+ L: 159,
468
+ S: this,
469
+ A: ["meta.bitfield", "'Bitfield not present'"]
470
+ });
471
+ invariant(chunk.chunkOffset !== void 0, "chunkOffset is not present", {
472
+ "~LogMeta": "~LogMeta",
473
+ F: __dxlog_file$1,
474
+ L: 160,
475
+ S: this,
476
+ A: ["chunk.chunkOffset !== undefined", "'chunkOffset is not present'"]
477
+ });
478
+ await this._getDataFile(chunk.id).write(chunk.chunkOffset, arrayToBuffer(chunk.payload));
479
+ BitField.set(meta.bitfield, Math.floor(chunk.chunkOffset / meta.chunkSize), true);
480
+ if (BitField.count(meta.bitfield, 0, meta.length) * meta.chunkSize >= meta.length) meta.state = BlobMeta.State.FULLY_PRESENT;
481
+ meta.updated = /* @__PURE__ */ new Date();
482
+ await this._writeMeta(chunk.id, meta);
483
+ return meta;
484
+ }
485
+ async _writeMeta(id, meta) {
486
+ const encoded = arrayToBuffer(BlobMetaCodec$1.encode(meta));
487
+ const data = Buffer.alloc(encoded.length + 4);
488
+ data.writeUInt32LE(encoded.length, 0);
489
+ encoded.copy(data, 4);
490
+ await this._getMetaFile(id).write(0, data);
491
+ }
492
+ async _getMeta(id) {
493
+ const file = this._getMetaFile(id);
494
+ const size = (await file.stat()).size;
495
+ if (size === 0) return;
496
+ const data = await file.read(0, size);
497
+ const protoSize = data.readUInt32LE(0);
498
+ return BlobMetaCodec$1.decode(data.subarray(4, protoSize + 4));
499
+ }
500
+ _getMetaFile(id) {
501
+ return this._directory.getOrCreateFile(path.join(arrayToBuffer(id).toString("hex"), "meta"));
502
+ }
503
+ _getDataFile(id) {
504
+ return this._directory.getOrCreateFile(path.join(arrayToBuffer(id).toString("hex"), "data"));
505
+ }
506
+ };
507
+ __decorate([synchronized], BlobStore.prototype, "getMeta", null);
508
+ __decorate([synchronized], BlobStore.prototype, "get", null);
509
+ __decorate([synchronized], BlobStore.prototype, "list", null);
510
+ __decorate([synchronized], BlobStore.prototype, "set", null);
511
+ __decorate([synchronized], BlobStore.prototype, "setChunk", null);
512
+ //#endregion
513
+ //#region src/sqlite-blob-store.ts
514
+ var __dxlog_file = "/__w/dxos/dxos/packages/core/mesh/teleport-extension-object-sync/src/sqlite-blob-store.ts";
515
+ var BlobMetaCodec = schema.getCodecForType("dxos.echo.blob.BlobMeta");
516
+ /**
517
+ * SQLite-backed BlobStore.
518
+ * Stores blob metadata and data in `blobs_meta` and `blobs_data` tables.
519
+ */
520
+ var SqliteBlobStore = class {
521
+ #runtime;
522
+ constructor({ runtime }) {
523
+ this.#runtime = runtime;
524
+ }
525
+ /**
526
+ * Creates the blobs_meta and blobs_data tables if they do not exist.
527
+ */
528
+ migrate = Effect.fn("SqliteBlobStore.migrate")(() => Effect.gen(function* () {
529
+ const sql = yield* SqlClient.SqlClient;
530
+ yield* sql`CREATE TABLE IF NOT EXISTS blobs_meta (
531
+ id TEXT PRIMARY KEY,
532
+ meta BLOB NOT NULL
533
+ )`;
534
+ yield* sql`CREATE TABLE IF NOT EXISTS blobs_data (
535
+ id TEXT PRIMARY KEY,
536
+ data BLOB NOT NULL
537
+ )`;
538
+ log("blobs tables ready", void 0, {
539
+ "~LogMeta": "~LogMeta",
540
+ F: __dxlog_file,
541
+ L: 59,
542
+ S: this
543
+ });
544
+ }).pipe(Effect.withSpan("SqliteBlobStore.migrate")))();
545
+ async getMeta(id) {
546
+ return this.#getMeta(id);
547
+ }
548
+ async get(id, options = {}) {
549
+ const metadata = await this.#getMeta(id);
550
+ if (!metadata) throw new Error("Blob not available");
551
+ const { offset = 0, length = metadata.length } = options;
552
+ if (!Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0 || offset + length > metadata.length) throw new Error("Invalid range");
553
+ if (metadata.state === BlobMeta.State.FULLY_PRESENT) {
554
+ const data = await this.#getData(id);
555
+ if (!data) throw new Error("Blob data missing");
556
+ return data.subarray(offset, offset + length);
557
+ } else if (options.offset === void 0 && options.length === void 0) throw new Error("Blob not available");
558
+ const beginChunk = Math.floor(offset / metadata.chunkSize);
559
+ const endChunk = Math.ceil((offset + length) / metadata.chunkSize);
560
+ invariant(metadata.bitfield, "Bitfield not present", {
561
+ "~LogMeta": "~LogMeta",
562
+ F: __dxlog_file,
563
+ L: 98,
564
+ S: this,
565
+ A: ["metadata.bitfield", "'Bitfield not present'"]
566
+ });
567
+ invariant(metadata.bitfield.length * 8 >= endChunk, "Invalid bitfield length", {
568
+ "~LogMeta": "~LogMeta",
569
+ F: __dxlog_file,
570
+ L: 99,
571
+ S: this,
572
+ A: ["metadata.bitfield.length * 8 >= endChunk", "'Invalid bitfield length'"]
573
+ });
574
+ if (!(BitField.count(metadata.bitfield, beginChunk, endChunk) === endChunk - beginChunk)) throw new Error("Blob not available");
575
+ const data = await this.#getData(id);
576
+ if (!data) throw new Error("Blob data missing");
577
+ return data.subarray(offset, offset + length);
578
+ }
579
+ async list() {
580
+ return (await RuntimeProvider.runPromise(this.#runtime)(Effect.gen(function* () {
581
+ return yield* (yield* SqlClient.SqlClient)`SELECT id, meta FROM blobs_meta`;
582
+ }))).map((row) => BlobMetaCodec.decode(row.meta));
583
+ }
584
+ async set(data) {
585
+ const id = new Uint8Array(await subtleCrypto.digest("SHA-256", data));
586
+ const bitfield = BitField.ones(Math.ceil(data.length / DEFAULT_CHUNK_SIZE));
587
+ const meta = {
588
+ id,
589
+ state: BlobMeta.State.FULLY_PRESENT,
590
+ length: data.length,
591
+ chunkSize: DEFAULT_CHUNK_SIZE,
592
+ bitfield,
593
+ created: /* @__PURE__ */ new Date(),
594
+ updated: /* @__PURE__ */ new Date()
595
+ };
596
+ const idHex = arrayToBuffer(id).toString("hex");
597
+ const encodedMeta = arrayToBuffer(BlobMetaCodec.encode(meta));
598
+ await RuntimeProvider.runPromise(this.#runtime)(Effect.gen(function* () {
599
+ const sql = yield* SqlClient.SqlClient;
600
+ yield* sql`INSERT OR REPLACE INTO blobs_meta (id, meta) VALUES (${idHex}, ${encodedMeta})`;
601
+ yield* sql`INSERT OR REPLACE INTO blobs_data (id, data) VALUES (${idHex}, ${data})`;
602
+ }));
603
+ return meta;
604
+ }
605
+ async setChunk(chunk) {
606
+ const idHex = arrayToBuffer(chunk.id).toString("hex");
607
+ let meta = await this.#getMeta(chunk.id);
608
+ if (!meta) {
609
+ invariant(chunk.totalLength, "totalLength is not present", {
610
+ "~LogMeta": "~LogMeta",
611
+ F: __dxlog_file,
612
+ L: 155,
613
+ S: this,
614
+ A: ["chunk.totalLength", "'totalLength is not present'"]
615
+ });
616
+ meta = {
617
+ id: chunk.id,
618
+ state: BlobMeta.State.PARTIALLY_PRESENT,
619
+ length: chunk.totalLength,
620
+ chunkSize: chunk.chunkSize ?? 4096,
621
+ created: /* @__PURE__ */ new Date()
622
+ };
623
+ meta.bitfield = BitField.zeros(Math.ceil(meta.length / meta.chunkSize));
624
+ }
625
+ if (chunk.chunkSize && chunk.chunkSize !== meta.chunkSize) throw new Error("Invalid chunk size");
626
+ invariant(meta.bitfield, "Bitfield not present", {
627
+ "~LogMeta": "~LogMeta",
628
+ F: __dxlog_file,
629
+ L: 170,
630
+ S: this,
631
+ A: ["meta.bitfield", "'Bitfield not present'"]
632
+ });
633
+ invariant(chunk.chunkOffset !== void 0, "chunkOffset is not present", {
634
+ "~LogMeta": "~LogMeta",
635
+ F: __dxlog_file,
636
+ L: 171,
637
+ S: this,
638
+ A: ["chunk.chunkOffset !== undefined", "'chunkOffset is not present'"]
639
+ });
640
+ if (chunk.chunkOffset < 0 || chunk.chunkOffset + chunk.payload.length > meta.length) throw new Error("Invalid chunk range");
641
+ const existingData = await this.#getData(chunk.id) ?? new Uint8Array(meta.length);
642
+ const newData = Buffer.from(existingData);
643
+ Buffer.from(chunk.payload).copy(newData, chunk.chunkOffset);
644
+ BitField.set(meta.bitfield, Math.floor(chunk.chunkOffset / meta.chunkSize), true);
645
+ const totalChunks = Math.ceil(meta.length / meta.chunkSize);
646
+ if (BitField.count(meta.bitfield, 0, totalChunks) === totalChunks) meta.state = BlobMeta.State.FULLY_PRESENT;
647
+ meta.updated = /* @__PURE__ */ new Date();
648
+ const encodedMeta = arrayToBuffer(BlobMetaCodec.encode(meta));
649
+ chunk.id;
650
+ await RuntimeProvider.runPromise(this.#runtime)(Effect.gen(function* () {
651
+ const sql = yield* SqlClient.SqlClient;
652
+ yield* sql`INSERT OR REPLACE INTO blobs_meta (id, meta) VALUES (${idHex}, ${encodedMeta})`;
653
+ yield* sql`INSERT OR REPLACE INTO blobs_data (id, data) VALUES (${idHex}, ${newData})`;
654
+ }));
655
+ return meta;
656
+ }
657
+ async #getMeta(id) {
658
+ const idHex = arrayToBuffer(id).toString("hex");
659
+ const rows = await RuntimeProvider.runPromise(this.#runtime)(Effect.gen(function* () {
660
+ return yield* (yield* SqlClient.SqlClient)`SELECT meta FROM blobs_meta WHERE id = ${idHex}`;
661
+ }));
662
+ if (rows.length === 0) return;
663
+ return BlobMetaCodec.decode(rows[0].meta);
664
+ }
665
+ async #getData(id) {
666
+ const idHex = arrayToBuffer(id).toString("hex");
667
+ const rows = await RuntimeProvider.runPromise(this.#runtime)(Effect.gen(function* () {
668
+ return yield* (yield* SqlClient.SqlClient)`SELECT data FROM blobs_data WHERE id = ${idHex}`;
669
+ }));
670
+ if (rows.length === 0) return;
671
+ return rows[0].data;
672
+ }
673
+ };
674
+ __decorate([synchronized], SqliteBlobStore.prototype, "getMeta", null);
675
+ __decorate([synchronized], SqliteBlobStore.prototype, "get", null);
676
+ __decorate([synchronized], SqliteBlobStore.prototype, "list", null);
677
+ __decorate([synchronized], SqliteBlobStore.prototype, "set", null);
678
+ __decorate([synchronized], SqliteBlobStore.prototype, "setChunk", null);
679
+ /**
680
+ * Effect Layer constructing a {@link SqliteBlobStore} from the ambient SQL runtime.
681
+ */
682
+ var SqliteBlobStoreLayer = () => Layer.effect(BlobStoreApiService, Effect.gen(function* () {
683
+ return new SqliteBlobStore({ runtime: yield* RuntimeProvider.currentRuntime() });
684
+ }));
685
+ //#endregion
686
+ export { BlobStore, BlobStoreApiService, BlobSync, BlobSyncExtension, DEFAULT_CHUNK_SIZE, SqliteBlobStore, SqliteBlobStoreLayer };
687
+
688
+ //# sourceMappingURL=index.mjs.map