@korajs/store 0.6.0 → 1.0.0-beta.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.
- package/LICENSE +21 -0
- package/dist/adapters/better-sqlite3.cjs +94 -40
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +9 -1
- package/dist/adapters/better-sqlite3.d.ts +9 -1
- package/dist/adapters/better-sqlite3.js +62 -41
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +24 -13
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +2 -2
- package/dist/adapters/indexeddb.d.ts +2 -2
- package/dist/adapters/indexeddb.js +4 -3
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +24 -13
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +27 -4
- package/dist/adapters/sqlite-wasm.d.ts +27 -4
- package/dist/adapters/sqlite-wasm.js +6 -4
- package/dist/{backup-JN3LB4YT.js → backup-ZV4Y424B.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-56LL4XP6.js → chunk-6NKLDBTY.js} +52 -1
- package/dist/chunk-6NKLDBTY.js.map +1 -0
- package/dist/{chunk-AGN3CIPB.js → chunk-FFYA7J5H.js} +2 -2
- package/dist/{chunk-XO4LMPAH.js → chunk-IOEFNHHP.js} +2 -35
- package/dist/chunk-IOEFNHHP.js.map +1 -0
- package/dist/{chunk-LAWV6CFH.js → chunk-QL2XVZRN.js} +12 -1
- package/dist/chunk-QL2XVZRN.js.map +1 -0
- package/dist/{chunk-6AHYGQQT.js → chunk-RBPVEAVM.js} +4 -4
- package/dist/chunk-TVJEC5S2.js +40 -0
- package/dist/chunk-TVJEC5S2.js.map +1 -0
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/{chunk-EZSTD2XC.js → chunk-X57MWW35.js} +2 -2
- package/dist/{chunk-7BPETY5Y.js → chunk-ZLWAYQEQ.js} +128 -15
- package/dist/chunk-ZLWAYQEQ.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-5GV6VRDZ.js → export-audit-AJ7RORNK.js} +3 -3
- package/dist/index.cjs +1245 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +468 -7
- package/dist/index.d.ts +468 -7
- package/dist/index.js +975 -61
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +123 -33
- 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-I9y_jObg.d.cts → sqlite-wasm-channel-DUDPclTE.d.cts} +2 -15
- package/dist/{sqlite-wasm-channel-I9y_jObg.d.ts → sqlite-wasm-channel-DUDPclTE.d.ts} +2 -15
- package/dist/sqlite-wasm-channel-H43KEZLM.js +12 -0
- package/dist/{types-CTr00g_d.d.cts → types-5h4Hqcwc.d.ts} +10 -3
- package/dist/{types-BMyHmwvn.d.ts → types-DfPiPwKr.d.cts} +10 -3
- package/dist/{types-DepnRDk_.d.cts → types-k6AZgiH4.d.cts} +48 -2
- package/dist/{types-DepnRDk_.d.ts → types-k6AZgiH4.d.ts} +48 -2
- package/package.json +15 -2
- package/dist/chunk-56LL4XP6.js.map +0 -1
- package/dist/chunk-7BPETY5Y.js.map +0 -1
- package/dist/chunk-LAWV6CFH.js.map +0 -1
- package/dist/chunk-XO4LMPAH.js.map +0 -1
- package/dist/sqlite-wasm-channel-HDDW6UBH.js +0 -10
- /package/dist/{backup-JN3LB4YT.js.map → backup-ZV4Y424B.js.map} +0 -0
- /package/dist/{chunk-AGN3CIPB.js.map → chunk-FFYA7J5H.js.map} +0 -0
- /package/dist/{chunk-6AHYGQQT.js.map → chunk-RBPVEAVM.js.map} +0 -0
- /package/dist/{chunk-EZSTD2XC.js.map → chunk-X57MWW35.js.map} +0 -0
- /package/dist/{export-audit-5GV6VRDZ.js.map → export-audit-AJ7RORNK.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-HDDW6UBH.js.map → sqlite-wasm-channel-H43KEZLM.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,56 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exportBackup,
|
|
3
|
+
readBackupManifest,
|
|
4
|
+
restoreBackup,
|
|
5
|
+
verifyBackupChecksum
|
|
6
|
+
} from "./chunk-X57MWW35.js";
|
|
1
7
|
import {
|
|
2
8
|
decodeAuditExport,
|
|
3
9
|
readAuditExportManifest,
|
|
4
10
|
verifyAuditExportChecksum
|
|
5
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FFYA7J5H.js";
|
|
6
12
|
import {
|
|
7
13
|
persistedAuditTraceFromEvent
|
|
8
14
|
} from "./chunk-MYSJN3DF.js";
|
|
15
|
+
import {
|
|
16
|
+
BlobIntegrityError,
|
|
17
|
+
MemoryBlobStore
|
|
18
|
+
} from "./chunk-W4MTTNEZ.js";
|
|
9
19
|
import {
|
|
10
20
|
InvalidStateTransitionError,
|
|
11
21
|
allocateNextSequenceInTransaction,
|
|
12
22
|
allocateNextSequenceNumber,
|
|
13
23
|
buildCountQuery,
|
|
24
|
+
buildFieldFastForwardUpdateQuery,
|
|
14
25
|
buildInsertQuery,
|
|
15
26
|
buildLwwSoftDeleteQuery,
|
|
16
|
-
buildLwwUpdateQuery,
|
|
17
27
|
buildSelectQuery,
|
|
18
28
|
buildSoftDeleteQuery,
|
|
19
29
|
buildUpdateQuery,
|
|
20
30
|
executeDelete,
|
|
21
31
|
executeInsert,
|
|
22
32
|
executeUpdate,
|
|
33
|
+
fieldVersionsForFields,
|
|
23
34
|
isIncomingNewerThanRow,
|
|
35
|
+
parseFieldVersions,
|
|
24
36
|
readSequenceNumber,
|
|
37
|
+
resolvePerFieldLww,
|
|
38
|
+
serializeFieldVersions,
|
|
25
39
|
serializeRowVersion,
|
|
26
40
|
validateStateTransition,
|
|
27
41
|
validateUpdateStateMachine
|
|
28
|
-
} from "./chunk-
|
|
29
|
-
import {
|
|
30
|
-
AdapterError,
|
|
31
|
-
PersistenceError,
|
|
32
|
-
QueryError,
|
|
33
|
-
RecordNotFoundError,
|
|
34
|
-
StoreNotOpenError,
|
|
35
|
-
WorkerInitError,
|
|
36
|
-
WorkerTimeoutError
|
|
37
|
-
} from "./chunk-LAWV6CFH.js";
|
|
38
|
-
import {
|
|
39
|
-
exportBackup,
|
|
40
|
-
readBackupManifest,
|
|
41
|
-
restoreBackup,
|
|
42
|
-
verifyBackupChecksum
|
|
43
|
-
} from "./chunk-EZSTD2XC.js";
|
|
42
|
+
} from "./chunk-ZLWAYQEQ.js";
|
|
44
43
|
import {
|
|
45
44
|
decodeRichtext,
|
|
45
|
+
decodeRichtextFieldsFromOpData,
|
|
46
46
|
deserializeOperationWithCollection,
|
|
47
47
|
deserializeRecord,
|
|
48
48
|
encodeRichtext,
|
|
49
|
+
encodeRichtextFieldsForOpData,
|
|
49
50
|
richtextStatesEqual,
|
|
50
51
|
richtextToPlainText,
|
|
51
52
|
serializeOperation,
|
|
52
53
|
serializeRecord
|
|
53
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-6NKLDBTY.js";
|
|
55
|
+
import {
|
|
56
|
+
AdapterError,
|
|
57
|
+
OptimisticLockError,
|
|
58
|
+
PersistenceError,
|
|
59
|
+
QueryError,
|
|
60
|
+
RecordNotFoundError,
|
|
61
|
+
StoreNotOpenError,
|
|
62
|
+
WorkerInitError,
|
|
63
|
+
WorkerTimeoutError
|
|
64
|
+
} from "./chunk-QL2XVZRN.js";
|
|
54
65
|
|
|
55
66
|
// src/compaction/types.ts
|
|
56
67
|
var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
|
|
@@ -195,7 +206,7 @@ async function saveDeltaCursor(adapter, cursor) {
|
|
|
195
206
|
// src/store/store.ts
|
|
196
207
|
import {
|
|
197
208
|
CausalTracker,
|
|
198
|
-
HybridLogicalClock as
|
|
209
|
+
HybridLogicalClock as HybridLogicalClock3,
|
|
199
210
|
createVersionVector as createVersionVector3,
|
|
200
211
|
generateUUIDv7 as generateUUIDv73,
|
|
201
212
|
migrationStepsToSQL
|
|
@@ -203,7 +214,7 @@ import {
|
|
|
203
214
|
|
|
204
215
|
// src/collection/collection.ts
|
|
205
216
|
var Collection = class {
|
|
206
|
-
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
|
|
217
|
+
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker, secretKeyProvider) {
|
|
207
218
|
this.name = name;
|
|
208
219
|
this.definition = definition;
|
|
209
220
|
this.schema = schema;
|
|
@@ -215,6 +226,7 @@ var Collection = class {
|
|
|
215
226
|
this.relationEnforcer = relationEnforcer;
|
|
216
227
|
this.mutationHandler = mutationHandler;
|
|
217
228
|
this.causalTracker = causalTracker;
|
|
229
|
+
this.secretKeyProvider = secretKeyProvider;
|
|
218
230
|
}
|
|
219
231
|
name;
|
|
220
232
|
definition;
|
|
@@ -227,6 +239,7 @@ var Collection = class {
|
|
|
227
239
|
relationEnforcer;
|
|
228
240
|
mutationHandler;
|
|
229
241
|
causalTracker;
|
|
242
|
+
secretKeyProvider;
|
|
230
243
|
mutationContext() {
|
|
231
244
|
return {
|
|
232
245
|
collection: this.name,
|
|
@@ -239,7 +252,8 @@ var Collection = class {
|
|
|
239
252
|
onMutation: this.onMutation,
|
|
240
253
|
relationEnforcer: this.relationEnforcer,
|
|
241
254
|
causalTracker: this.causalTracker,
|
|
242
|
-
inTransaction: false
|
|
255
|
+
inTransaction: false,
|
|
256
|
+
secretKeyProvider: this.secretKeyProvider
|
|
243
257
|
};
|
|
244
258
|
}
|
|
245
259
|
async insert(data) {
|
|
@@ -885,7 +899,9 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
885
899
|
}
|
|
886
900
|
colMap.set(op.recordId, {
|
|
887
901
|
id: op.recordId,
|
|
888
|
-
|
|
902
|
+
// op.data stores binary richtext as tagged JSON; snapshots must
|
|
903
|
+
// expose record-shaped values (Uint8Array/string).
|
|
904
|
+
fields: decodeRichtextFieldsFromOpData(op.data, definition.fields),
|
|
889
905
|
deleted: false,
|
|
890
906
|
createdAt: wallTime,
|
|
891
907
|
updatedAt: wallTime
|
|
@@ -896,9 +912,10 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
896
912
|
if (!op.data) {
|
|
897
913
|
return;
|
|
898
914
|
}
|
|
915
|
+
const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
|
|
899
916
|
const existing = colMap.get(op.recordId);
|
|
900
917
|
if (existing && !existing.deleted) {
|
|
901
|
-
existing.fields = { ...existing.fields, ...
|
|
918
|
+
existing.fields = { ...existing.fields, ...decodedData };
|
|
902
919
|
existing.updatedAt = wallTime;
|
|
903
920
|
break;
|
|
904
921
|
}
|
|
@@ -907,7 +924,7 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
907
924
|
}
|
|
908
925
|
colMap.set(op.recordId, {
|
|
909
926
|
id: op.recordId,
|
|
910
|
-
fields:
|
|
927
|
+
fields: decodedData,
|
|
911
928
|
deleted: false,
|
|
912
929
|
createdAt: wallTime,
|
|
913
930
|
updatedAt: wallTime
|
|
@@ -1458,6 +1475,137 @@ var SubscriptionManager = class {
|
|
|
1458
1475
|
}
|
|
1459
1476
|
};
|
|
1460
1477
|
|
|
1478
|
+
// src/sync/rebase-unsynced-operations.ts
|
|
1479
|
+
import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL } from "@korajs/core";
|
|
1480
|
+
import { computeOperationId } from "@korajs/core/internal";
|
|
1481
|
+
var LOGICAL_COUNTER_LIMIT = MAX_LOGICAL + 1;
|
|
1482
|
+
function emptyResult() {
|
|
1483
|
+
return { operations: [], idMapping: {}, rebasedCount: 0, newMaxTimestamp: null };
|
|
1484
|
+
}
|
|
1485
|
+
async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, correctedNowMs) {
|
|
1486
|
+
if (unsyncedOpIds.length === 0) {
|
|
1487
|
+
return emptyResult();
|
|
1488
|
+
}
|
|
1489
|
+
const rebaseIds = new Set(unsyncedOpIds);
|
|
1490
|
+
const rebased = [];
|
|
1491
|
+
let maxOtherSerializedTs = null;
|
|
1492
|
+
for (const collectionName of Object.keys(schema.collections)) {
|
|
1493
|
+
const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
|
|
1494
|
+
for (const row of rows) {
|
|
1495
|
+
if (rebaseIds.has(row.id)) {
|
|
1496
|
+
rebased.push(deserializeOperationWithCollection(row, collectionName));
|
|
1497
|
+
} else if (maxOtherSerializedTs === null || row.timestamp > maxOtherSerializedTs) {
|
|
1498
|
+
maxOtherSerializedTs = row.timestamp;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
if (rebased.length === 0) {
|
|
1503
|
+
return emptyResult();
|
|
1504
|
+
}
|
|
1505
|
+
const maxOtherWallTime = maxOtherSerializedTs === null ? Number.NEGATIVE_INFINITY : HybridLogicalClock2.deserialize(maxOtherSerializedTs).wallTime;
|
|
1506
|
+
const baseWallTime = Math.max(correctedNowMs, maxOtherWallTime + 1);
|
|
1507
|
+
rebased.sort((a, b) => HybridLogicalClock2.compare(a.timestamp, b.timestamp));
|
|
1508
|
+
const newTimestamps = rebased.map((op, i) => ({
|
|
1509
|
+
// Spill into wallTime past the 5-digit logical serialization limit so the
|
|
1510
|
+
// serialized order stays correct even for enormous offline queues.
|
|
1511
|
+
wallTime: baseWallTime + Math.floor(i / LOGICAL_COUNTER_LIMIT),
|
|
1512
|
+
logical: i % LOGICAL_COUNTER_LIMIT,
|
|
1513
|
+
nodeId: op.nodeId
|
|
1514
|
+
}));
|
|
1515
|
+
const idMapping = {};
|
|
1516
|
+
const newIds = [];
|
|
1517
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
1518
|
+
const op = rebased[i];
|
|
1519
|
+
const ts = newTimestamps[i];
|
|
1520
|
+
if (!op || !ts) {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const input = {
|
|
1524
|
+
nodeId: op.nodeId,
|
|
1525
|
+
type: op.type,
|
|
1526
|
+
collection: op.collection,
|
|
1527
|
+
recordId: op.recordId,
|
|
1528
|
+
data: op.data,
|
|
1529
|
+
previousData: op.previousData,
|
|
1530
|
+
sequenceNumber: op.sequenceNumber,
|
|
1531
|
+
causalDeps: op.causalDeps,
|
|
1532
|
+
schemaVersion: op.schemaVersion,
|
|
1533
|
+
...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
|
|
1534
|
+
};
|
|
1535
|
+
const newId = await computeOperationId(input, HybridLogicalClock2.serialize(ts));
|
|
1536
|
+
idMapping[op.id] = newId;
|
|
1537
|
+
newIds.push(newId);
|
|
1538
|
+
}
|
|
1539
|
+
const newOperations = [];
|
|
1540
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
1541
|
+
const op = rebased[i];
|
|
1542
|
+
const ts = newTimestamps[i];
|
|
1543
|
+
const newId = newIds[i];
|
|
1544
|
+
if (!op || !ts || newId === void 0) {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
newOperations.push({
|
|
1548
|
+
...op,
|
|
1549
|
+
id: newId,
|
|
1550
|
+
timestamp: ts,
|
|
1551
|
+
causalDeps: op.causalDeps.map((dep) => idMapping[dep] ?? dep)
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
await adapter.transaction(async (tx) => {
|
|
1555
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
1556
|
+
const oldOp = rebased[i];
|
|
1557
|
+
const newOp = newOperations[i];
|
|
1558
|
+
if (!oldOp || !newOp) {
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
const opsTable = `_kora_ops_${oldOp.collection}`;
|
|
1562
|
+
await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
|
|
1563
|
+
const opInsert = buildInsertQuery(
|
|
1564
|
+
opsTable,
|
|
1565
|
+
serializeOperation(newOp)
|
|
1566
|
+
);
|
|
1567
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1568
|
+
const oldVersion = serializeRowVersion(oldOp.timestamp);
|
|
1569
|
+
const newVersion = serializeRowVersion(newOp.timestamp);
|
|
1570
|
+
await tx.execute(
|
|
1571
|
+
`UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
|
|
1572
|
+
[newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
|
|
1573
|
+
);
|
|
1574
|
+
const rows = await tx.query(
|
|
1575
|
+
`SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
|
|
1576
|
+
[oldOp.recordId]
|
|
1577
|
+
);
|
|
1578
|
+
const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
|
|
1579
|
+
let changed = false;
|
|
1580
|
+
for (const [field, version] of Object.entries(fieldVersions)) {
|
|
1581
|
+
if (version === oldVersion) {
|
|
1582
|
+
fieldVersions[field] = newVersion;
|
|
1583
|
+
changed = true;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (changed) {
|
|
1587
|
+
await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
|
|
1588
|
+
serializeFieldVersions(fieldVersions),
|
|
1589
|
+
oldOp.recordId
|
|
1590
|
+
]);
|
|
1591
|
+
}
|
|
1592
|
+
if (oldOp.type === "insert") {
|
|
1593
|
+
await tx.execute(
|
|
1594
|
+
`UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
|
|
1595
|
+
[newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
const newMaxTimestamp = newTimestamps[newTimestamps.length - 1] ?? null;
|
|
1601
|
+
return {
|
|
1602
|
+
operations: newOperations,
|
|
1603
|
+
idMapping,
|
|
1604
|
+
rebasedCount: newOperations.length,
|
|
1605
|
+
newMaxTimestamp
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1461
1609
|
// src/transaction/transaction-context.ts
|
|
1462
1610
|
import {
|
|
1463
1611
|
createOperation as createOperation2,
|
|
@@ -1607,7 +1755,9 @@ var TransactionContext = class {
|
|
|
1607
1755
|
type: "insert",
|
|
1608
1756
|
collection: collectionName,
|
|
1609
1757
|
recordId,
|
|
1610
|
-
|
|
1758
|
+
// Binary richtext values are tagged as canonical JSON before the
|
|
1759
|
+
// operation is content-hashed (see encodeRichtextFieldsForOpData).
|
|
1760
|
+
data: encodeRichtextFieldsForOpData(validated, definition.fields),
|
|
1611
1761
|
previousData: null,
|
|
1612
1762
|
sequenceNumber,
|
|
1613
1763
|
causalDeps,
|
|
@@ -1678,8 +1828,10 @@ var TransactionContext = class {
|
|
|
1678
1828
|
type: "update",
|
|
1679
1829
|
collection: collectionName,
|
|
1680
1830
|
recordId: id,
|
|
1681
|
-
|
|
1682
|
-
|
|
1831
|
+
// Binary richtext values (new and previous) are tagged as canonical
|
|
1832
|
+
// JSON before the operation is content-hashed.
|
|
1833
|
+
data: encodeRichtextFieldsForOpData(resolvedData, definition.fields),
|
|
1834
|
+
previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
|
|
1683
1835
|
sequenceNumber,
|
|
1684
1836
|
causalDeps,
|
|
1685
1837
|
schemaVersion: this.config.schema.version,
|
|
@@ -1786,14 +1938,16 @@ var TransactionContext = class {
|
|
|
1786
1938
|
if (entry.operation.type === "insert" && entry.operation.data) {
|
|
1787
1939
|
record = {
|
|
1788
1940
|
id,
|
|
1789
|
-
|
|
1941
|
+
// op.data stores binary richtext as tagged JSON; the effective
|
|
1942
|
+
// record must expose record-shaped values (Uint8Array/string).
|
|
1943
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
1790
1944
|
createdAt: entry.operation.timestamp.wallTime,
|
|
1791
1945
|
updatedAt: entry.operation.timestamp.wallTime
|
|
1792
1946
|
};
|
|
1793
1947
|
} else if (entry.operation.type === "update" && entry.operation.data && record) {
|
|
1794
1948
|
record = {
|
|
1795
1949
|
...record,
|
|
1796
|
-
...entry.operation.data,
|
|
1950
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
1797
1951
|
updatedAt: entry.operation.timestamp.wallTime
|
|
1798
1952
|
};
|
|
1799
1953
|
} else if (entry.operation.type === "delete") {
|
|
@@ -1873,6 +2027,7 @@ var Store = class {
|
|
|
1873
2027
|
localMutationHandler;
|
|
1874
2028
|
relationEnforcer = null;
|
|
1875
2029
|
causalTracker = null;
|
|
2030
|
+
secretKeyProvider;
|
|
1876
2031
|
constructor(config) {
|
|
1877
2032
|
this.schema = config.schema;
|
|
1878
2033
|
this.adapter = config.adapter;
|
|
@@ -1881,6 +2036,7 @@ var Store = class {
|
|
|
1881
2036
|
this.isolation = config.isolation ?? "shared";
|
|
1882
2037
|
this.emitter = config.emitter ?? null;
|
|
1883
2038
|
this.localMutationHandler = config.localMutationHandler ?? null;
|
|
2039
|
+
this.secretKeyProvider = config.secretKeyProvider;
|
|
1884
2040
|
this.subscriptionManager = new SubscriptionManager({
|
|
1885
2041
|
onQuerySubscribed: config.onQuerySubscribed
|
|
1886
2042
|
});
|
|
@@ -1893,7 +2049,7 @@ var Store = class {
|
|
|
1893
2049
|
await this.adapter.open(this.schema);
|
|
1894
2050
|
await this.runMigrationsIfNeeded();
|
|
1895
2051
|
this.nodeId = await this.loadOrGenerateNodeId();
|
|
1896
|
-
this.clock = new
|
|
2052
|
+
this.clock = new HybridLogicalClock3(this.nodeId);
|
|
1897
2053
|
this.causalTracker = new CausalTracker();
|
|
1898
2054
|
this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
|
|
1899
2055
|
this.sequenceNumber = await this.loadSequenceNumber();
|
|
@@ -1923,7 +2079,8 @@ var Store = class {
|
|
|
1923
2079
|
},
|
|
1924
2080
|
this.relationEnforcer,
|
|
1925
2081
|
this.localMutationHandler,
|
|
1926
|
-
this.causalTracker
|
|
2082
|
+
this.causalTracker,
|
|
2083
|
+
this.secretKeyProvider
|
|
1927
2084
|
);
|
|
1928
2085
|
this.collections.set(name, col);
|
|
1929
2086
|
}
|
|
@@ -1980,6 +2137,14 @@ var Store = class {
|
|
|
1980
2137
|
/**
|
|
1981
2138
|
* Get the node ID for this store instance.
|
|
1982
2139
|
*/
|
|
2140
|
+
/**
|
|
2141
|
+
* Records the offset between server time and this device's clock
|
|
2142
|
+
* (serverTime - localTime), learned at sync handshake. Lets the store's HLC
|
|
2143
|
+
* validate remote timestamps correctly even when the local clock is wrong.
|
|
2144
|
+
*/
|
|
2145
|
+
setClockReferenceOffset(offsetMs) {
|
|
2146
|
+
this.clock?.setReferenceOffset(offsetMs);
|
|
2147
|
+
}
|
|
1983
2148
|
getNodeId() {
|
|
1984
2149
|
this.ensureOpen();
|
|
1985
2150
|
return this.nodeId;
|
|
@@ -2006,45 +2171,158 @@ var Store = class {
|
|
|
2006
2171
|
if (this.clock) {
|
|
2007
2172
|
this.clock.receive(op.timestamp);
|
|
2008
2173
|
}
|
|
2009
|
-
const
|
|
2010
|
-
const
|
|
2174
|
+
const materializeTimestamp = options?.materializeTimestamp ?? op.timestamp;
|
|
2175
|
+
const remoteVersion = serializeRowVersion(materializeTimestamp);
|
|
2176
|
+
const wallTime = materializeTimestamp.wallTime;
|
|
2177
|
+
const materializeSource = options?.materializeData ?? op.data;
|
|
2011
2178
|
await this.adapter.transaction(async (tx) => {
|
|
2012
|
-
|
|
2179
|
+
const checkGuard = (row) => {
|
|
2180
|
+
const guard = options?.guardRowState;
|
|
2181
|
+
if (!guard) {
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const version = typeof row?._version === "string" ? row._version : null;
|
|
2185
|
+
const fieldVersions = typeof row?._field_versions === "string" ? row._field_versions : null;
|
|
2186
|
+
if (version !== guard.version || fieldVersions !== guard.fieldVersions) {
|
|
2187
|
+
throw new OptimisticLockError(collection, op.recordId);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
if (op.type === "insert" && materializeSource) {
|
|
2191
|
+
const serializedData = serializeRecord(materializeSource, definition.fields);
|
|
2013
2192
|
const existing2 = await tx.query(
|
|
2014
|
-
`SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
|
|
2193
|
+
`SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
|
|
2015
2194
|
[op.recordId]
|
|
2016
2195
|
);
|
|
2017
2196
|
const row = existing2[0];
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
const serializedData = serializeRecord(op.data, definition.fields);
|
|
2197
|
+
checkGuard(row);
|
|
2198
|
+
if (!row) {
|
|
2021
2199
|
const record = {
|
|
2022
2200
|
id: op.recordId,
|
|
2023
2201
|
...serializedData,
|
|
2024
2202
|
_created_at: wallTime,
|
|
2025
2203
|
_updated_at: wallTime,
|
|
2026
|
-
_version: remoteVersion
|
|
2204
|
+
_version: remoteVersion,
|
|
2205
|
+
// Stamp every inserted field so per-field LWW has a real baseline.
|
|
2206
|
+
_field_versions: serializeFieldVersions(
|
|
2207
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
2208
|
+
)
|
|
2027
2209
|
};
|
|
2028
2210
|
const insertQuery = buildInsertQuery(collection, record);
|
|
2029
2211
|
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
2212
|
+
await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
|
|
2213
|
+
} else if (row._deleted === 1) {
|
|
2214
|
+
if (isIncomingNewerThanRow(materializeTimestamp, row)) {
|
|
2215
|
+
const fieldChanges = {
|
|
2216
|
+
...serializedData,
|
|
2217
|
+
_deleted: 0,
|
|
2218
|
+
_field_versions: serializeFieldVersions(
|
|
2219
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
2220
|
+
)
|
|
2221
|
+
};
|
|
2222
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
2223
|
+
collection,
|
|
2224
|
+
op.recordId,
|
|
2225
|
+
fieldChanges,
|
|
2226
|
+
remoteVersion,
|
|
2227
|
+
wallTime,
|
|
2228
|
+
{ maxCreatedAt: wallTime }
|
|
2229
|
+
);
|
|
2230
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
2231
|
+
}
|
|
2232
|
+
} else {
|
|
2233
|
+
const currentVersions = parseFieldVersions(row._field_versions);
|
|
2234
|
+
const rowVersion = typeof row._version === "string" ? row._version : void 0;
|
|
2235
|
+
const fieldChanges = {};
|
|
2236
|
+
let fieldVersionsJson;
|
|
2237
|
+
if (options?.forceMaterialize) {
|
|
2238
|
+
const merged = { ...currentVersions };
|
|
2239
|
+
for (const field of Object.keys(serializedData)) {
|
|
2240
|
+
fieldChanges[field] = serializedData[field];
|
|
2241
|
+
merged[field] = remoteVersion;
|
|
2242
|
+
}
|
|
2243
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
2244
|
+
} else {
|
|
2245
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
2246
|
+
currentVersions,
|
|
2247
|
+
Object.keys(serializedData),
|
|
2248
|
+
remoteVersion,
|
|
2249
|
+
rowVersion
|
|
2250
|
+
);
|
|
2251
|
+
for (const field of winners) {
|
|
2252
|
+
fieldChanges[field] = serializedData[field];
|
|
2253
|
+
}
|
|
2254
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
2255
|
+
}
|
|
2256
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
2257
|
+
collection,
|
|
2258
|
+
op.recordId,
|
|
2259
|
+
{ ...fieldChanges, _field_versions: fieldVersionsJson },
|
|
2260
|
+
remoteVersion,
|
|
2261
|
+
wallTime,
|
|
2262
|
+
{ maxCreatedAt: wallTime }
|
|
2263
|
+
);
|
|
2264
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
2030
2265
|
}
|
|
2031
|
-
} else if (op.type === "update" &&
|
|
2032
|
-
const serializedChanges = serializeRecord(
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
};
|
|
2038
|
-
if (options?.reactivateIfDeleted) {
|
|
2039
|
-
updatePayload._deleted = 0;
|
|
2040
|
-
}
|
|
2041
|
-
const updateQuery = buildLwwUpdateQuery(
|
|
2042
|
-
collection,
|
|
2043
|
-
op.recordId,
|
|
2044
|
-
updatePayload,
|
|
2045
|
-
remoteVersion
|
|
2266
|
+
} else if (op.type === "update" && materializeSource) {
|
|
2267
|
+
const serializedChanges = serializeRecord(materializeSource, definition.fields);
|
|
2268
|
+
const changedFields = Object.keys(serializedChanges);
|
|
2269
|
+
const currentRows = await tx.query(
|
|
2270
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2271
|
+
[op.recordId]
|
|
2046
2272
|
);
|
|
2047
|
-
|
|
2273
|
+
const currentRow = currentRows[0];
|
|
2274
|
+
checkGuard(currentRow);
|
|
2275
|
+
if (currentRow) {
|
|
2276
|
+
const currentVersions = parseFieldVersions(currentRow._field_versions);
|
|
2277
|
+
const rowVersion = typeof currentRow._version === "string" ? currentRow._version : void 0;
|
|
2278
|
+
if (options?.forceMaterialize) {
|
|
2279
|
+
const merged = { ...currentVersions };
|
|
2280
|
+
for (const field of changedFields) {
|
|
2281
|
+
merged[field] = remoteVersion;
|
|
2282
|
+
}
|
|
2283
|
+
const fieldChanges = {
|
|
2284
|
+
...serializedChanges,
|
|
2285
|
+
_field_versions: serializeFieldVersions(merged)
|
|
2286
|
+
};
|
|
2287
|
+
if (options?.reactivateIfDeleted) {
|
|
2288
|
+
fieldChanges._deleted = 0;
|
|
2289
|
+
}
|
|
2290
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
2291
|
+
collection,
|
|
2292
|
+
op.recordId,
|
|
2293
|
+
fieldChanges,
|
|
2294
|
+
remoteVersion,
|
|
2295
|
+
wallTime
|
|
2296
|
+
);
|
|
2297
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
2298
|
+
} else {
|
|
2299
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
2300
|
+
currentVersions,
|
|
2301
|
+
changedFields,
|
|
2302
|
+
remoteVersion,
|
|
2303
|
+
rowVersion
|
|
2304
|
+
);
|
|
2305
|
+
if (winners.length > 0 || options?.reactivateIfDeleted) {
|
|
2306
|
+
const winningChanges = {
|
|
2307
|
+
_field_versions: serializeFieldVersions(merged)
|
|
2308
|
+
};
|
|
2309
|
+
for (const field of winners) {
|
|
2310
|
+
winningChanges[field] = serializedChanges[field];
|
|
2311
|
+
}
|
|
2312
|
+
if (options?.reactivateIfDeleted) {
|
|
2313
|
+
winningChanges._deleted = 0;
|
|
2314
|
+
}
|
|
2315
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
2316
|
+
collection,
|
|
2317
|
+
op.recordId,
|
|
2318
|
+
winningChanges,
|
|
2319
|
+
remoteVersion,
|
|
2320
|
+
wallTime
|
|
2321
|
+
);
|
|
2322
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2048
2326
|
} else if (op.type === "delete") {
|
|
2049
2327
|
const deleteQuery = buildLwwSoftDeleteQuery(
|
|
2050
2328
|
collection,
|
|
@@ -2072,6 +2350,67 @@ var Store = class {
|
|
|
2072
2350
|
this.subscriptionManager.notify(collection, op);
|
|
2073
2351
|
return "applied";
|
|
2074
2352
|
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Materialize update/delete operations that were logged for a record BEFORE
|
|
2355
|
+
* its insert arrived (reordered delivery). Runs inside the insert's write
|
|
2356
|
+
* transaction, folding each orphan in timestamp order through the exact
|
|
2357
|
+
* per-field LWW / LWW-delete rules the normal apply paths use, so the final
|
|
2358
|
+
* row state equals what a device that received the operations in causal
|
|
2359
|
+
* order computed. Called only when the insert created a previously-absent
|
|
2360
|
+
* row — in that case every logged update/delete for the record is
|
|
2361
|
+
* necessarily an orphan (rows are soft-deleted, never removed, so a row
|
|
2362
|
+
* that is absent now was never materialized).
|
|
2363
|
+
*/
|
|
2364
|
+
async foldOrphanedOperations(tx, collection, definition, recordId) {
|
|
2365
|
+
const orphanRows = await tx.query(
|
|
2366
|
+
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
|
|
2367
|
+
[recordId]
|
|
2368
|
+
);
|
|
2369
|
+
if (orphanRows.length === 0) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const orphans = orphanRows.map((r) => deserializeOperationWithCollection(r, collection)).sort((a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp));
|
|
2373
|
+
for (const orphan of orphans) {
|
|
2374
|
+
const orphanVersion = serializeRowVersion(orphan.timestamp);
|
|
2375
|
+
const orphanWall = orphan.timestamp.wallTime;
|
|
2376
|
+
if (orphan.type === "update" && orphan.data) {
|
|
2377
|
+
const orphanChanges = serializeRecord(orphan.data, definition.fields);
|
|
2378
|
+
const rows = await tx.query(
|
|
2379
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2380
|
+
[recordId]
|
|
2381
|
+
);
|
|
2382
|
+
const current = rows[0];
|
|
2383
|
+
if (!current) {
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
2387
|
+
parseFieldVersions(current._field_versions),
|
|
2388
|
+
Object.keys(orphanChanges),
|
|
2389
|
+
orphanVersion,
|
|
2390
|
+
typeof current._version === "string" ? current._version : void 0
|
|
2391
|
+
);
|
|
2392
|
+
if (winners.length > 0) {
|
|
2393
|
+
const winningChanges = {
|
|
2394
|
+
_field_versions: serializeFieldVersions(merged)
|
|
2395
|
+
};
|
|
2396
|
+
for (const field of winners) {
|
|
2397
|
+
winningChanges[field] = orphanChanges[field];
|
|
2398
|
+
}
|
|
2399
|
+
const query = buildFieldFastForwardUpdateQuery(
|
|
2400
|
+
collection,
|
|
2401
|
+
recordId,
|
|
2402
|
+
winningChanges,
|
|
2403
|
+
orphanVersion,
|
|
2404
|
+
orphanWall
|
|
2405
|
+
);
|
|
2406
|
+
await tx.execute(query.sql, query.params);
|
|
2407
|
+
}
|
|
2408
|
+
} else if (orphan.type === "delete") {
|
|
2409
|
+
const query = buildLwwSoftDeleteQuery(collection, recordId, orphanWall, orphanVersion);
|
|
2410
|
+
await tx.execute(query.sql, query.params);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2075
2414
|
/**
|
|
2076
2415
|
* Get operations from a node within a sequence number range.
|
|
2077
2416
|
* Implements the OperationLog interface for computeDelta.
|
|
@@ -2157,7 +2496,7 @@ var Store = class {
|
|
|
2157
2496
|
*/
|
|
2158
2497
|
async exportAudit(options) {
|
|
2159
2498
|
this.ensureOpen();
|
|
2160
|
-
const { exportAudit: doExport } = await import("./export-audit-
|
|
2499
|
+
const { exportAudit: doExport } = await import("./export-audit-AJ7RORNK.js");
|
|
2161
2500
|
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
2162
2501
|
}
|
|
2163
2502
|
/**
|
|
@@ -2202,7 +2541,8 @@ var Store = class {
|
|
|
2202
2541
|
relationEnforcer: this.relationEnforcer,
|
|
2203
2542
|
causalTracker: this.causalTracker,
|
|
2204
2543
|
inTransaction: options?.inTransaction ?? false,
|
|
2205
|
-
extraCausalDeps: options?.extraCausalDeps
|
|
2544
|
+
extraCausalDeps: options?.extraCausalDeps,
|
|
2545
|
+
secretKeyProvider: this.secretKeyProvider
|
|
2206
2546
|
};
|
|
2207
2547
|
}
|
|
2208
2548
|
/**
|
|
@@ -2227,6 +2567,31 @@ var Store = class {
|
|
|
2227
2567
|
deleted: row._deleted === 1
|
|
2228
2568
|
};
|
|
2229
2569
|
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Raw version state of a materialized row, for optimistic-concurrency guarded
|
|
2572
|
+
* applies: capture this snapshot before computing a merge, pass it as
|
|
2573
|
+
* `guardRowState`, and the apply refuses to write if the row changed since.
|
|
2574
|
+
* Returns null when no row exists (which is itself a valid guard state — the
|
|
2575
|
+
* apply then requires the row to still be absent).
|
|
2576
|
+
*/
|
|
2577
|
+
async getRowVersionState(collection, recordId) {
|
|
2578
|
+
this.ensureOpen();
|
|
2579
|
+
if (!this.schema.collections[collection]) {
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
const rows = await this.adapter.query(
|
|
2583
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2584
|
+
[recordId]
|
|
2585
|
+
);
|
|
2586
|
+
const row = rows[0];
|
|
2587
|
+
if (!row) {
|
|
2588
|
+
return null;
|
|
2589
|
+
}
|
|
2590
|
+
return {
|
|
2591
|
+
version: typeof row._version === "string" ? row._version : null,
|
|
2592
|
+
fieldVersions: typeof row._field_versions === "string" ? row._field_versions : null
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2230
2595
|
/**
|
|
2231
2596
|
* Latest operation from this device for a record (used for delete-vs-update merge on sync).
|
|
2232
2597
|
*/
|
|
@@ -2269,6 +2634,30 @@ var Store = class {
|
|
|
2269
2634
|
(nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
|
|
2270
2635
|
);
|
|
2271
2636
|
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Re-stamp never-acknowledged local operations after a fast device clock was
|
|
2639
|
+
* corrected, so sync can resume immediately. Safe because unacknowledged
|
|
2640
|
+
* operations are private to this device (like unpushed git commits);
|
|
2641
|
+
* acknowledged/shared operations are immutable and never touched.
|
|
2642
|
+
*
|
|
2643
|
+
* After the rewrite the store's HLC is advanced past the highest new
|
|
2644
|
+
* timestamp so subsequent local writes keep sorting after the rebased ops.
|
|
2645
|
+
* No data-change notifications are emitted: materialized values are
|
|
2646
|
+
* unchanged, only version stamps move.
|
|
2647
|
+
*/
|
|
2648
|
+
async rebaseUnsyncedOperations(unsyncedOpIds, correctedNowMs) {
|
|
2649
|
+
this.ensureOpen();
|
|
2650
|
+
const result = await rebaseUnsyncedOperationsInLog(
|
|
2651
|
+
this.adapter,
|
|
2652
|
+
this.schema,
|
|
2653
|
+
unsyncedOpIds,
|
|
2654
|
+
correctedNowMs
|
|
2655
|
+
);
|
|
2656
|
+
if (result.newMaxTimestamp && this.clock) {
|
|
2657
|
+
this.clock.advanceTo(result.newMaxTimestamp);
|
|
2658
|
+
}
|
|
2659
|
+
return result;
|
|
2660
|
+
}
|
|
2272
2661
|
/**
|
|
2273
2662
|
* Count of local operations ahead of the server version vector.
|
|
2274
2663
|
*/
|
|
@@ -2318,7 +2707,7 @@ var Store = class {
|
|
|
2318
2707
|
let latest = null;
|
|
2319
2708
|
for (const row of rows) {
|
|
2320
2709
|
const op = deserializeOperationWithCollection(row, collection);
|
|
2321
|
-
if (!latest ||
|
|
2710
|
+
if (!latest || HybridLogicalClock3.compare(op.timestamp, latest.timestamp) > 0) {
|
|
2322
2711
|
latest = op;
|
|
2323
2712
|
}
|
|
2324
2713
|
}
|
|
@@ -2402,7 +2791,7 @@ var Store = class {
|
|
|
2402
2791
|
*/
|
|
2403
2792
|
async exportBackup(options) {
|
|
2404
2793
|
this.ensureOpen();
|
|
2405
|
-
const { exportBackup: doExport } = await import("./backup-
|
|
2794
|
+
const { exportBackup: doExport } = await import("./backup-ZV4Y424B.js");
|
|
2406
2795
|
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
2407
2796
|
}
|
|
2408
2797
|
/**
|
|
@@ -2414,7 +2803,7 @@ var Store = class {
|
|
|
2414
2803
|
*/
|
|
2415
2804
|
async importBackup(data, options) {
|
|
2416
2805
|
this.ensureOpen();
|
|
2417
|
-
const { restoreBackup: doRestore } = await import("./backup-
|
|
2806
|
+
const { restoreBackup: doRestore } = await import("./backup-ZV4Y424B.js");
|
|
2418
2807
|
return doRestore(this.adapter, this.schema, data, options);
|
|
2419
2808
|
}
|
|
2420
2809
|
/**
|
|
@@ -3044,12 +3433,519 @@ function asRichTextSyncEngine(engine) {
|
|
|
3044
3433
|
}
|
|
3045
3434
|
return engine;
|
|
3046
3435
|
}
|
|
3436
|
+
|
|
3437
|
+
// src/blob/opfs-blob-store.ts
|
|
3438
|
+
import { createBlobRef, hashBlob } from "@korajs/core";
|
|
3439
|
+
var OpfsBlobStore = class {
|
|
3440
|
+
constructor(dir) {
|
|
3441
|
+
this.dir = dir;
|
|
3442
|
+
}
|
|
3443
|
+
dir;
|
|
3444
|
+
shardFor(hash) {
|
|
3445
|
+
return hash.slice(0, 2);
|
|
3446
|
+
}
|
|
3447
|
+
async put(bytes, metadata = {}) {
|
|
3448
|
+
const ref = await createBlobRef(bytes, metadata);
|
|
3449
|
+
const shard = this.shardFor(ref.hash);
|
|
3450
|
+
if (await this.dir.has(shard, ref.hash)) {
|
|
3451
|
+
return ref;
|
|
3452
|
+
}
|
|
3453
|
+
const out = new Uint8Array(bytes.byteLength);
|
|
3454
|
+
out.set(bytes);
|
|
3455
|
+
await this.dir.write(shard, ref.hash, out);
|
|
3456
|
+
return ref;
|
|
3457
|
+
}
|
|
3458
|
+
async get(hash) {
|
|
3459
|
+
const bytes = await this.dir.read(this.shardFor(hash), hash);
|
|
3460
|
+
if (bytes === null) {
|
|
3461
|
+
return null;
|
|
3462
|
+
}
|
|
3463
|
+
const actual = await hashBlob(bytes);
|
|
3464
|
+
if (actual !== hash) {
|
|
3465
|
+
throw new BlobIntegrityError(hash, actual);
|
|
3466
|
+
}
|
|
3467
|
+
return bytes;
|
|
3468
|
+
}
|
|
3469
|
+
async has(hash) {
|
|
3470
|
+
return this.dir.has(this.shardFor(hash), hash);
|
|
3471
|
+
}
|
|
3472
|
+
async delete(hash) {
|
|
3473
|
+
return this.dir.remove(this.shardFor(hash), hash);
|
|
3474
|
+
}
|
|
3475
|
+
async size() {
|
|
3476
|
+
return this.dir.count();
|
|
3477
|
+
}
|
|
3478
|
+
async list() {
|
|
3479
|
+
return this.dir.keys();
|
|
3480
|
+
}
|
|
3481
|
+
};
|
|
3482
|
+
function getStorageManager() {
|
|
3483
|
+
const nav = globalThis.navigator;
|
|
3484
|
+
if (!nav?.storage || typeof nav.storage.getDirectory !== "function") {
|
|
3485
|
+
throw new Error(
|
|
3486
|
+
"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."
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
return nav.storage;
|
|
3490
|
+
}
|
|
3491
|
+
function isNotFoundError(error) {
|
|
3492
|
+
return typeof error === "object" && error !== null && error.name === "NotFoundError";
|
|
3493
|
+
}
|
|
3494
|
+
async function createOpfsBlobDirectory(rootDirName = "kora-blobs") {
|
|
3495
|
+
const storage = getStorageManager();
|
|
3496
|
+
if (typeof storage.persist === "function") {
|
|
3497
|
+
try {
|
|
3498
|
+
await storage.persist();
|
|
3499
|
+
} catch {
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
const opfsRoot = await storage.getDirectory();
|
|
3503
|
+
const root = await opfsRoot.getDirectoryHandle(rootDirName, { create: true });
|
|
3504
|
+
async function shardHandle(shard, create) {
|
|
3505
|
+
try {
|
|
3506
|
+
return await root.getDirectoryHandle(shard, { create });
|
|
3507
|
+
} catch (error) {
|
|
3508
|
+
if (isNotFoundError(error)) {
|
|
3509
|
+
return null;
|
|
3510
|
+
}
|
|
3511
|
+
throw error;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return {
|
|
3515
|
+
async read(shard, name) {
|
|
3516
|
+
const dir = await shardHandle(shard, false);
|
|
3517
|
+
if (!dir) {
|
|
3518
|
+
return null;
|
|
3519
|
+
}
|
|
3520
|
+
let handle;
|
|
3521
|
+
try {
|
|
3522
|
+
handle = await dir.getFileHandle(name, { create: false });
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
if (isNotFoundError(error)) {
|
|
3525
|
+
return null;
|
|
3526
|
+
}
|
|
3527
|
+
throw error;
|
|
3528
|
+
}
|
|
3529
|
+
const file = await handle.getFile();
|
|
3530
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
3531
|
+
},
|
|
3532
|
+
async write(shard, name, bytes) {
|
|
3533
|
+
const dir = await root.getDirectoryHandle(shard, { create: true });
|
|
3534
|
+
const handle = await dir.getFileHandle(name, { create: true });
|
|
3535
|
+
const writable = await handle.createWritable({ keepExistingData: false });
|
|
3536
|
+
try {
|
|
3537
|
+
await writable.write(bytes);
|
|
3538
|
+
} finally {
|
|
3539
|
+
await writable.close();
|
|
3540
|
+
}
|
|
3541
|
+
},
|
|
3542
|
+
async remove(shard, name) {
|
|
3543
|
+
const dir = await shardHandle(shard, false);
|
|
3544
|
+
if (!dir) {
|
|
3545
|
+
return false;
|
|
3546
|
+
}
|
|
3547
|
+
try {
|
|
3548
|
+
await dir.removeEntry(name);
|
|
3549
|
+
return true;
|
|
3550
|
+
} catch (error) {
|
|
3551
|
+
if (isNotFoundError(error)) {
|
|
3552
|
+
return false;
|
|
3553
|
+
}
|
|
3554
|
+
throw error;
|
|
3555
|
+
}
|
|
3556
|
+
},
|
|
3557
|
+
async has(shard, name) {
|
|
3558
|
+
const dir = await shardHandle(shard, false);
|
|
3559
|
+
if (!dir) {
|
|
3560
|
+
return false;
|
|
3561
|
+
}
|
|
3562
|
+
try {
|
|
3563
|
+
await dir.getFileHandle(name, { create: false });
|
|
3564
|
+
return true;
|
|
3565
|
+
} catch (error) {
|
|
3566
|
+
if (isNotFoundError(error)) {
|
|
3567
|
+
return false;
|
|
3568
|
+
}
|
|
3569
|
+
throw error;
|
|
3570
|
+
}
|
|
3571
|
+
},
|
|
3572
|
+
async count() {
|
|
3573
|
+
let total = 0;
|
|
3574
|
+
for await (const shard of root.keys()) {
|
|
3575
|
+
const dir = await shardHandle(shard, false);
|
|
3576
|
+
if (!dir) {
|
|
3577
|
+
continue;
|
|
3578
|
+
}
|
|
3579
|
+
for await (const name of dir.keys()) {
|
|
3580
|
+
if (!name.endsWith(".tmp")) {
|
|
3581
|
+
total++;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
return total;
|
|
3586
|
+
},
|
|
3587
|
+
async keys() {
|
|
3588
|
+
const names = [];
|
|
3589
|
+
for await (const shard of root.keys()) {
|
|
3590
|
+
const dir = await shardHandle(shard, false);
|
|
3591
|
+
if (!dir) {
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3594
|
+
for await (const name of dir.keys()) {
|
|
3595
|
+
if (!name.endsWith(".tmp")) {
|
|
3596
|
+
names.push(name);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
return names;
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
async function createOpfsBlobStore(rootDirName = "kora-blobs") {
|
|
3605
|
+
return new OpfsBlobStore(await createOpfsBlobDirectory(rootDirName));
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// src/blob/blob-chunking.ts
|
|
3609
|
+
import { hashBlob as hashBlob2 } from "@korajs/core";
|
|
3610
|
+
var DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
3611
|
+
async function chunkBlob(bytes, chunkSize = DEFAULT_CHUNK_SIZE, metadata = {}) {
|
|
3612
|
+
if (chunkSize <= 0) {
|
|
3613
|
+
throw new Error(`chunkBlob requires a positive chunkSize, got ${chunkSize}`);
|
|
3614
|
+
}
|
|
3615
|
+
const chunkHashes = [];
|
|
3616
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
3617
|
+
for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
|
|
3618
|
+
const end = Math.min(offset + chunkSize, bytes.byteLength);
|
|
3619
|
+
const chunk = new Uint8Array(end - offset);
|
|
3620
|
+
chunk.set(bytes.subarray(offset, end));
|
|
3621
|
+
const hash = await hashBlob2(chunk);
|
|
3622
|
+
chunkHashes.push(hash);
|
|
3623
|
+
if (!chunks.has(hash)) {
|
|
3624
|
+
chunks.set(hash, chunk);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
const blobHash = await hashBlob2(bytes);
|
|
3628
|
+
const manifest = {
|
|
3629
|
+
blobHash,
|
|
3630
|
+
size: bytes.byteLength,
|
|
3631
|
+
chunkSize,
|
|
3632
|
+
chunkHashes
|
|
3633
|
+
};
|
|
3634
|
+
if (metadata.mimeType !== void 0) {
|
|
3635
|
+
manifest.mimeType = metadata.mimeType;
|
|
3636
|
+
}
|
|
3637
|
+
if (metadata.filename !== void 0) {
|
|
3638
|
+
manifest.filename = metadata.filename;
|
|
3639
|
+
}
|
|
3640
|
+
return { manifest, chunks };
|
|
3641
|
+
}
|
|
3642
|
+
async function reassembleBlob(manifest, chunkStore) {
|
|
3643
|
+
const parts = [];
|
|
3644
|
+
let total = 0;
|
|
3645
|
+
for (const hash of manifest.chunkHashes) {
|
|
3646
|
+
const chunk = await chunkStore.get(hash);
|
|
3647
|
+
if (chunk === null) {
|
|
3648
|
+
throw new Error(`Cannot reassemble blob ${manifest.blobHash}: missing chunk ${hash}`);
|
|
3649
|
+
}
|
|
3650
|
+
parts.push(chunk);
|
|
3651
|
+
total += chunk.byteLength;
|
|
3652
|
+
}
|
|
3653
|
+
const out = new Uint8Array(total);
|
|
3654
|
+
let offset = 0;
|
|
3655
|
+
for (const part of parts) {
|
|
3656
|
+
out.set(part, offset);
|
|
3657
|
+
offset += part.byteLength;
|
|
3658
|
+
}
|
|
3659
|
+
const actual = await hashBlob2(out);
|
|
3660
|
+
if (actual !== manifest.blobHash) {
|
|
3661
|
+
throw new BlobIntegrityError(manifest.blobHash, actual);
|
|
3662
|
+
}
|
|
3663
|
+
return out;
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
// src/blob/blob-transfer.ts
|
|
3667
|
+
import { hashBlob as hashBlob3 } from "@korajs/core";
|
|
3668
|
+
async function receiveBlob(manifest, provider, stores) {
|
|
3669
|
+
let chunksFetched = 0;
|
|
3670
|
+
let chunksSkipped = 0;
|
|
3671
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3672
|
+
for (const hash of manifest.chunkHashes) {
|
|
3673
|
+
if (seen.has(hash)) {
|
|
3674
|
+
continue;
|
|
3675
|
+
}
|
|
3676
|
+
seen.add(hash);
|
|
3677
|
+
if (await stores.chunkStore.has(hash)) {
|
|
3678
|
+
chunksSkipped++;
|
|
3679
|
+
continue;
|
|
3680
|
+
}
|
|
3681
|
+
const bytes = await provider.getChunk(hash);
|
|
3682
|
+
if (bytes === null) {
|
|
3683
|
+
throw new Error(`Blob transfer failed: provider could not supply chunk ${hash}`);
|
|
3684
|
+
}
|
|
3685
|
+
const actual = await hashBlob3(bytes);
|
|
3686
|
+
if (actual !== hash) {
|
|
3687
|
+
throw new BlobIntegrityError(hash, actual);
|
|
3688
|
+
}
|
|
3689
|
+
await stores.chunkStore.put(bytes);
|
|
3690
|
+
chunksFetched++;
|
|
3691
|
+
}
|
|
3692
|
+
const full = await reassembleBlob(manifest, stores.chunkStore);
|
|
3693
|
+
const ref = await stores.blobStore.put(full, {
|
|
3694
|
+
mimeType: manifest.mimeType,
|
|
3695
|
+
filename: manifest.filename
|
|
3696
|
+
});
|
|
3697
|
+
return { ref, chunksFetched, chunksSkipped };
|
|
3698
|
+
}
|
|
3699
|
+
async function prepareBlobForSend(bytes, chunkStore, options = {}) {
|
|
3700
|
+
const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
|
|
3701
|
+
mimeType: options.mimeType,
|
|
3702
|
+
filename: options.filename
|
|
3703
|
+
});
|
|
3704
|
+
for (const chunk of chunks.values()) {
|
|
3705
|
+
await chunkStore.put(chunk);
|
|
3706
|
+
}
|
|
3707
|
+
const provider = {
|
|
3708
|
+
getChunk: (hash) => chunkStore.get(hash)
|
|
3709
|
+
};
|
|
3710
|
+
return { manifest, provider };
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
// src/blob/blob-manifest-transfer.ts
|
|
3714
|
+
import { hashBlob as hashBlob4 } from "@korajs/core";
|
|
3715
|
+
function serializeBlobManifest(manifest) {
|
|
3716
|
+
const canonical = {
|
|
3717
|
+
blobHash: manifest.blobHash,
|
|
3718
|
+
size: manifest.size,
|
|
3719
|
+
chunkSize: manifest.chunkSize,
|
|
3720
|
+
chunkHashes: manifest.chunkHashes
|
|
3721
|
+
};
|
|
3722
|
+
if (manifest.mimeType !== void 0) {
|
|
3723
|
+
canonical.mimeType = manifest.mimeType;
|
|
3724
|
+
}
|
|
3725
|
+
if (manifest.filename !== void 0) {
|
|
3726
|
+
canonical.filename = manifest.filename;
|
|
3727
|
+
}
|
|
3728
|
+
return new TextEncoder().encode(JSON.stringify(canonical));
|
|
3729
|
+
}
|
|
3730
|
+
function parseBlobManifest(bytes) {
|
|
3731
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
3732
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
3733
|
+
throw new Error("Invalid blob manifest: not an object");
|
|
3734
|
+
}
|
|
3735
|
+
const obj = parsed;
|
|
3736
|
+
if (typeof obj.blobHash !== "string") {
|
|
3737
|
+
throw new Error("Invalid blob manifest: missing blobHash");
|
|
3738
|
+
}
|
|
3739
|
+
if (typeof obj.size !== "number" || typeof obj.chunkSize !== "number") {
|
|
3740
|
+
throw new Error("Invalid blob manifest: missing size or chunkSize");
|
|
3741
|
+
}
|
|
3742
|
+
if (!Array.isArray(obj.chunkHashes) || obj.chunkHashes.some((h) => typeof h !== "string")) {
|
|
3743
|
+
throw new Error("Invalid blob manifest: chunkHashes must be an array of strings");
|
|
3744
|
+
}
|
|
3745
|
+
const manifest = {
|
|
3746
|
+
blobHash: obj.blobHash,
|
|
3747
|
+
size: obj.size,
|
|
3748
|
+
chunkSize: obj.chunkSize,
|
|
3749
|
+
chunkHashes: obj.chunkHashes
|
|
3750
|
+
};
|
|
3751
|
+
if (typeof obj.mimeType === "string") {
|
|
3752
|
+
manifest.mimeType = obj.mimeType;
|
|
3753
|
+
}
|
|
3754
|
+
if (typeof obj.filename === "string") {
|
|
3755
|
+
manifest.filename = obj.filename;
|
|
3756
|
+
}
|
|
3757
|
+
return manifest;
|
|
3758
|
+
}
|
|
3759
|
+
async function putBlobForTransfer(store, bytes, options = {}) {
|
|
3760
|
+
const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
|
|
3761
|
+
...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
|
|
3762
|
+
...options.filename !== void 0 ? { filename: options.filename } : {}
|
|
3763
|
+
});
|
|
3764
|
+
for (const chunk of chunks.values()) {
|
|
3765
|
+
await store.put(chunk);
|
|
3766
|
+
}
|
|
3767
|
+
const baseRef = await store.put(bytes, {
|
|
3768
|
+
...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
|
|
3769
|
+
...options.filename !== void 0 ? { filename: options.filename } : {}
|
|
3770
|
+
});
|
|
3771
|
+
const manifestRef = await store.put(serializeBlobManifest(manifest));
|
|
3772
|
+
return { ref: { ...baseRef, manifestHash: manifestRef.hash }, manifest };
|
|
3773
|
+
}
|
|
3774
|
+
async function fetchBlobManifest(provider, manifestHash) {
|
|
3775
|
+
const bytes = await provider.getChunk(manifestHash);
|
|
3776
|
+
if (bytes === null) {
|
|
3777
|
+
throw new Error(`Blob manifest ${manifestHash} could not be fetched from the provider`);
|
|
3778
|
+
}
|
|
3779
|
+
const actual = await hashBlob4(bytes);
|
|
3780
|
+
if (actual !== manifestHash) {
|
|
3781
|
+
throw new BlobIntegrityError(manifestHash, actual);
|
|
3782
|
+
}
|
|
3783
|
+
return parseBlobManifest(bytes);
|
|
3784
|
+
}
|
|
3785
|
+
async function resolveBlobManifest(provider, ref) {
|
|
3786
|
+
if (ref.manifestHash === void 0) {
|
|
3787
|
+
throw new Error(
|
|
3788
|
+
`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.`
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
return fetchBlobManifest(provider, ref.manifestHash);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
// src/blob/server-blob-callbacks.ts
|
|
3795
|
+
import { hashBlob as hashBlob5 } from "@korajs/core";
|
|
3796
|
+
function toServerBlobCallbacks(store) {
|
|
3797
|
+
return {
|
|
3798
|
+
resolveBlobChunk: (hash) => store.get(hash),
|
|
3799
|
+
async persistBlobChunk(hash, bytes) {
|
|
3800
|
+
const actual = await hashBlob5(bytes);
|
|
3801
|
+
if (actual !== hash) {
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
await store.put(bytes);
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
function createMemoryServerBlobStore() {
|
|
3809
|
+
const store = new MemoryBlobStore();
|
|
3810
|
+
return { store, callbacks: toServerBlobCallbacks(store) };
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
// src/blob/blob-gc.ts
|
|
3814
|
+
import { isBlobRef } from "@korajs/core";
|
|
3815
|
+
function extractBlobRefs(record) {
|
|
3816
|
+
const refs = [];
|
|
3817
|
+
for (const value of Object.values(record)) {
|
|
3818
|
+
if (isBlobRef(value)) {
|
|
3819
|
+
refs.push(value);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return refs;
|
|
3823
|
+
}
|
|
3824
|
+
async function collectBlobGarbage(store, liveRefs, options = {}) {
|
|
3825
|
+
const live = /* @__PURE__ */ new Set();
|
|
3826
|
+
for (const ref of liveRefs) {
|
|
3827
|
+
live.add(ref.hash);
|
|
3828
|
+
if (ref.manifestHash === void 0) {
|
|
3829
|
+
continue;
|
|
3830
|
+
}
|
|
3831
|
+
live.add(ref.manifestHash);
|
|
3832
|
+
try {
|
|
3833
|
+
const manifestBytes = await store.get(ref.manifestHash);
|
|
3834
|
+
if (manifestBytes !== null) {
|
|
3835
|
+
const manifest = parseBlobManifest(manifestBytes);
|
|
3836
|
+
for (const chunkHash of manifest.chunkHashes) {
|
|
3837
|
+
live.add(chunkHash);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
} catch {
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
const all = await store.list();
|
|
3844
|
+
const collectedHashes = [];
|
|
3845
|
+
for (const hash of all) {
|
|
3846
|
+
if (live.has(hash)) {
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
collectedHashes.push(hash);
|
|
3850
|
+
if (!options.dryRun) {
|
|
3851
|
+
await store.delete(hash);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
return {
|
|
3855
|
+
scanned: all.length,
|
|
3856
|
+
live: live.size,
|
|
3857
|
+
collected: collectedHashes.length,
|
|
3858
|
+
collectedHashes
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
// src/blob/blob-chunk-transport.ts
|
|
3863
|
+
import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
|
|
3864
|
+
function createRemoteChunkProvider(port, options = {}) {
|
|
3865
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
3866
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3867
|
+
port.onMessage((message) => {
|
|
3868
|
+
if (message.type !== "blob-chunk-response") {
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
const entry = pending.get(message.requestId);
|
|
3872
|
+
if (!entry) {
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
pending.delete(message.requestId);
|
|
3876
|
+
clearTimeout(entry.timer);
|
|
3877
|
+
entry.resolve(message.bytes);
|
|
3878
|
+
});
|
|
3879
|
+
return {
|
|
3880
|
+
pendingCount: () => pending.size,
|
|
3881
|
+
getChunk(hash) {
|
|
3882
|
+
const requestId = generateUUIDv74();
|
|
3883
|
+
return new Promise((resolve, reject) => {
|
|
3884
|
+
const timer = setTimeout(() => {
|
|
3885
|
+
pending.delete(requestId);
|
|
3886
|
+
reject(new Error(`Blob chunk request for ${hash} timed out after ${timeoutMs}ms`));
|
|
3887
|
+
}, timeoutMs);
|
|
3888
|
+
pending.set(requestId, { resolve, reject, timer });
|
|
3889
|
+
port.send({ type: "blob-chunk-request", requestId, hash });
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
function serveBlobChunks(port, blobStore) {
|
|
3895
|
+
port.onMessage((message) => {
|
|
3896
|
+
if (message.type !== "blob-chunk-request") {
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
void blobStore.get(message.hash).then(
|
|
3900
|
+
(bytes) => {
|
|
3901
|
+
port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes });
|
|
3902
|
+
},
|
|
3903
|
+
() => {
|
|
3904
|
+
port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes: null });
|
|
3905
|
+
}
|
|
3906
|
+
);
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
function createChunkPortPair() {
|
|
3910
|
+
const handlersA = [];
|
|
3911
|
+
const handlersB = [];
|
|
3912
|
+
const a = {
|
|
3913
|
+
send(message) {
|
|
3914
|
+
queueMicrotask(() => {
|
|
3915
|
+
for (const handler of handlersB) {
|
|
3916
|
+
handler(message);
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
},
|
|
3920
|
+
onMessage(handler) {
|
|
3921
|
+
handlersA.push(handler);
|
|
3922
|
+
}
|
|
3923
|
+
};
|
|
3924
|
+
const b = {
|
|
3925
|
+
send(message) {
|
|
3926
|
+
queueMicrotask(() => {
|
|
3927
|
+
for (const handler of handlersA) {
|
|
3928
|
+
handler(message);
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
},
|
|
3932
|
+
onMessage(handler) {
|
|
3933
|
+
handlersB.push(handler);
|
|
3934
|
+
}
|
|
3935
|
+
};
|
|
3936
|
+
return { a, b };
|
|
3937
|
+
}
|
|
3047
3938
|
export {
|
|
3048
3939
|
AdapterError,
|
|
3940
|
+
BlobIntegrityError,
|
|
3049
3941
|
COMPACTION_BASELINE_META_KEY,
|
|
3050
3942
|
Collection,
|
|
3943
|
+
DEFAULT_CHUNK_SIZE,
|
|
3051
3944
|
InvalidStateTransitionError,
|
|
3052
3945
|
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
3946
|
+
MemoryBlobStore,
|
|
3947
|
+
OpfsBlobStore,
|
|
3948
|
+
OptimisticLockError,
|
|
3053
3949
|
PersistenceError,
|
|
3054
3950
|
QueryBuilder,
|
|
3055
3951
|
QueryError,
|
|
@@ -3066,26 +3962,44 @@ export {
|
|
|
3066
3962
|
WorkerTimeoutError,
|
|
3067
3963
|
asRichTextSyncEngine,
|
|
3068
3964
|
assertQueryReady,
|
|
3965
|
+
chunkBlob,
|
|
3966
|
+
collectBlobGarbage,
|
|
3069
3967
|
collectOperationsAheadOfServer,
|
|
3070
3968
|
compactOperationLog,
|
|
3071
3969
|
computeAckCompactionWatermark,
|
|
3970
|
+
createChunkPortPair,
|
|
3971
|
+
createMemoryServerBlobStore,
|
|
3972
|
+
createOpfsBlobDirectory,
|
|
3973
|
+
createOpfsBlobStore,
|
|
3974
|
+
createRemoteChunkProvider,
|
|
3072
3975
|
createRichTextController,
|
|
3073
3976
|
decodeAuditExport,
|
|
3074
3977
|
decodeRichtext,
|
|
3075
3978
|
deserializeVersionVectorFromMeta,
|
|
3076
3979
|
encodeRichtext,
|
|
3077
3980
|
exportBackup,
|
|
3981
|
+
extractBlobRefs,
|
|
3982
|
+
fetchBlobManifest,
|
|
3078
3983
|
getSharedQueryStoreCache,
|
|
3079
3984
|
mergeVersionVectors,
|
|
3985
|
+
parseBlobManifest,
|
|
3080
3986
|
persistedAuditTraceFromEvent,
|
|
3081
3987
|
pluralize,
|
|
3988
|
+
prepareBlobForSend,
|
|
3989
|
+
putBlobForTransfer,
|
|
3082
3990
|
readAuditExportManifest,
|
|
3083
3991
|
readBackupManifest,
|
|
3992
|
+
reassembleBlob,
|
|
3993
|
+
receiveBlob,
|
|
3994
|
+
resolveBlobManifest,
|
|
3084
3995
|
restoreBackup,
|
|
3085
3996
|
richtextStatesEqual,
|
|
3086
3997
|
richtextToPlainText,
|
|
3998
|
+
serializeBlobManifest,
|
|
3087
3999
|
serializeVersionVectorToMeta,
|
|
4000
|
+
serveBlobChunks,
|
|
3088
4001
|
singularize,
|
|
4002
|
+
toServerBlobCallbacks,
|
|
3089
4003
|
validateStateTransition,
|
|
3090
4004
|
validateUpdateStateMachine,
|
|
3091
4005
|
verifyAuditExportChecksum,
|