@korajs/store 1.0.0-beta.0 → 1.0.0-beta.10
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/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/better-sqlite3.js +1 -1
- package/dist/adapters/indexeddb.cjs +523 -148
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +4 -2
- package/dist/adapters/indexeddb.d.ts +4 -2
- package/dist/adapters/indexeddb.js +55 -11
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +207 -187
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +208 -188
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +474 -140
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +45 -8
- package/dist/adapters/sqlite-wasm.d.ts +45 -8
- package/dist/adapters/sqlite-wasm.js +3 -3
- package/dist/{backup-5XZ7THNJ.js → backup-5S7K2Z35.js} +3 -3
- package/dist/blob-fs.cjs +166 -0
- package/dist/blob-fs.cjs.map +1 -0
- package/dist/blob-fs.d.cts +29 -0
- package/dist/blob-fs.d.ts +29 -0
- package/dist/blob-fs.js +128 -0
- package/dist/blob-fs.js.map +1 -0
- package/dist/{chunk-EIVXWF4B.js → chunk-CYO3YKFL.js} +4 -3
- package/dist/chunk-CYO3YKFL.js.map +1 -0
- package/dist/{chunk-ETDJEH2I.js → chunk-GSJ7EEUC.js} +12 -1
- package/dist/chunk-GSJ7EEUC.js.map +1 -0
- package/dist/{chunk-5R4QMY3Z.js → chunk-HS6DBMST.js} +13 -10
- package/dist/chunk-HS6DBMST.js.map +1 -0
- package/dist/chunk-RJIEKEN4.js +751 -0
- package/dist/chunk-RJIEKEN4.js.map +1 -0
- package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
- package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
- package/dist/{chunk-IOEFNHHP.js → chunk-VDBJQ75K.js} +2 -2
- package/dist/chunk-VDBJQ75K.js.map +1 -0
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/{chunk-OUQDC5Z6.js → chunk-YT7MRFCP.js} +55 -35
- package/dist/chunk-YT7MRFCP.js.map +1 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
- package/dist/{export-audit-VPGQWNR6.js → export-audit-ODGQTQIK.js} +3 -3
- package/dist/index.cjs +923 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +443 -6
- package/dist/index.d.ts +443 -6
- package/dist/index.js +737 -49
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +69 -43
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +3 -3
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +12 -0
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-L6o3bWQK.d.cts} +1 -1
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-L6o3bWQK.d.ts} +1 -1
- package/dist/{types-CyjXwrjW.d.cts → types-BOO9EPt1.d.ts} +10 -3
- package/dist/{types-CJOVKxLw.d.ts → types-BslDOTk0.d.cts} +32 -2
- package/dist/{types-CJOVKxLw.d.cts → types-BslDOTk0.d.ts} +32 -2
- package/dist/{types-DAxCladl.d.ts → types-C_GDavG-.d.cts} +10 -3
- package/package.json +13 -12
- package/dist/adapters/sqlite-wasm-shared-host.cjs +0 -68
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +0 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +0 -2
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +0 -2
- package/dist/adapters/sqlite-wasm-shared-host.js +0 -66
- package/dist/adapters/sqlite-wasm-shared-host.js.map +0 -1
- package/dist/chunk-5R4QMY3Z.js.map +0 -1
- package/dist/chunk-EIVXWF4B.js.map +0 -1
- package/dist/chunk-ETDJEH2I.js.map +0 -1
- package/dist/chunk-IOEFNHHP.js.map +0 -1
- package/dist/chunk-OUQDC5Z6.js.map +0 -1
- package/dist/chunk-RBPVEAVM.js +0 -425
- package/dist/chunk-RBPVEAVM.js.map +0 -1
- package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
- /package/dist/{backup-5XZ7THNJ.js.map → backup-5S7K2Z35.js.map} +0 -0
- /package/dist/{export-audit-VPGQWNR6.js.map → export-audit-ODGQTQIK.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-GGHA2BXU.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,10 +2,14 @@ import {
|
|
|
2
2
|
decodeAuditExport,
|
|
3
3
|
readAuditExportManifest,
|
|
4
4
|
verifyAuditExportChecksum
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CYO3YKFL.js";
|
|
6
6
|
import {
|
|
7
7
|
persistedAuditTraceFromEvent
|
|
8
8
|
} from "./chunk-MYSJN3DF.js";
|
|
9
|
+
import {
|
|
10
|
+
BlobIntegrityError,
|
|
11
|
+
MemoryBlobStore
|
|
12
|
+
} from "./chunk-W4MTTNEZ.js";
|
|
9
13
|
import {
|
|
10
14
|
InvalidStateTransitionError,
|
|
11
15
|
allocateNextSequenceInTransaction,
|
|
@@ -29,9 +33,10 @@ import {
|
|
|
29
33
|
serializeRowVersion,
|
|
30
34
|
validateStateTransition,
|
|
31
35
|
validateUpdateStateMachine
|
|
32
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-YT7MRFCP.js";
|
|
33
37
|
import {
|
|
34
38
|
AdapterError,
|
|
39
|
+
NoLeaderError,
|
|
35
40
|
OptimisticLockError,
|
|
36
41
|
PersistenceError,
|
|
37
42
|
QueryError,
|
|
@@ -39,13 +44,13 @@ import {
|
|
|
39
44
|
StoreNotOpenError,
|
|
40
45
|
WorkerInitError,
|
|
41
46
|
WorkerTimeoutError
|
|
42
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-TUR7RGEP.js";
|
|
43
48
|
import {
|
|
44
49
|
exportBackup,
|
|
45
50
|
readBackupManifest,
|
|
46
51
|
restoreBackup,
|
|
47
52
|
verifyBackupChecksum
|
|
48
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-HS6DBMST.js";
|
|
49
54
|
import {
|
|
50
55
|
decodeRichtext,
|
|
51
56
|
decodeRichtextFieldsFromOpData,
|
|
@@ -57,13 +62,13 @@ import {
|
|
|
57
62
|
richtextToPlainText,
|
|
58
63
|
serializeOperation,
|
|
59
64
|
serializeRecord
|
|
60
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-GSJ7EEUC.js";
|
|
61
66
|
|
|
62
67
|
// src/compaction/types.ts
|
|
63
68
|
var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
|
|
64
69
|
|
|
65
70
|
// src/compaction/compact-operation-log.ts
|
|
66
|
-
import { HybridLogicalClock, createVersionVector } from "@korajs/core";
|
|
71
|
+
import { HybridLogicalClock, createVersionVector, quoteIdent } from "@korajs/core";
|
|
67
72
|
function computeAckCompactionWatermark(serverVector) {
|
|
68
73
|
const watermark = createVersionVector();
|
|
69
74
|
for (const [nodeId, seq] of serverVector) {
|
|
@@ -91,7 +96,7 @@ async function compactOperationLog(adapter, schema, strategy, serverVector) {
|
|
|
91
96
|
const collectionNames = Object.keys(schema.collections);
|
|
92
97
|
await adapter.transaction(async (tx) => {
|
|
93
98
|
for (const collectionName of collectionNames) {
|
|
94
|
-
const table = `_kora_ops_${collectionName}
|
|
99
|
+
const table = quoteIdent(`_kora_ops_${collectionName}`);
|
|
95
100
|
for (const [nodeId, maxSeq] of watermark) {
|
|
96
101
|
if (maxSeq <= 0) continue;
|
|
97
102
|
const countSql = buildCountSql(table, ageCutoffSerialized !== null);
|
|
@@ -126,6 +131,7 @@ function buildDeleteSql(table, withAge) {
|
|
|
126
131
|
import { createVersionVector as createVersionVector2 } from "@korajs/core";
|
|
127
132
|
var LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
|
|
128
133
|
var DELTA_CURSOR_META_KEY = "delta_cursor";
|
|
134
|
+
var DELIVERY_WATERMARK_META_KEY = "delivery_watermark";
|
|
129
135
|
function serializeVersionVectorToMeta(vector) {
|
|
130
136
|
const record = {};
|
|
131
137
|
for (const [nodeId, seq] of vector) {
|
|
@@ -198,6 +204,45 @@ async function saveDeltaCursor(adapter, cursor) {
|
|
|
198
204
|
cursor
|
|
199
205
|
]);
|
|
200
206
|
}
|
|
207
|
+
function deliveryWatermarkKey(signature) {
|
|
208
|
+
return signature === "" ? DELIVERY_WATERMARK_META_KEY : `${DELIVERY_WATERMARK_META_KEY}:${signature}`;
|
|
209
|
+
}
|
|
210
|
+
async function loadDeliveryWatermark(adapter, signature) {
|
|
211
|
+
const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
|
|
212
|
+
deliveryWatermarkKey(signature)
|
|
213
|
+
]);
|
|
214
|
+
const value = rows[0]?.value;
|
|
215
|
+
if (value === void 0 || value === null) {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
const parsed = Number(value);
|
|
219
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
220
|
+
}
|
|
221
|
+
async function saveDeliveryWatermark(adapter, signature, watermark) {
|
|
222
|
+
await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
223
|
+
deliveryWatermarkKey(signature),
|
|
224
|
+
String(watermark)
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
async function deleteDeliveryWatermark(adapter, signature) {
|
|
228
|
+
await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [deliveryWatermarkKey(signature)]);
|
|
229
|
+
}
|
|
230
|
+
async function loadAllDeliveryWatermarks(adapter) {
|
|
231
|
+
const rows = await adapter.query(
|
|
232
|
+
"SELECT key, value FROM _kora_meta WHERE key = ? OR key LIKE ?",
|
|
233
|
+
[DELIVERY_WATERMARK_META_KEY, `${DELIVERY_WATERMARK_META_KEY}:%`]
|
|
234
|
+
);
|
|
235
|
+
const result = {};
|
|
236
|
+
const prefix = `${DELIVERY_WATERMARK_META_KEY}:`;
|
|
237
|
+
for (const row of rows) {
|
|
238
|
+
const signature = row.key === DELIVERY_WATERMARK_META_KEY ? "" : row.key.slice(prefix.length);
|
|
239
|
+
const parsed = Number(row.value);
|
|
240
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
241
|
+
result[signature] = parsed;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
201
246
|
|
|
202
247
|
// src/store/store.ts
|
|
203
248
|
import {
|
|
@@ -205,12 +250,15 @@ import {
|
|
|
205
250
|
HybridLogicalClock as HybridLogicalClock3,
|
|
206
251
|
createVersionVector as createVersionVector3,
|
|
207
252
|
generateUUIDv7 as generateUUIDv73,
|
|
208
|
-
migrationStepsToSQL
|
|
253
|
+
migrationStepsToSQL,
|
|
254
|
+
quoteIdent as quoteIdent7,
|
|
255
|
+
replayOperationsForRecord
|
|
209
256
|
} from "@korajs/core";
|
|
210
257
|
|
|
211
258
|
// src/collection/collection.ts
|
|
259
|
+
import { quoteIdent as quoteIdent2 } from "@korajs/core";
|
|
212
260
|
var Collection = class {
|
|
213
|
-
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
|
|
261
|
+
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker, secretKeyProvider) {
|
|
214
262
|
this.name = name;
|
|
215
263
|
this.definition = definition;
|
|
216
264
|
this.schema = schema;
|
|
@@ -222,6 +270,7 @@ var Collection = class {
|
|
|
222
270
|
this.relationEnforcer = relationEnforcer;
|
|
223
271
|
this.mutationHandler = mutationHandler;
|
|
224
272
|
this.causalTracker = causalTracker;
|
|
273
|
+
this.secretKeyProvider = secretKeyProvider;
|
|
225
274
|
}
|
|
226
275
|
name;
|
|
227
276
|
definition;
|
|
@@ -234,6 +283,7 @@ var Collection = class {
|
|
|
234
283
|
relationEnforcer;
|
|
235
284
|
mutationHandler;
|
|
236
285
|
causalTracker;
|
|
286
|
+
secretKeyProvider;
|
|
237
287
|
mutationContext() {
|
|
238
288
|
return {
|
|
239
289
|
collection: this.name,
|
|
@@ -246,7 +296,8 @@ var Collection = class {
|
|
|
246
296
|
onMutation: this.onMutation,
|
|
247
297
|
relationEnforcer: this.relationEnforcer,
|
|
248
298
|
causalTracker: this.causalTracker,
|
|
249
|
-
inTransaction: false
|
|
299
|
+
inTransaction: false,
|
|
300
|
+
secretKeyProvider: this.secretKeyProvider
|
|
250
301
|
};
|
|
251
302
|
}
|
|
252
303
|
async insert(data) {
|
|
@@ -257,7 +308,7 @@ var Collection = class {
|
|
|
257
308
|
}
|
|
258
309
|
async findById(id) {
|
|
259
310
|
const rows = await this.adapter.query(
|
|
260
|
-
`SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
|
|
311
|
+
`SELECT * FROM ${quoteIdent2(this.name)} WHERE id = ? AND _deleted = 0`,
|
|
261
312
|
[id]
|
|
262
313
|
);
|
|
263
314
|
const row = rows[0];
|
|
@@ -289,6 +340,9 @@ var Collection = class {
|
|
|
289
340
|
}
|
|
290
341
|
};
|
|
291
342
|
|
|
343
|
+
// src/query/query-builder.ts
|
|
344
|
+
import { quoteIdent as quoteIdent3 } from "@korajs/core";
|
|
345
|
+
|
|
292
346
|
// src/query/pluralize.ts
|
|
293
347
|
function isVowel(char) {
|
|
294
348
|
if (!char) return false;
|
|
@@ -518,7 +572,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
518
572
|
const relatedDef = this.schema?.collections[relatedCollection];
|
|
519
573
|
if (!relatedDef) return;
|
|
520
574
|
const placeholders = uniqueFks.map(() => "?").join(", ");
|
|
521
|
-
const sql = `SELECT * FROM ${relatedCollection} WHERE id IN (${placeholders}) AND _deleted = 0`;
|
|
575
|
+
const sql = `SELECT * FROM ${quoteIdent3(relatedCollection)} WHERE id IN (${placeholders}) AND _deleted = 0`;
|
|
522
576
|
const rows = await this.adapter.query(sql, uniqueFks);
|
|
523
577
|
const relatedRecords = rows.map((row) => deserializeRecord(row, relatedDef.fields));
|
|
524
578
|
const lookup = /* @__PURE__ */ new Map();
|
|
@@ -541,7 +595,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
541
595
|
if (!relatedDef) return;
|
|
542
596
|
const fkField = relation.field;
|
|
543
597
|
const placeholders = primaryIds.map(() => "?").join(", ");
|
|
544
|
-
const sql = `SELECT * FROM ${relatedCollection} WHERE ${fkField} IN (${placeholders}) AND _deleted = 0`;
|
|
598
|
+
const sql = `SELECT * FROM ${quoteIdent3(relatedCollection)} WHERE ${quoteIdent3(fkField)} IN (${placeholders}) AND _deleted = 0`;
|
|
545
599
|
const rows = await this.adapter.query(sql, primaryIds);
|
|
546
600
|
const relatedRecords = rows.map((row) => deserializeRecord(row, relatedDef.fields));
|
|
547
601
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -583,7 +637,7 @@ var QueryError2 = class extends Error {
|
|
|
583
637
|
};
|
|
584
638
|
|
|
585
639
|
// src/relations/relation-enforcer.ts
|
|
586
|
-
import { KoraError, createOperation } from "@korajs/core";
|
|
640
|
+
import { KoraError, createOperation, quoteIdent as quoteIdent4 } from "@korajs/core";
|
|
587
641
|
|
|
588
642
|
// src/relations/relation-lookup.ts
|
|
589
643
|
function buildRelationLookup(schema) {
|
|
@@ -686,7 +740,7 @@ var RelationEnforcer = class {
|
|
|
686
740
|
async enforceCascade(incoming, deletedRecordId, tx, causalDeps) {
|
|
687
741
|
const { sourceCollection, foreignKeyField } = incoming;
|
|
688
742
|
const referencingRows = await tx.query(
|
|
689
|
-
`SELECT id FROM ${sourceCollection} WHERE ${foreignKeyField} = ? AND _deleted = 0`,
|
|
743
|
+
`SELECT id FROM ${quoteIdent4(sourceCollection)} WHERE ${quoteIdent4(foreignKeyField)} = ? AND _deleted = 0`,
|
|
690
744
|
[deletedRecordId]
|
|
691
745
|
);
|
|
692
746
|
if (referencingRows.length === 0) {
|
|
@@ -733,7 +787,7 @@ var RelationEnforcer = class {
|
|
|
733
787
|
return [];
|
|
734
788
|
}
|
|
735
789
|
const referencingRows = await tx.query(
|
|
736
|
-
`SELECT id FROM ${sourceCollection} WHERE ${foreignKeyField} = ? AND _deleted = 0`,
|
|
790
|
+
`SELECT id FROM ${quoteIdent4(sourceCollection)} WHERE ${quoteIdent4(foreignKeyField)} = ? AND _deleted = 0`,
|
|
737
791
|
[deletedRecordId]
|
|
738
792
|
);
|
|
739
793
|
if (referencingRows.length === 0) {
|
|
@@ -781,7 +835,7 @@ var RelationEnforcer = class {
|
|
|
781
835
|
async enforceRestrict(incoming, deletedRecordId, tx) {
|
|
782
836
|
const { sourceCollection, foreignKeyField, relationName } = incoming;
|
|
783
837
|
const countRows = await tx.query(
|
|
784
|
-
`SELECT COUNT(*) as cnt FROM ${sourceCollection} WHERE ${foreignKeyField} = ? AND _deleted = 0`,
|
|
838
|
+
`SELECT COUNT(*) as cnt FROM ${quoteIdent4(sourceCollection)} WHERE ${quoteIdent4(foreignKeyField)} = ? AND _deleted = 0`,
|
|
785
839
|
[deletedRecordId]
|
|
786
840
|
);
|
|
787
841
|
const count = countRows[0]?.cnt ?? 0;
|
|
@@ -1469,7 +1523,7 @@ var SubscriptionManager = class {
|
|
|
1469
1523
|
};
|
|
1470
1524
|
|
|
1471
1525
|
// src/sync/rebase-unsynced-operations.ts
|
|
1472
|
-
import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL } from "@korajs/core";
|
|
1526
|
+
import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL, quoteIdent as quoteIdent5 } from "@korajs/core";
|
|
1473
1527
|
import { computeOperationId } from "@korajs/core/internal";
|
|
1474
1528
|
var LOGICAL_COUNTER_LIMIT = MAX_LOGICAL + 1;
|
|
1475
1529
|
function emptyResult() {
|
|
@@ -1483,7 +1537,9 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
|
|
|
1483
1537
|
const rebased = [];
|
|
1484
1538
|
let maxOtherSerializedTs = null;
|
|
1485
1539
|
for (const collectionName of Object.keys(schema.collections)) {
|
|
1486
|
-
const rows = await adapter.query(
|
|
1540
|
+
const rows = await adapter.query(
|
|
1541
|
+
`SELECT * FROM ${quoteIdent5(`_kora_ops_${collectionName}`)}`
|
|
1542
|
+
);
|
|
1487
1543
|
for (const row of rows) {
|
|
1488
1544
|
if (rebaseIds.has(row.id)) {
|
|
1489
1545
|
rebased.push(deserializeOperationWithCollection(row, collectionName));
|
|
@@ -1552,7 +1608,7 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
|
|
|
1552
1608
|
continue;
|
|
1553
1609
|
}
|
|
1554
1610
|
const opsTable = `_kora_ops_${oldOp.collection}`;
|
|
1555
|
-
await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
|
|
1611
|
+
await tx.execute(`DELETE FROM ${quoteIdent5(opsTable)} WHERE id = ?`, [oldOp.id]);
|
|
1556
1612
|
const opInsert = buildInsertQuery(
|
|
1557
1613
|
opsTable,
|
|
1558
1614
|
serializeOperation(newOp)
|
|
@@ -1561,11 +1617,11 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
|
|
|
1561
1617
|
const oldVersion = serializeRowVersion(oldOp.timestamp);
|
|
1562
1618
|
const newVersion = serializeRowVersion(newOp.timestamp);
|
|
1563
1619
|
await tx.execute(
|
|
1564
|
-
`UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
|
|
1620
|
+
`UPDATE ${quoteIdent5(oldOp.collection)} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
|
|
1565
1621
|
[newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
|
|
1566
1622
|
);
|
|
1567
1623
|
const rows = await tx.query(
|
|
1568
|
-
`SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
|
|
1624
|
+
`SELECT _field_versions FROM ${quoteIdent5(oldOp.collection)} WHERE id = ?`,
|
|
1569
1625
|
[oldOp.recordId]
|
|
1570
1626
|
);
|
|
1571
1627
|
const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
|
|
@@ -1577,14 +1633,14 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
|
|
|
1577
1633
|
}
|
|
1578
1634
|
}
|
|
1579
1635
|
if (changed) {
|
|
1580
|
-
await tx.execute(
|
|
1581
|
-
|
|
1582
|
-
oldOp.recordId
|
|
1583
|
-
|
|
1636
|
+
await tx.execute(
|
|
1637
|
+
`UPDATE ${quoteIdent5(oldOp.collection)} SET _field_versions = ? WHERE id = ?`,
|
|
1638
|
+
[serializeFieldVersions(fieldVersions), oldOp.recordId]
|
|
1639
|
+
);
|
|
1584
1640
|
}
|
|
1585
1641
|
if (oldOp.type === "insert") {
|
|
1586
1642
|
await tx.execute(
|
|
1587
|
-
`UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
|
|
1643
|
+
`UPDATE ${quoteIdent5(oldOp.collection)} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
|
|
1588
1644
|
[newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
|
|
1589
1645
|
);
|
|
1590
1646
|
}
|
|
@@ -1604,6 +1660,7 @@ import {
|
|
|
1604
1660
|
createOperation as createOperation2,
|
|
1605
1661
|
generateUUIDv7,
|
|
1606
1662
|
isAtomicOp,
|
|
1663
|
+
quoteIdent as quoteIdent6,
|
|
1607
1664
|
resolveAtomicOp,
|
|
1608
1665
|
toAtomicOp,
|
|
1609
1666
|
validateRecord
|
|
@@ -1920,7 +1977,7 @@ var TransactionContext = class {
|
|
|
1920
1977
|
}
|
|
1921
1978
|
}
|
|
1922
1979
|
const rows = await this.config.adapter.query(
|
|
1923
|
-
`SELECT * FROM ${collectionName} WHERE id = ? AND _deleted = 0`,
|
|
1980
|
+
`SELECT * FROM ${quoteIdent6(collectionName)} WHERE id = ? AND _deleted = 0`,
|
|
1924
1981
|
[id]
|
|
1925
1982
|
);
|
|
1926
1983
|
let record = rows[0] ? deserializeRecord(rows[0], definition.fields) : null;
|
|
@@ -2020,6 +2077,7 @@ var Store = class {
|
|
|
2020
2077
|
localMutationHandler;
|
|
2021
2078
|
relationEnforcer = null;
|
|
2022
2079
|
causalTracker = null;
|
|
2080
|
+
secretKeyProvider;
|
|
2023
2081
|
constructor(config) {
|
|
2024
2082
|
this.schema = config.schema;
|
|
2025
2083
|
this.adapter = config.adapter;
|
|
@@ -2028,6 +2086,7 @@ var Store = class {
|
|
|
2028
2086
|
this.isolation = config.isolation ?? "shared";
|
|
2029
2087
|
this.emitter = config.emitter ?? null;
|
|
2030
2088
|
this.localMutationHandler = config.localMutationHandler ?? null;
|
|
2089
|
+
this.secretKeyProvider = config.secretKeyProvider;
|
|
2031
2090
|
this.subscriptionManager = new SubscriptionManager({
|
|
2032
2091
|
onQuerySubscribed: config.onQuerySubscribed
|
|
2033
2092
|
});
|
|
@@ -2070,7 +2129,8 @@ var Store = class {
|
|
|
2070
2129
|
},
|
|
2071
2130
|
this.relationEnforcer,
|
|
2072
2131
|
this.localMutationHandler,
|
|
2073
|
-
this.causalTracker
|
|
2132
|
+
this.causalTracker,
|
|
2133
|
+
this.secretKeyProvider
|
|
2074
2134
|
);
|
|
2075
2135
|
this.collections.set(name, col);
|
|
2076
2136
|
}
|
|
@@ -2152,7 +2212,7 @@ var Store = class {
|
|
|
2152
2212
|
return "skipped";
|
|
2153
2213
|
}
|
|
2154
2214
|
const existing = await this.adapter.query(
|
|
2155
|
-
`SELECT id FROM _kora_ops_${collection} WHERE id = ?`,
|
|
2215
|
+
`SELECT id FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE id = ?`,
|
|
2156
2216
|
[op.id]
|
|
2157
2217
|
);
|
|
2158
2218
|
if (existing.length > 0) {
|
|
@@ -2177,7 +2237,8 @@ var Store = class {
|
|
|
2177
2237
|
throw new OptimisticLockError(collection, op.recordId);
|
|
2178
2238
|
}
|
|
2179
2239
|
};
|
|
2180
|
-
if (
|
|
2240
|
+
if (options?.logOnly) {
|
|
2241
|
+
} else if (op.type === "insert" && materializeSource) {
|
|
2181
2242
|
const serializedData = serializeRecord(materializeSource, definition.fields);
|
|
2182
2243
|
const existing2 = await tx.query(
|
|
2183
2244
|
`SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
|
|
@@ -2199,7 +2260,7 @@ var Store = class {
|
|
|
2199
2260
|
};
|
|
2200
2261
|
const insertQuery = buildInsertQuery(collection, record);
|
|
2201
2262
|
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
2202
|
-
await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
|
|
2263
|
+
await this.foldOrphanedOperations(tx, collection, definition, op.recordId, op);
|
|
2203
2264
|
} else if (row._deleted === 1) {
|
|
2204
2265
|
if (isIncomingNewerThanRow(materializeTimestamp, row)) {
|
|
2205
2266
|
const fieldChanges = {
|
|
@@ -2257,7 +2318,7 @@ var Store = class {
|
|
|
2257
2318
|
const serializedChanges = serializeRecord(materializeSource, definition.fields);
|
|
2258
2319
|
const changedFields = Object.keys(serializedChanges);
|
|
2259
2320
|
const currentRows = await tx.query(
|
|
2260
|
-
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2321
|
+
`SELECT _version, _field_versions FROM ${quoteIdent7(collection)} WHERE id = ?`,
|
|
2261
2322
|
[op.recordId]
|
|
2262
2323
|
);
|
|
2263
2324
|
const currentRow = currentRows[0];
|
|
@@ -2351,9 +2412,9 @@ var Store = class {
|
|
|
2351
2412
|
* necessarily an orphan (rows are soft-deleted, never removed, so a row
|
|
2352
2413
|
* that is absent now was never materialized).
|
|
2353
2414
|
*/
|
|
2354
|
-
async foldOrphanedOperations(tx, collection, definition, recordId) {
|
|
2415
|
+
async foldOrphanedOperations(tx, collection, definition, recordId, insertOp) {
|
|
2355
2416
|
const orphanRows = await tx.query(
|
|
2356
|
-
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
|
|
2417
|
+
`SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE record_id = ? AND type IN ('update', 'delete')`,
|
|
2357
2418
|
[recordId]
|
|
2358
2419
|
);
|
|
2359
2420
|
if (orphanRows.length === 0) {
|
|
@@ -2366,7 +2427,7 @@ var Store = class {
|
|
|
2366
2427
|
if (orphan.type === "update" && orphan.data) {
|
|
2367
2428
|
const orphanChanges = serializeRecord(orphan.data, definition.fields);
|
|
2368
2429
|
const rows = await tx.query(
|
|
2369
|
-
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2430
|
+
`SELECT _version, _field_versions FROM ${quoteIdent7(collection)} WHERE id = ?`,
|
|
2370
2431
|
[recordId]
|
|
2371
2432
|
);
|
|
2372
2433
|
const current = rows[0];
|
|
@@ -2400,6 +2461,37 @@ var Store = class {
|
|
|
2400
2461
|
await tx.execute(query.sql, query.params);
|
|
2401
2462
|
}
|
|
2402
2463
|
}
|
|
2464
|
+
const atomicFields = /* @__PURE__ */ new Set();
|
|
2465
|
+
for (const orphan of orphans) {
|
|
2466
|
+
if (orphan.type === "update" && orphan.atomicOps) {
|
|
2467
|
+
for (const field of Object.keys(orphan.atomicOps)) {
|
|
2468
|
+
atomicFields.add(field);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (atomicFields.size > 0) {
|
|
2473
|
+
const ordered = [insertOp, ...orphans].sort(
|
|
2474
|
+
(a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp)
|
|
2475
|
+
);
|
|
2476
|
+
const folded = replayOperationsForRecord(ordered);
|
|
2477
|
+
if (folded) {
|
|
2478
|
+
const composed = {};
|
|
2479
|
+
for (const field of atomicFields) {
|
|
2480
|
+
if (field in folded) {
|
|
2481
|
+
composed[field] = folded[field];
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
const serialized = serializeRecord(composed, definition.fields);
|
|
2485
|
+
const cols = Object.keys(serialized);
|
|
2486
|
+
if (cols.length > 0) {
|
|
2487
|
+
const setClause = cols.map((c) => `${quoteIdent7(c)} = ?`).join(", ");
|
|
2488
|
+
await tx.execute(`UPDATE ${quoteIdent7(collection)} SET ${setClause} WHERE id = ?`, [
|
|
2489
|
+
...cols.map((c) => serialized[c]),
|
|
2490
|
+
recordId
|
|
2491
|
+
]);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2403
2495
|
}
|
|
2404
2496
|
/**
|
|
2405
2497
|
* Get operations from a node within a sequence number range.
|
|
@@ -2416,7 +2508,7 @@ var Store = class {
|
|
|
2416
2508
|
const allOps = [];
|
|
2417
2509
|
for (const collectionName of Object.keys(this.schema.collections)) {
|
|
2418
2510
|
const rows = await this.adapter.query(
|
|
2419
|
-
`SELECT * FROM _kora_ops_${collectionName} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
|
|
2511
|
+
`SELECT * FROM ${quoteIdent7(`_kora_ops_${collectionName}`)} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
|
|
2420
2512
|
[nodeId, fromSeq, toSeq]
|
|
2421
2513
|
);
|
|
2422
2514
|
for (const row of rows) {
|
|
@@ -2435,7 +2527,7 @@ var Store = class {
|
|
|
2435
2527
|
const allOps = [];
|
|
2436
2528
|
for (const collectionName of Object.keys(this.schema.collections)) {
|
|
2437
2529
|
const rows = await this.adapter.query(
|
|
2438
|
-
`SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
|
|
2530
|
+
`SELECT * FROM ${quoteIdent7(`_kora_ops_${collectionName}`)} ORDER BY sequence_number ASC`
|
|
2439
2531
|
);
|
|
2440
2532
|
for (const row of rows) {
|
|
2441
2533
|
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
@@ -2486,7 +2578,7 @@ var Store = class {
|
|
|
2486
2578
|
*/
|
|
2487
2579
|
async exportAudit(options) {
|
|
2488
2580
|
this.ensureOpen();
|
|
2489
|
-
const { exportAudit: doExport } = await import("./export-audit-
|
|
2581
|
+
const { exportAudit: doExport } = await import("./export-audit-ODGQTQIK.js");
|
|
2490
2582
|
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
2491
2583
|
}
|
|
2492
2584
|
/**
|
|
@@ -2504,6 +2596,22 @@ var Store = class {
|
|
|
2504
2596
|
col.setMutationHandler(handler);
|
|
2505
2597
|
}
|
|
2506
2598
|
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Notify this store that another same-origin runtime committed an operation to
|
|
2601
|
+
* the shared local database. The operation has already been persisted and
|
|
2602
|
+
* materialized by the database owner; this method only advances in-memory causal
|
|
2603
|
+
* watermarks and invalidates local reactive queries so already-open tabs stay
|
|
2604
|
+
* live without reapplying or rewriting the operation.
|
|
2605
|
+
*/
|
|
2606
|
+
notifyExternalOperation(operation) {
|
|
2607
|
+
this.ensureOpen();
|
|
2608
|
+
const definition = this.schema.collections[operation.collection];
|
|
2609
|
+
if (!definition) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
this.recordOperationSequence(operation);
|
|
2613
|
+
this.subscriptionManager.notify(operation.collection, operation);
|
|
2614
|
+
}
|
|
2507
2615
|
/**
|
|
2508
2616
|
* Build mutation context for a collection (used by ApplyPipeline side effects).
|
|
2509
2617
|
*/
|
|
@@ -2531,7 +2639,8 @@ var Store = class {
|
|
|
2531
2639
|
relationEnforcer: this.relationEnforcer,
|
|
2532
2640
|
causalTracker: this.causalTracker,
|
|
2533
2641
|
inTransaction: options?.inTransaction ?? false,
|
|
2534
|
-
extraCausalDeps: options?.extraCausalDeps
|
|
2642
|
+
extraCausalDeps: options?.extraCausalDeps,
|
|
2643
|
+
secretKeyProvider: this.secretKeyProvider
|
|
2535
2644
|
};
|
|
2536
2645
|
}
|
|
2537
2646
|
/**
|
|
@@ -2544,7 +2653,7 @@ var Store = class {
|
|
|
2544
2653
|
return null;
|
|
2545
2654
|
}
|
|
2546
2655
|
const rows = await this.adapter.query(
|
|
2547
|
-
`SELECT * FROM ${collection} WHERE id = ?`,
|
|
2656
|
+
`SELECT * FROM ${quoteIdent7(collection)} WHERE id = ?`,
|
|
2548
2657
|
[recordId]
|
|
2549
2658
|
);
|
|
2550
2659
|
const row = rows[0];
|
|
@@ -2569,7 +2678,7 @@ var Store = class {
|
|
|
2569
2678
|
return null;
|
|
2570
2679
|
}
|
|
2571
2680
|
const rows = await this.adapter.query(
|
|
2572
|
-
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2681
|
+
`SELECT _version, _field_versions FROM ${quoteIdent7(collection)} WHERE id = ?`,
|
|
2573
2682
|
[recordId]
|
|
2574
2683
|
);
|
|
2575
2684
|
const row = rows[0];
|
|
@@ -2612,6 +2721,35 @@ var Store = class {
|
|
|
2612
2721
|
this.ensureOpen();
|
|
2613
2722
|
await saveDeltaCursor(this.adapter, cursor);
|
|
2614
2723
|
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Load the persisted delivery watermark for a view signature (0 when none recorded).
|
|
2726
|
+
* Defaults to the unfiltered view so existing single-watermark reads keep working.
|
|
2727
|
+
*/
|
|
2728
|
+
async loadDeliveryWatermark(signature = "") {
|
|
2729
|
+
this.ensureOpen();
|
|
2730
|
+
return loadDeliveryWatermark(this.adapter, signature);
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Persist the delivery watermark for a view signature so it survives a client restart.
|
|
2734
|
+
*/
|
|
2735
|
+
async saveDeliveryWatermark(signature, watermark) {
|
|
2736
|
+
this.ensureOpen();
|
|
2737
|
+
await saveDeliveryWatermark(this.adapter, signature, watermark);
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* Load every persisted view watermark, keyed by signature.
|
|
2741
|
+
*/
|
|
2742
|
+
async loadAllDeliveryWatermarks() {
|
|
2743
|
+
this.ensureOpen();
|
|
2744
|
+
return loadAllDeliveryWatermarks(this.adapter);
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Delete a persisted view watermark (safe: the view back-fills from 0 when next visited).
|
|
2748
|
+
*/
|
|
2749
|
+
async deleteDeliveryWatermark(signature) {
|
|
2750
|
+
this.ensureOpen();
|
|
2751
|
+
await deleteDeliveryWatermark(this.adapter, signature);
|
|
2752
|
+
}
|
|
2615
2753
|
/**
|
|
2616
2754
|
* Local operations not yet reflected on the server version vector.
|
|
2617
2755
|
*/
|
|
@@ -2675,7 +2813,7 @@ var Store = class {
|
|
|
2675
2813
|
async getLatestLocalOperationForRecord(collection, recordId) {
|
|
2676
2814
|
this.ensureOpen();
|
|
2677
2815
|
const rows = await this.adapter.query(
|
|
2678
|
-
`SELECT * FROM _kora_ops_${collection} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
|
|
2816
|
+
`SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
|
|
2679
2817
|
[this.nodeId, recordId]
|
|
2680
2818
|
);
|
|
2681
2819
|
const row = rows[0];
|
|
@@ -2690,7 +2828,7 @@ var Store = class {
|
|
|
2690
2828
|
async getLatestOperationForRecord(collection, recordId) {
|
|
2691
2829
|
this.ensureOpen();
|
|
2692
2830
|
const rows = await this.adapter.query(
|
|
2693
|
-
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ?`,
|
|
2831
|
+
`SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE record_id = ?`,
|
|
2694
2832
|
[recordId]
|
|
2695
2833
|
);
|
|
2696
2834
|
let latest = null;
|
|
@@ -2702,6 +2840,21 @@ var Store = class {
|
|
|
2702
2840
|
}
|
|
2703
2841
|
return latest;
|
|
2704
2842
|
}
|
|
2843
|
+
/**
|
|
2844
|
+
* All operations for a record, sorted in HLC total order (wallTime, logical,
|
|
2845
|
+
* nodeId). Used by the apply pipeline to materialize atomic-op fields by folding
|
|
2846
|
+
* the record's log, matching the server's materialization exactly.
|
|
2847
|
+
*/
|
|
2848
|
+
async getOperationsForRecord(collection, recordId) {
|
|
2849
|
+
this.ensureOpen();
|
|
2850
|
+
const rows = await this.adapter.query(
|
|
2851
|
+
`SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE record_id = ?`,
|
|
2852
|
+
[recordId]
|
|
2853
|
+
);
|
|
2854
|
+
const ops = rows.map((row) => deserializeOperationWithCollection(row, collection));
|
|
2855
|
+
ops.sort((a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp));
|
|
2856
|
+
return ops;
|
|
2857
|
+
}
|
|
2705
2858
|
/** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
|
|
2706
2859
|
getSubscriptionManager() {
|
|
2707
2860
|
return this.subscriptionManager;
|
|
@@ -2780,7 +2933,7 @@ var Store = class {
|
|
|
2780
2933
|
*/
|
|
2781
2934
|
async exportBackup(options) {
|
|
2782
2935
|
this.ensureOpen();
|
|
2783
|
-
const { exportBackup: doExport } = await import("./backup-
|
|
2936
|
+
const { exportBackup: doExport } = await import("./backup-5S7K2Z35.js");
|
|
2784
2937
|
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
2785
2938
|
}
|
|
2786
2939
|
/**
|
|
@@ -2792,7 +2945,7 @@ var Store = class {
|
|
|
2792
2945
|
*/
|
|
2793
2946
|
async importBackup(data, options) {
|
|
2794
2947
|
this.ensureOpen();
|
|
2795
|
-
const { restoreBackup: doRestore } = await import("./backup-
|
|
2948
|
+
const { restoreBackup: doRestore } = await import("./backup-5S7K2Z35.js");
|
|
2796
2949
|
return doRestore(this.adapter, this.schema, data, options);
|
|
2797
2950
|
}
|
|
2798
2951
|
/**
|
|
@@ -2877,14 +3030,14 @@ var Store = class {
|
|
|
2877
3030
|
*/
|
|
2878
3031
|
async runBackfill(collection, transform) {
|
|
2879
3032
|
const rows = await this.adapter.query(
|
|
2880
|
-
`SELECT * FROM ${collection} WHERE _deleted = 0`
|
|
3033
|
+
`SELECT * FROM ${quoteIdent7(collection)} WHERE _deleted = 0`
|
|
2881
3034
|
);
|
|
2882
3035
|
await this.adapter.transaction(async (tx) => {
|
|
2883
3036
|
for (const row of rows) {
|
|
2884
3037
|
const updates = transform(row);
|
|
2885
3038
|
const fields = Object.keys(updates);
|
|
2886
3039
|
if (fields.length === 0) continue;
|
|
2887
|
-
const setClauses = fields.map((f) => `${f} = ?`).join(", ");
|
|
3040
|
+
const setClauses = fields.map((f) => `${quoteIdent7(f)} = ?`).join(", ");
|
|
2888
3041
|
const values = fields.map((f) => {
|
|
2889
3042
|
const val = updates[f];
|
|
2890
3043
|
if (typeof val === "boolean") return val ? 1 : 0;
|
|
@@ -2894,7 +3047,7 @@ var Store = class {
|
|
|
2894
3047
|
return val;
|
|
2895
3048
|
});
|
|
2896
3049
|
values.push(row.id);
|
|
2897
|
-
await tx.execute(`UPDATE ${collection} SET ${setClauses} WHERE id = ?`, values);
|
|
3050
|
+
await tx.execute(`UPDATE ${quoteIdent7(collection)} SET ${setClauses} WHERE id = ?`, values);
|
|
2898
3051
|
}
|
|
2899
3052
|
});
|
|
2900
3053
|
}
|
|
@@ -2954,6 +3107,7 @@ var QueryStore = class {
|
|
|
2954
3107
|
listeners = /* @__PURE__ */ new Set();
|
|
2955
3108
|
unsubscribeQuery = null;
|
|
2956
3109
|
active = false;
|
|
3110
|
+
hasEmittedSnapshot = false;
|
|
2957
3111
|
queryBuilder;
|
|
2958
3112
|
constructor(queryBuilder) {
|
|
2959
3113
|
this.queryBuilder = queryBuilder;
|
|
@@ -2979,16 +3133,25 @@ var QueryStore = class {
|
|
|
2979
3133
|
getSnapshot = () => {
|
|
2980
3134
|
return this.snapshot;
|
|
2981
3135
|
};
|
|
3136
|
+
/**
|
|
3137
|
+
* Whether the underlying query subscription has delivered at least one
|
|
3138
|
+
* authoritative result for the current subscription lifecycle.
|
|
3139
|
+
*/
|
|
3140
|
+
hasSnapshot = () => {
|
|
3141
|
+
return this.hasEmittedSnapshot;
|
|
3142
|
+
};
|
|
2982
3143
|
/** Tear down listeners and the underlying query subscription. */
|
|
2983
3144
|
destroy() {
|
|
2984
3145
|
this.stopSubscription();
|
|
2985
3146
|
this.listeners.clear();
|
|
2986
3147
|
this.snapshot = EMPTY_ARRAY;
|
|
3148
|
+
this.hasEmittedSnapshot = false;
|
|
2987
3149
|
}
|
|
2988
3150
|
startSubscription() {
|
|
2989
3151
|
this.active = true;
|
|
2990
3152
|
this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
|
|
2991
3153
|
if (!this.active) return;
|
|
3154
|
+
this.hasEmittedSnapshot = true;
|
|
2992
3155
|
this.snapshot = Object.freeze([...results]);
|
|
2993
3156
|
this.notifyListeners();
|
|
2994
3157
|
});
|
|
@@ -3422,12 +3585,519 @@ function asRichTextSyncEngine(engine) {
|
|
|
3422
3585
|
}
|
|
3423
3586
|
return engine;
|
|
3424
3587
|
}
|
|
3588
|
+
|
|
3589
|
+
// src/blob/opfs-blob-store.ts
|
|
3590
|
+
import { createBlobRef, hashBlob } from "@korajs/core";
|
|
3591
|
+
var OpfsBlobStore = class {
|
|
3592
|
+
constructor(dir) {
|
|
3593
|
+
this.dir = dir;
|
|
3594
|
+
}
|
|
3595
|
+
dir;
|
|
3596
|
+
shardFor(hash) {
|
|
3597
|
+
return hash.slice(0, 2);
|
|
3598
|
+
}
|
|
3599
|
+
async put(bytes, metadata = {}) {
|
|
3600
|
+
const ref = await createBlobRef(bytes, metadata);
|
|
3601
|
+
const shard = this.shardFor(ref.hash);
|
|
3602
|
+
if (await this.dir.has(shard, ref.hash)) {
|
|
3603
|
+
return ref;
|
|
3604
|
+
}
|
|
3605
|
+
const out = new Uint8Array(bytes.byteLength);
|
|
3606
|
+
out.set(bytes);
|
|
3607
|
+
await this.dir.write(shard, ref.hash, out);
|
|
3608
|
+
return ref;
|
|
3609
|
+
}
|
|
3610
|
+
async get(hash) {
|
|
3611
|
+
const bytes = await this.dir.read(this.shardFor(hash), hash);
|
|
3612
|
+
if (bytes === null) {
|
|
3613
|
+
return null;
|
|
3614
|
+
}
|
|
3615
|
+
const actual = await hashBlob(bytes);
|
|
3616
|
+
if (actual !== hash) {
|
|
3617
|
+
throw new BlobIntegrityError(hash, actual);
|
|
3618
|
+
}
|
|
3619
|
+
return bytes;
|
|
3620
|
+
}
|
|
3621
|
+
async has(hash) {
|
|
3622
|
+
return this.dir.has(this.shardFor(hash), hash);
|
|
3623
|
+
}
|
|
3624
|
+
async delete(hash) {
|
|
3625
|
+
return this.dir.remove(this.shardFor(hash), hash);
|
|
3626
|
+
}
|
|
3627
|
+
async size() {
|
|
3628
|
+
return this.dir.count();
|
|
3629
|
+
}
|
|
3630
|
+
async list() {
|
|
3631
|
+
return this.dir.keys();
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
function getStorageManager() {
|
|
3635
|
+
const nav = globalThis.navigator;
|
|
3636
|
+
if (!nav?.storage || typeof nav.storage.getDirectory !== "function") {
|
|
3637
|
+
throw new Error(
|
|
3638
|
+
"OPFS is unavailable in this environment. The OPFS blob store requires a browser with Origin Private File System support; use MemoryBlobStore or a server-side store elsewhere."
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3641
|
+
return nav.storage;
|
|
3642
|
+
}
|
|
3643
|
+
function isNotFoundError(error) {
|
|
3644
|
+
return typeof error === "object" && error !== null && error.name === "NotFoundError";
|
|
3645
|
+
}
|
|
3646
|
+
async function createOpfsBlobDirectory(rootDirName = "kora-blobs") {
|
|
3647
|
+
const storage = getStorageManager();
|
|
3648
|
+
if (typeof storage.persist === "function") {
|
|
3649
|
+
try {
|
|
3650
|
+
await storage.persist();
|
|
3651
|
+
} catch {
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
const opfsRoot = await storage.getDirectory();
|
|
3655
|
+
const root = await opfsRoot.getDirectoryHandle(rootDirName, { create: true });
|
|
3656
|
+
async function shardHandle(shard, create) {
|
|
3657
|
+
try {
|
|
3658
|
+
return await root.getDirectoryHandle(shard, { create });
|
|
3659
|
+
} catch (error) {
|
|
3660
|
+
if (isNotFoundError(error)) {
|
|
3661
|
+
return null;
|
|
3662
|
+
}
|
|
3663
|
+
throw error;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
return {
|
|
3667
|
+
async read(shard, name) {
|
|
3668
|
+
const dir = await shardHandle(shard, false);
|
|
3669
|
+
if (!dir) {
|
|
3670
|
+
return null;
|
|
3671
|
+
}
|
|
3672
|
+
let handle;
|
|
3673
|
+
try {
|
|
3674
|
+
handle = await dir.getFileHandle(name, { create: false });
|
|
3675
|
+
} catch (error) {
|
|
3676
|
+
if (isNotFoundError(error)) {
|
|
3677
|
+
return null;
|
|
3678
|
+
}
|
|
3679
|
+
throw error;
|
|
3680
|
+
}
|
|
3681
|
+
const file = await handle.getFile();
|
|
3682
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
3683
|
+
},
|
|
3684
|
+
async write(shard, name, bytes) {
|
|
3685
|
+
const dir = await root.getDirectoryHandle(shard, { create: true });
|
|
3686
|
+
const handle = await dir.getFileHandle(name, { create: true });
|
|
3687
|
+
const writable = await handle.createWritable({ keepExistingData: false });
|
|
3688
|
+
try {
|
|
3689
|
+
await writable.write(bytes);
|
|
3690
|
+
} finally {
|
|
3691
|
+
await writable.close();
|
|
3692
|
+
}
|
|
3693
|
+
},
|
|
3694
|
+
async remove(shard, name) {
|
|
3695
|
+
const dir = await shardHandle(shard, false);
|
|
3696
|
+
if (!dir) {
|
|
3697
|
+
return false;
|
|
3698
|
+
}
|
|
3699
|
+
try {
|
|
3700
|
+
await dir.removeEntry(name);
|
|
3701
|
+
return true;
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
if (isNotFoundError(error)) {
|
|
3704
|
+
return false;
|
|
3705
|
+
}
|
|
3706
|
+
throw error;
|
|
3707
|
+
}
|
|
3708
|
+
},
|
|
3709
|
+
async has(shard, name) {
|
|
3710
|
+
const dir = await shardHandle(shard, false);
|
|
3711
|
+
if (!dir) {
|
|
3712
|
+
return false;
|
|
3713
|
+
}
|
|
3714
|
+
try {
|
|
3715
|
+
await dir.getFileHandle(name, { create: false });
|
|
3716
|
+
return true;
|
|
3717
|
+
} catch (error) {
|
|
3718
|
+
if (isNotFoundError(error)) {
|
|
3719
|
+
return false;
|
|
3720
|
+
}
|
|
3721
|
+
throw error;
|
|
3722
|
+
}
|
|
3723
|
+
},
|
|
3724
|
+
async count() {
|
|
3725
|
+
let total = 0;
|
|
3726
|
+
for await (const shard of root.keys()) {
|
|
3727
|
+
const dir = await shardHandle(shard, false);
|
|
3728
|
+
if (!dir) {
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3731
|
+
for await (const name of dir.keys()) {
|
|
3732
|
+
if (!name.endsWith(".tmp")) {
|
|
3733
|
+
total++;
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
return total;
|
|
3738
|
+
},
|
|
3739
|
+
async keys() {
|
|
3740
|
+
const names = [];
|
|
3741
|
+
for await (const shard of root.keys()) {
|
|
3742
|
+
const dir = await shardHandle(shard, false);
|
|
3743
|
+
if (!dir) {
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
for await (const name of dir.keys()) {
|
|
3747
|
+
if (!name.endsWith(".tmp")) {
|
|
3748
|
+
names.push(name);
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
return names;
|
|
3753
|
+
}
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
async function createOpfsBlobStore(rootDirName = "kora-blobs") {
|
|
3757
|
+
return new OpfsBlobStore(await createOpfsBlobDirectory(rootDirName));
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
// src/blob/blob-chunking.ts
|
|
3761
|
+
import { hashBlob as hashBlob2 } from "@korajs/core";
|
|
3762
|
+
var DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
3763
|
+
async function chunkBlob(bytes, chunkSize = DEFAULT_CHUNK_SIZE, metadata = {}) {
|
|
3764
|
+
if (chunkSize <= 0) {
|
|
3765
|
+
throw new Error(`chunkBlob requires a positive chunkSize, got ${chunkSize}`);
|
|
3766
|
+
}
|
|
3767
|
+
const chunkHashes = [];
|
|
3768
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
3769
|
+
for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
|
|
3770
|
+
const end = Math.min(offset + chunkSize, bytes.byteLength);
|
|
3771
|
+
const chunk = new Uint8Array(end - offset);
|
|
3772
|
+
chunk.set(bytes.subarray(offset, end));
|
|
3773
|
+
const hash = await hashBlob2(chunk);
|
|
3774
|
+
chunkHashes.push(hash);
|
|
3775
|
+
if (!chunks.has(hash)) {
|
|
3776
|
+
chunks.set(hash, chunk);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
const blobHash = await hashBlob2(bytes);
|
|
3780
|
+
const manifest = {
|
|
3781
|
+
blobHash,
|
|
3782
|
+
size: bytes.byteLength,
|
|
3783
|
+
chunkSize,
|
|
3784
|
+
chunkHashes
|
|
3785
|
+
};
|
|
3786
|
+
if (metadata.mimeType !== void 0) {
|
|
3787
|
+
manifest.mimeType = metadata.mimeType;
|
|
3788
|
+
}
|
|
3789
|
+
if (metadata.filename !== void 0) {
|
|
3790
|
+
manifest.filename = metadata.filename;
|
|
3791
|
+
}
|
|
3792
|
+
return { manifest, chunks };
|
|
3793
|
+
}
|
|
3794
|
+
async function reassembleBlob(manifest, chunkStore) {
|
|
3795
|
+
const parts = [];
|
|
3796
|
+
let total = 0;
|
|
3797
|
+
for (const hash of manifest.chunkHashes) {
|
|
3798
|
+
const chunk = await chunkStore.get(hash);
|
|
3799
|
+
if (chunk === null) {
|
|
3800
|
+
throw new Error(`Cannot reassemble blob ${manifest.blobHash}: missing chunk ${hash}`);
|
|
3801
|
+
}
|
|
3802
|
+
parts.push(chunk);
|
|
3803
|
+
total += chunk.byteLength;
|
|
3804
|
+
}
|
|
3805
|
+
const out = new Uint8Array(total);
|
|
3806
|
+
let offset = 0;
|
|
3807
|
+
for (const part of parts) {
|
|
3808
|
+
out.set(part, offset);
|
|
3809
|
+
offset += part.byteLength;
|
|
3810
|
+
}
|
|
3811
|
+
const actual = await hashBlob2(out);
|
|
3812
|
+
if (actual !== manifest.blobHash) {
|
|
3813
|
+
throw new BlobIntegrityError(manifest.blobHash, actual);
|
|
3814
|
+
}
|
|
3815
|
+
return out;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
// src/blob/blob-transfer.ts
|
|
3819
|
+
import { hashBlob as hashBlob3 } from "@korajs/core";
|
|
3820
|
+
async function receiveBlob(manifest, provider, stores) {
|
|
3821
|
+
let chunksFetched = 0;
|
|
3822
|
+
let chunksSkipped = 0;
|
|
3823
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3824
|
+
for (const hash of manifest.chunkHashes) {
|
|
3825
|
+
if (seen.has(hash)) {
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
seen.add(hash);
|
|
3829
|
+
if (await stores.chunkStore.has(hash)) {
|
|
3830
|
+
chunksSkipped++;
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
const bytes = await provider.getChunk(hash);
|
|
3834
|
+
if (bytes === null) {
|
|
3835
|
+
throw new Error(`Blob transfer failed: provider could not supply chunk ${hash}`);
|
|
3836
|
+
}
|
|
3837
|
+
const actual = await hashBlob3(bytes);
|
|
3838
|
+
if (actual !== hash) {
|
|
3839
|
+
throw new BlobIntegrityError(hash, actual);
|
|
3840
|
+
}
|
|
3841
|
+
await stores.chunkStore.put(bytes);
|
|
3842
|
+
chunksFetched++;
|
|
3843
|
+
}
|
|
3844
|
+
const full = await reassembleBlob(manifest, stores.chunkStore);
|
|
3845
|
+
const ref = await stores.blobStore.put(full, {
|
|
3846
|
+
mimeType: manifest.mimeType,
|
|
3847
|
+
filename: manifest.filename
|
|
3848
|
+
});
|
|
3849
|
+
return { ref, chunksFetched, chunksSkipped };
|
|
3850
|
+
}
|
|
3851
|
+
async function prepareBlobForSend(bytes, chunkStore, options = {}) {
|
|
3852
|
+
const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
|
|
3853
|
+
mimeType: options.mimeType,
|
|
3854
|
+
filename: options.filename
|
|
3855
|
+
});
|
|
3856
|
+
for (const chunk of chunks.values()) {
|
|
3857
|
+
await chunkStore.put(chunk);
|
|
3858
|
+
}
|
|
3859
|
+
const provider = {
|
|
3860
|
+
getChunk: (hash) => chunkStore.get(hash)
|
|
3861
|
+
};
|
|
3862
|
+
return { manifest, provider };
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
// src/blob/blob-manifest-transfer.ts
|
|
3866
|
+
import { hashBlob as hashBlob4 } from "@korajs/core";
|
|
3867
|
+
function serializeBlobManifest(manifest) {
|
|
3868
|
+
const canonical = {
|
|
3869
|
+
blobHash: manifest.blobHash,
|
|
3870
|
+
size: manifest.size,
|
|
3871
|
+
chunkSize: manifest.chunkSize,
|
|
3872
|
+
chunkHashes: manifest.chunkHashes
|
|
3873
|
+
};
|
|
3874
|
+
if (manifest.mimeType !== void 0) {
|
|
3875
|
+
canonical.mimeType = manifest.mimeType;
|
|
3876
|
+
}
|
|
3877
|
+
if (manifest.filename !== void 0) {
|
|
3878
|
+
canonical.filename = manifest.filename;
|
|
3879
|
+
}
|
|
3880
|
+
return new TextEncoder().encode(JSON.stringify(canonical));
|
|
3881
|
+
}
|
|
3882
|
+
function parseBlobManifest(bytes) {
|
|
3883
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
3884
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
3885
|
+
throw new Error("Invalid blob manifest: not an object");
|
|
3886
|
+
}
|
|
3887
|
+
const obj = parsed;
|
|
3888
|
+
if (typeof obj.blobHash !== "string") {
|
|
3889
|
+
throw new Error("Invalid blob manifest: missing blobHash");
|
|
3890
|
+
}
|
|
3891
|
+
if (typeof obj.size !== "number" || typeof obj.chunkSize !== "number") {
|
|
3892
|
+
throw new Error("Invalid blob manifest: missing size or chunkSize");
|
|
3893
|
+
}
|
|
3894
|
+
if (!Array.isArray(obj.chunkHashes) || obj.chunkHashes.some((h) => typeof h !== "string")) {
|
|
3895
|
+
throw new Error("Invalid blob manifest: chunkHashes must be an array of strings");
|
|
3896
|
+
}
|
|
3897
|
+
const manifest = {
|
|
3898
|
+
blobHash: obj.blobHash,
|
|
3899
|
+
size: obj.size,
|
|
3900
|
+
chunkSize: obj.chunkSize,
|
|
3901
|
+
chunkHashes: obj.chunkHashes
|
|
3902
|
+
};
|
|
3903
|
+
if (typeof obj.mimeType === "string") {
|
|
3904
|
+
manifest.mimeType = obj.mimeType;
|
|
3905
|
+
}
|
|
3906
|
+
if (typeof obj.filename === "string") {
|
|
3907
|
+
manifest.filename = obj.filename;
|
|
3908
|
+
}
|
|
3909
|
+
return manifest;
|
|
3910
|
+
}
|
|
3911
|
+
async function putBlobForTransfer(store, bytes, options = {}) {
|
|
3912
|
+
const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
|
|
3913
|
+
...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
|
|
3914
|
+
...options.filename !== void 0 ? { filename: options.filename } : {}
|
|
3915
|
+
});
|
|
3916
|
+
for (const chunk of chunks.values()) {
|
|
3917
|
+
await store.put(chunk);
|
|
3918
|
+
}
|
|
3919
|
+
const baseRef = await store.put(bytes, {
|
|
3920
|
+
...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
|
|
3921
|
+
...options.filename !== void 0 ? { filename: options.filename } : {}
|
|
3922
|
+
});
|
|
3923
|
+
const manifestRef = await store.put(serializeBlobManifest(manifest));
|
|
3924
|
+
return { ref: { ...baseRef, manifestHash: manifestRef.hash }, manifest };
|
|
3925
|
+
}
|
|
3926
|
+
async function fetchBlobManifest(provider, manifestHash) {
|
|
3927
|
+
const bytes = await provider.getChunk(manifestHash);
|
|
3928
|
+
if (bytes === null) {
|
|
3929
|
+
throw new Error(`Blob manifest ${manifestHash} could not be fetched from the provider`);
|
|
3930
|
+
}
|
|
3931
|
+
const actual = await hashBlob4(bytes);
|
|
3932
|
+
if (actual !== manifestHash) {
|
|
3933
|
+
throw new BlobIntegrityError(manifestHash, actual);
|
|
3934
|
+
}
|
|
3935
|
+
return parseBlobManifest(bytes);
|
|
3936
|
+
}
|
|
3937
|
+
async function resolveBlobManifest(provider, ref) {
|
|
3938
|
+
if (ref.manifestHash === void 0) {
|
|
3939
|
+
throw new Error(
|
|
3940
|
+
`Blob ${ref.hash} has no manifest to resolve. Store it with putBlobForTransfer (or app.blobs.put) so its reference carries a manifestHash, or pull with an explicit manifest.`
|
|
3941
|
+
);
|
|
3942
|
+
}
|
|
3943
|
+
return fetchBlobManifest(provider, ref.manifestHash);
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
// src/blob/server-blob-callbacks.ts
|
|
3947
|
+
import { hashBlob as hashBlob5 } from "@korajs/core";
|
|
3948
|
+
function toServerBlobCallbacks(store) {
|
|
3949
|
+
return {
|
|
3950
|
+
resolveBlobChunk: (hash) => store.get(hash),
|
|
3951
|
+
async persistBlobChunk(hash, bytes) {
|
|
3952
|
+
const actual = await hashBlob5(bytes);
|
|
3953
|
+
if (actual !== hash) {
|
|
3954
|
+
return;
|
|
3955
|
+
}
|
|
3956
|
+
await store.put(bytes);
|
|
3957
|
+
}
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
function createMemoryServerBlobStore() {
|
|
3961
|
+
const store = new MemoryBlobStore();
|
|
3962
|
+
return { store, callbacks: toServerBlobCallbacks(store) };
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
// src/blob/blob-gc.ts
|
|
3966
|
+
import { isBlobRef } from "@korajs/core";
|
|
3967
|
+
function extractBlobRefs(record) {
|
|
3968
|
+
const refs = [];
|
|
3969
|
+
for (const value of Object.values(record)) {
|
|
3970
|
+
if (isBlobRef(value)) {
|
|
3971
|
+
refs.push(value);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
return refs;
|
|
3975
|
+
}
|
|
3976
|
+
async function collectBlobGarbage(store, liveRefs, options = {}) {
|
|
3977
|
+
const live = /* @__PURE__ */ new Set();
|
|
3978
|
+
for (const ref of liveRefs) {
|
|
3979
|
+
live.add(ref.hash);
|
|
3980
|
+
if (ref.manifestHash === void 0) {
|
|
3981
|
+
continue;
|
|
3982
|
+
}
|
|
3983
|
+
live.add(ref.manifestHash);
|
|
3984
|
+
try {
|
|
3985
|
+
const manifestBytes = await store.get(ref.manifestHash);
|
|
3986
|
+
if (manifestBytes !== null) {
|
|
3987
|
+
const manifest = parseBlobManifest(manifestBytes);
|
|
3988
|
+
for (const chunkHash of manifest.chunkHashes) {
|
|
3989
|
+
live.add(chunkHash);
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
} catch {
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
const all = await store.list();
|
|
3996
|
+
const collectedHashes = [];
|
|
3997
|
+
for (const hash of all) {
|
|
3998
|
+
if (live.has(hash)) {
|
|
3999
|
+
continue;
|
|
4000
|
+
}
|
|
4001
|
+
collectedHashes.push(hash);
|
|
4002
|
+
if (!options.dryRun) {
|
|
4003
|
+
await store.delete(hash);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
return {
|
|
4007
|
+
scanned: all.length,
|
|
4008
|
+
live: live.size,
|
|
4009
|
+
collected: collectedHashes.length,
|
|
4010
|
+
collectedHashes
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
// src/blob/blob-chunk-transport.ts
|
|
4015
|
+
import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
|
|
4016
|
+
function createRemoteChunkProvider(port, options = {}) {
|
|
4017
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
4018
|
+
const pending = /* @__PURE__ */ new Map();
|
|
4019
|
+
port.onMessage((message) => {
|
|
4020
|
+
if (message.type !== "blob-chunk-response") {
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
const entry = pending.get(message.requestId);
|
|
4024
|
+
if (!entry) {
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
pending.delete(message.requestId);
|
|
4028
|
+
clearTimeout(entry.timer);
|
|
4029
|
+
entry.resolve(message.bytes);
|
|
4030
|
+
});
|
|
4031
|
+
return {
|
|
4032
|
+
pendingCount: () => pending.size,
|
|
4033
|
+
getChunk(hash) {
|
|
4034
|
+
const requestId = generateUUIDv74();
|
|
4035
|
+
return new Promise((resolve, reject) => {
|
|
4036
|
+
const timer = setTimeout(() => {
|
|
4037
|
+
pending.delete(requestId);
|
|
4038
|
+
reject(new Error(`Blob chunk request for ${hash} timed out after ${timeoutMs}ms`));
|
|
4039
|
+
}, timeoutMs);
|
|
4040
|
+
pending.set(requestId, { resolve, reject, timer });
|
|
4041
|
+
port.send({ type: "blob-chunk-request", requestId, hash });
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
function serveBlobChunks(port, blobStore) {
|
|
4047
|
+
port.onMessage((message) => {
|
|
4048
|
+
if (message.type !== "blob-chunk-request") {
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
void blobStore.get(message.hash).then(
|
|
4052
|
+
(bytes) => {
|
|
4053
|
+
port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes });
|
|
4054
|
+
},
|
|
4055
|
+
() => {
|
|
4056
|
+
port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes: null });
|
|
4057
|
+
}
|
|
4058
|
+
);
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
function createChunkPortPair() {
|
|
4062
|
+
const handlersA = [];
|
|
4063
|
+
const handlersB = [];
|
|
4064
|
+
const a = {
|
|
4065
|
+
send(message) {
|
|
4066
|
+
queueMicrotask(() => {
|
|
4067
|
+
for (const handler of handlersB) {
|
|
4068
|
+
handler(message);
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
},
|
|
4072
|
+
onMessage(handler) {
|
|
4073
|
+
handlersA.push(handler);
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
const b = {
|
|
4077
|
+
send(message) {
|
|
4078
|
+
queueMicrotask(() => {
|
|
4079
|
+
for (const handler of handlersA) {
|
|
4080
|
+
handler(message);
|
|
4081
|
+
}
|
|
4082
|
+
});
|
|
4083
|
+
},
|
|
4084
|
+
onMessage(handler) {
|
|
4085
|
+
handlersB.push(handler);
|
|
4086
|
+
}
|
|
4087
|
+
};
|
|
4088
|
+
return { a, b };
|
|
4089
|
+
}
|
|
3425
4090
|
export {
|
|
3426
4091
|
AdapterError,
|
|
4092
|
+
BlobIntegrityError,
|
|
3427
4093
|
COMPACTION_BASELINE_META_KEY,
|
|
3428
4094
|
Collection,
|
|
4095
|
+
DEFAULT_CHUNK_SIZE,
|
|
3429
4096
|
InvalidStateTransitionError,
|
|
3430
4097
|
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
4098
|
+
MemoryBlobStore,
|
|
4099
|
+
NoLeaderError,
|
|
4100
|
+
OpfsBlobStore,
|
|
3431
4101
|
OptimisticLockError,
|
|
3432
4102
|
PersistenceError,
|
|
3433
4103
|
QueryBuilder,
|
|
@@ -3445,26 +4115,44 @@ export {
|
|
|
3445
4115
|
WorkerTimeoutError,
|
|
3446
4116
|
asRichTextSyncEngine,
|
|
3447
4117
|
assertQueryReady,
|
|
4118
|
+
chunkBlob,
|
|
4119
|
+
collectBlobGarbage,
|
|
3448
4120
|
collectOperationsAheadOfServer,
|
|
3449
4121
|
compactOperationLog,
|
|
3450
4122
|
computeAckCompactionWatermark,
|
|
4123
|
+
createChunkPortPair,
|
|
4124
|
+
createMemoryServerBlobStore,
|
|
4125
|
+
createOpfsBlobDirectory,
|
|
4126
|
+
createOpfsBlobStore,
|
|
4127
|
+
createRemoteChunkProvider,
|
|
3451
4128
|
createRichTextController,
|
|
3452
4129
|
decodeAuditExport,
|
|
3453
4130
|
decodeRichtext,
|
|
3454
4131
|
deserializeVersionVectorFromMeta,
|
|
3455
4132
|
encodeRichtext,
|
|
3456
4133
|
exportBackup,
|
|
4134
|
+
extractBlobRefs,
|
|
4135
|
+
fetchBlobManifest,
|
|
3457
4136
|
getSharedQueryStoreCache,
|
|
3458
4137
|
mergeVersionVectors,
|
|
4138
|
+
parseBlobManifest,
|
|
3459
4139
|
persistedAuditTraceFromEvent,
|
|
3460
4140
|
pluralize,
|
|
4141
|
+
prepareBlobForSend,
|
|
4142
|
+
putBlobForTransfer,
|
|
3461
4143
|
readAuditExportManifest,
|
|
3462
4144
|
readBackupManifest,
|
|
4145
|
+
reassembleBlob,
|
|
4146
|
+
receiveBlob,
|
|
4147
|
+
resolveBlobManifest,
|
|
3463
4148
|
restoreBackup,
|
|
3464
4149
|
richtextStatesEqual,
|
|
3465
4150
|
richtextToPlainText,
|
|
4151
|
+
serializeBlobManifest,
|
|
3466
4152
|
serializeVersionVectorToMeta,
|
|
4153
|
+
serveBlobChunks,
|
|
3467
4154
|
singularize,
|
|
4155
|
+
toServerBlobCallbacks,
|
|
3468
4156
|
validateStateTransition,
|
|
3469
4157
|
validateUpdateStateMachine,
|
|
3470
4158
|
verifyAuditExportChecksum,
|