@korajs/store 0.6.0 → 1.0.0-beta.0
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-5XZ7THNJ.js} +3 -3
- package/dist/{chunk-EZSTD2XC.js → chunk-5R4QMY3Z.js} +2 -2
- package/dist/{chunk-AGN3CIPB.js → chunk-EIVXWF4B.js} +2 -2
- package/dist/{chunk-56LL4XP6.js → chunk-ETDJEH2I.js} +42 -1
- package/dist/chunk-ETDJEH2I.js.map +1 -0
- package/dist/{chunk-XO4LMPAH.js → chunk-IOEFNHHP.js} +2 -35
- package/dist/chunk-IOEFNHHP.js.map +1 -0
- package/dist/{chunk-7BPETY5Y.js → chunk-OUQDC5Z6.js} +105 -8
- package/dist/chunk-OUQDC5Z6.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/{export-audit-5GV6VRDZ.js → export-audit-VPGQWNR6.js} +3 -3
- package/dist/index.cjs +615 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -7
- package/dist/index.d.ts +77 -7
- package/dist/index.js +423 -44
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +100 -30
- 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-DepnRDk_.d.cts → types-CJOVKxLw.d.cts} +43 -2
- package/dist/{types-DepnRDk_.d.ts → types-CJOVKxLw.d.ts} +43 -2
- package/dist/{types-CTr00g_d.d.cts → types-CyjXwrjW.d.cts} +1 -1
- package/dist/{types-BMyHmwvn.d.ts → types-DAxCladl.d.ts} +1 -1
- package/package.json +4 -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-5XZ7THNJ.js.map} +0 -0
- /package/dist/{chunk-EZSTD2XC.js.map → chunk-5R4QMY3Z.js.map} +0 -0
- /package/dist/{chunk-AGN3CIPB.js.map → chunk-EIVXWF4B.js.map} +0 -0
- /package/dist/{chunk-6AHYGQQT.js.map → chunk-RBPVEAVM.js.map} +0 -0
- /package/dist/{export-audit-5GV6VRDZ.js.map → export-audit-VPGQWNR6.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
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
decodeAuditExport,
|
|
3
3
|
readAuditExportManifest,
|
|
4
4
|
verifyAuditExportChecksum
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EIVXWF4B.js";
|
|
6
6
|
import {
|
|
7
7
|
persistedAuditTraceFromEvent
|
|
8
8
|
} from "./chunk-MYSJN3DF.js";
|
|
@@ -11,46 +11,53 @@ import {
|
|
|
11
11
|
allocateNextSequenceInTransaction,
|
|
12
12
|
allocateNextSequenceNumber,
|
|
13
13
|
buildCountQuery,
|
|
14
|
+
buildFieldFastForwardUpdateQuery,
|
|
14
15
|
buildInsertQuery,
|
|
15
16
|
buildLwwSoftDeleteQuery,
|
|
16
|
-
buildLwwUpdateQuery,
|
|
17
17
|
buildSelectQuery,
|
|
18
18
|
buildSoftDeleteQuery,
|
|
19
19
|
buildUpdateQuery,
|
|
20
20
|
executeDelete,
|
|
21
21
|
executeInsert,
|
|
22
22
|
executeUpdate,
|
|
23
|
+
fieldVersionsForFields,
|
|
23
24
|
isIncomingNewerThanRow,
|
|
25
|
+
parseFieldVersions,
|
|
24
26
|
readSequenceNumber,
|
|
27
|
+
resolvePerFieldLww,
|
|
28
|
+
serializeFieldVersions,
|
|
25
29
|
serializeRowVersion,
|
|
26
30
|
validateStateTransition,
|
|
27
31
|
validateUpdateStateMachine
|
|
28
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-OUQDC5Z6.js";
|
|
29
33
|
import {
|
|
30
34
|
AdapterError,
|
|
35
|
+
OptimisticLockError,
|
|
31
36
|
PersistenceError,
|
|
32
37
|
QueryError,
|
|
33
38
|
RecordNotFoundError,
|
|
34
39
|
StoreNotOpenError,
|
|
35
40
|
WorkerInitError,
|
|
36
41
|
WorkerTimeoutError
|
|
37
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-QL2XVZRN.js";
|
|
38
43
|
import {
|
|
39
44
|
exportBackup,
|
|
40
45
|
readBackupManifest,
|
|
41
46
|
restoreBackup,
|
|
42
47
|
verifyBackupChecksum
|
|
43
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-5R4QMY3Z.js";
|
|
44
49
|
import {
|
|
45
50
|
decodeRichtext,
|
|
51
|
+
decodeRichtextFieldsFromOpData,
|
|
46
52
|
deserializeOperationWithCollection,
|
|
47
53
|
deserializeRecord,
|
|
48
54
|
encodeRichtext,
|
|
55
|
+
encodeRichtextFieldsForOpData,
|
|
49
56
|
richtextStatesEqual,
|
|
50
57
|
richtextToPlainText,
|
|
51
58
|
serializeOperation,
|
|
52
59
|
serializeRecord
|
|
53
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-ETDJEH2I.js";
|
|
54
61
|
|
|
55
62
|
// src/compaction/types.ts
|
|
56
63
|
var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
|
|
@@ -195,7 +202,7 @@ async function saveDeltaCursor(adapter, cursor) {
|
|
|
195
202
|
// src/store/store.ts
|
|
196
203
|
import {
|
|
197
204
|
CausalTracker,
|
|
198
|
-
HybridLogicalClock as
|
|
205
|
+
HybridLogicalClock as HybridLogicalClock3,
|
|
199
206
|
createVersionVector as createVersionVector3,
|
|
200
207
|
generateUUIDv7 as generateUUIDv73,
|
|
201
208
|
migrationStepsToSQL
|
|
@@ -885,7 +892,9 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
885
892
|
}
|
|
886
893
|
colMap.set(op.recordId, {
|
|
887
894
|
id: op.recordId,
|
|
888
|
-
|
|
895
|
+
// op.data stores binary richtext as tagged JSON; snapshots must
|
|
896
|
+
// expose record-shaped values (Uint8Array/string).
|
|
897
|
+
fields: decodeRichtextFieldsFromOpData(op.data, definition.fields),
|
|
889
898
|
deleted: false,
|
|
890
899
|
createdAt: wallTime,
|
|
891
900
|
updatedAt: wallTime
|
|
@@ -896,9 +905,10 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
896
905
|
if (!op.data) {
|
|
897
906
|
return;
|
|
898
907
|
}
|
|
908
|
+
const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
|
|
899
909
|
const existing = colMap.get(op.recordId);
|
|
900
910
|
if (existing && !existing.deleted) {
|
|
901
|
-
existing.fields = { ...existing.fields, ...
|
|
911
|
+
existing.fields = { ...existing.fields, ...decodedData };
|
|
902
912
|
existing.updatedAt = wallTime;
|
|
903
913
|
break;
|
|
904
914
|
}
|
|
@@ -907,7 +917,7 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
907
917
|
}
|
|
908
918
|
colMap.set(op.recordId, {
|
|
909
919
|
id: op.recordId,
|
|
910
|
-
fields:
|
|
920
|
+
fields: decodedData,
|
|
911
921
|
deleted: false,
|
|
912
922
|
createdAt: wallTime,
|
|
913
923
|
updatedAt: wallTime
|
|
@@ -1458,6 +1468,137 @@ var SubscriptionManager = class {
|
|
|
1458
1468
|
}
|
|
1459
1469
|
};
|
|
1460
1470
|
|
|
1471
|
+
// src/sync/rebase-unsynced-operations.ts
|
|
1472
|
+
import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL } from "@korajs/core";
|
|
1473
|
+
import { computeOperationId } from "@korajs/core/internal";
|
|
1474
|
+
var LOGICAL_COUNTER_LIMIT = MAX_LOGICAL + 1;
|
|
1475
|
+
function emptyResult() {
|
|
1476
|
+
return { operations: [], idMapping: {}, rebasedCount: 0, newMaxTimestamp: null };
|
|
1477
|
+
}
|
|
1478
|
+
async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, correctedNowMs) {
|
|
1479
|
+
if (unsyncedOpIds.length === 0) {
|
|
1480
|
+
return emptyResult();
|
|
1481
|
+
}
|
|
1482
|
+
const rebaseIds = new Set(unsyncedOpIds);
|
|
1483
|
+
const rebased = [];
|
|
1484
|
+
let maxOtherSerializedTs = null;
|
|
1485
|
+
for (const collectionName of Object.keys(schema.collections)) {
|
|
1486
|
+
const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
|
|
1487
|
+
for (const row of rows) {
|
|
1488
|
+
if (rebaseIds.has(row.id)) {
|
|
1489
|
+
rebased.push(deserializeOperationWithCollection(row, collectionName));
|
|
1490
|
+
} else if (maxOtherSerializedTs === null || row.timestamp > maxOtherSerializedTs) {
|
|
1491
|
+
maxOtherSerializedTs = row.timestamp;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (rebased.length === 0) {
|
|
1496
|
+
return emptyResult();
|
|
1497
|
+
}
|
|
1498
|
+
const maxOtherWallTime = maxOtherSerializedTs === null ? Number.NEGATIVE_INFINITY : HybridLogicalClock2.deserialize(maxOtherSerializedTs).wallTime;
|
|
1499
|
+
const baseWallTime = Math.max(correctedNowMs, maxOtherWallTime + 1);
|
|
1500
|
+
rebased.sort((a, b) => HybridLogicalClock2.compare(a.timestamp, b.timestamp));
|
|
1501
|
+
const newTimestamps = rebased.map((op, i) => ({
|
|
1502
|
+
// Spill into wallTime past the 5-digit logical serialization limit so the
|
|
1503
|
+
// serialized order stays correct even for enormous offline queues.
|
|
1504
|
+
wallTime: baseWallTime + Math.floor(i / LOGICAL_COUNTER_LIMIT),
|
|
1505
|
+
logical: i % LOGICAL_COUNTER_LIMIT,
|
|
1506
|
+
nodeId: op.nodeId
|
|
1507
|
+
}));
|
|
1508
|
+
const idMapping = {};
|
|
1509
|
+
const newIds = [];
|
|
1510
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
1511
|
+
const op = rebased[i];
|
|
1512
|
+
const ts = newTimestamps[i];
|
|
1513
|
+
if (!op || !ts) {
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
const input = {
|
|
1517
|
+
nodeId: op.nodeId,
|
|
1518
|
+
type: op.type,
|
|
1519
|
+
collection: op.collection,
|
|
1520
|
+
recordId: op.recordId,
|
|
1521
|
+
data: op.data,
|
|
1522
|
+
previousData: op.previousData,
|
|
1523
|
+
sequenceNumber: op.sequenceNumber,
|
|
1524
|
+
causalDeps: op.causalDeps,
|
|
1525
|
+
schemaVersion: op.schemaVersion,
|
|
1526
|
+
...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
|
|
1527
|
+
};
|
|
1528
|
+
const newId = await computeOperationId(input, HybridLogicalClock2.serialize(ts));
|
|
1529
|
+
idMapping[op.id] = newId;
|
|
1530
|
+
newIds.push(newId);
|
|
1531
|
+
}
|
|
1532
|
+
const newOperations = [];
|
|
1533
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
1534
|
+
const op = rebased[i];
|
|
1535
|
+
const ts = newTimestamps[i];
|
|
1536
|
+
const newId = newIds[i];
|
|
1537
|
+
if (!op || !ts || newId === void 0) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
newOperations.push({
|
|
1541
|
+
...op,
|
|
1542
|
+
id: newId,
|
|
1543
|
+
timestamp: ts,
|
|
1544
|
+
causalDeps: op.causalDeps.map((dep) => idMapping[dep] ?? dep)
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
await adapter.transaction(async (tx) => {
|
|
1548
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
1549
|
+
const oldOp = rebased[i];
|
|
1550
|
+
const newOp = newOperations[i];
|
|
1551
|
+
if (!oldOp || !newOp) {
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
const opsTable = `_kora_ops_${oldOp.collection}`;
|
|
1555
|
+
await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
|
|
1556
|
+
const opInsert = buildInsertQuery(
|
|
1557
|
+
opsTable,
|
|
1558
|
+
serializeOperation(newOp)
|
|
1559
|
+
);
|
|
1560
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1561
|
+
const oldVersion = serializeRowVersion(oldOp.timestamp);
|
|
1562
|
+
const newVersion = serializeRowVersion(newOp.timestamp);
|
|
1563
|
+
await tx.execute(
|
|
1564
|
+
`UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
|
|
1565
|
+
[newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
|
|
1566
|
+
);
|
|
1567
|
+
const rows = await tx.query(
|
|
1568
|
+
`SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
|
|
1569
|
+
[oldOp.recordId]
|
|
1570
|
+
);
|
|
1571
|
+
const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
|
|
1572
|
+
let changed = false;
|
|
1573
|
+
for (const [field, version] of Object.entries(fieldVersions)) {
|
|
1574
|
+
if (version === oldVersion) {
|
|
1575
|
+
fieldVersions[field] = newVersion;
|
|
1576
|
+
changed = true;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (changed) {
|
|
1580
|
+
await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
|
|
1581
|
+
serializeFieldVersions(fieldVersions),
|
|
1582
|
+
oldOp.recordId
|
|
1583
|
+
]);
|
|
1584
|
+
}
|
|
1585
|
+
if (oldOp.type === "insert") {
|
|
1586
|
+
await tx.execute(
|
|
1587
|
+
`UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
|
|
1588
|
+
[newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
const newMaxTimestamp = newTimestamps[newTimestamps.length - 1] ?? null;
|
|
1594
|
+
return {
|
|
1595
|
+
operations: newOperations,
|
|
1596
|
+
idMapping,
|
|
1597
|
+
rebasedCount: newOperations.length,
|
|
1598
|
+
newMaxTimestamp
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1461
1602
|
// src/transaction/transaction-context.ts
|
|
1462
1603
|
import {
|
|
1463
1604
|
createOperation as createOperation2,
|
|
@@ -1607,7 +1748,9 @@ var TransactionContext = class {
|
|
|
1607
1748
|
type: "insert",
|
|
1608
1749
|
collection: collectionName,
|
|
1609
1750
|
recordId,
|
|
1610
|
-
|
|
1751
|
+
// Binary richtext values are tagged as canonical JSON before the
|
|
1752
|
+
// operation is content-hashed (see encodeRichtextFieldsForOpData).
|
|
1753
|
+
data: encodeRichtextFieldsForOpData(validated, definition.fields),
|
|
1611
1754
|
previousData: null,
|
|
1612
1755
|
sequenceNumber,
|
|
1613
1756
|
causalDeps,
|
|
@@ -1678,8 +1821,10 @@ var TransactionContext = class {
|
|
|
1678
1821
|
type: "update",
|
|
1679
1822
|
collection: collectionName,
|
|
1680
1823
|
recordId: id,
|
|
1681
|
-
|
|
1682
|
-
|
|
1824
|
+
// Binary richtext values (new and previous) are tagged as canonical
|
|
1825
|
+
// JSON before the operation is content-hashed.
|
|
1826
|
+
data: encodeRichtextFieldsForOpData(resolvedData, definition.fields),
|
|
1827
|
+
previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
|
|
1683
1828
|
sequenceNumber,
|
|
1684
1829
|
causalDeps,
|
|
1685
1830
|
schemaVersion: this.config.schema.version,
|
|
@@ -1786,14 +1931,16 @@ var TransactionContext = class {
|
|
|
1786
1931
|
if (entry.operation.type === "insert" && entry.operation.data) {
|
|
1787
1932
|
record = {
|
|
1788
1933
|
id,
|
|
1789
|
-
|
|
1934
|
+
// op.data stores binary richtext as tagged JSON; the effective
|
|
1935
|
+
// record must expose record-shaped values (Uint8Array/string).
|
|
1936
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
1790
1937
|
createdAt: entry.operation.timestamp.wallTime,
|
|
1791
1938
|
updatedAt: entry.operation.timestamp.wallTime
|
|
1792
1939
|
};
|
|
1793
1940
|
} else if (entry.operation.type === "update" && entry.operation.data && record) {
|
|
1794
1941
|
record = {
|
|
1795
1942
|
...record,
|
|
1796
|
-
...entry.operation.data,
|
|
1943
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
1797
1944
|
updatedAt: entry.operation.timestamp.wallTime
|
|
1798
1945
|
};
|
|
1799
1946
|
} else if (entry.operation.type === "delete") {
|
|
@@ -1893,7 +2040,7 @@ var Store = class {
|
|
|
1893
2040
|
await this.adapter.open(this.schema);
|
|
1894
2041
|
await this.runMigrationsIfNeeded();
|
|
1895
2042
|
this.nodeId = await this.loadOrGenerateNodeId();
|
|
1896
|
-
this.clock = new
|
|
2043
|
+
this.clock = new HybridLogicalClock3(this.nodeId);
|
|
1897
2044
|
this.causalTracker = new CausalTracker();
|
|
1898
2045
|
this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
|
|
1899
2046
|
this.sequenceNumber = await this.loadSequenceNumber();
|
|
@@ -1980,6 +2127,14 @@ var Store = class {
|
|
|
1980
2127
|
/**
|
|
1981
2128
|
* Get the node ID for this store instance.
|
|
1982
2129
|
*/
|
|
2130
|
+
/**
|
|
2131
|
+
* Records the offset between server time and this device's clock
|
|
2132
|
+
* (serverTime - localTime), learned at sync handshake. Lets the store's HLC
|
|
2133
|
+
* validate remote timestamps correctly even when the local clock is wrong.
|
|
2134
|
+
*/
|
|
2135
|
+
setClockReferenceOffset(offsetMs) {
|
|
2136
|
+
this.clock?.setReferenceOffset(offsetMs);
|
|
2137
|
+
}
|
|
1983
2138
|
getNodeId() {
|
|
1984
2139
|
this.ensureOpen();
|
|
1985
2140
|
return this.nodeId;
|
|
@@ -2006,45 +2161,158 @@ var Store = class {
|
|
|
2006
2161
|
if (this.clock) {
|
|
2007
2162
|
this.clock.receive(op.timestamp);
|
|
2008
2163
|
}
|
|
2009
|
-
const
|
|
2010
|
-
const
|
|
2164
|
+
const materializeTimestamp = options?.materializeTimestamp ?? op.timestamp;
|
|
2165
|
+
const remoteVersion = serializeRowVersion(materializeTimestamp);
|
|
2166
|
+
const wallTime = materializeTimestamp.wallTime;
|
|
2167
|
+
const materializeSource = options?.materializeData ?? op.data;
|
|
2011
2168
|
await this.adapter.transaction(async (tx) => {
|
|
2012
|
-
|
|
2169
|
+
const checkGuard = (row) => {
|
|
2170
|
+
const guard = options?.guardRowState;
|
|
2171
|
+
if (!guard) {
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const version = typeof row?._version === "string" ? row._version : null;
|
|
2175
|
+
const fieldVersions = typeof row?._field_versions === "string" ? row._field_versions : null;
|
|
2176
|
+
if (version !== guard.version || fieldVersions !== guard.fieldVersions) {
|
|
2177
|
+
throw new OptimisticLockError(collection, op.recordId);
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
if (op.type === "insert" && materializeSource) {
|
|
2181
|
+
const serializedData = serializeRecord(materializeSource, definition.fields);
|
|
2013
2182
|
const existing2 = await tx.query(
|
|
2014
|
-
`SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
|
|
2183
|
+
`SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
|
|
2015
2184
|
[op.recordId]
|
|
2016
2185
|
);
|
|
2017
2186
|
const row = existing2[0];
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
const serializedData = serializeRecord(op.data, definition.fields);
|
|
2187
|
+
checkGuard(row);
|
|
2188
|
+
if (!row) {
|
|
2021
2189
|
const record = {
|
|
2022
2190
|
id: op.recordId,
|
|
2023
2191
|
...serializedData,
|
|
2024
2192
|
_created_at: wallTime,
|
|
2025
2193
|
_updated_at: wallTime,
|
|
2026
|
-
_version: remoteVersion
|
|
2194
|
+
_version: remoteVersion,
|
|
2195
|
+
// Stamp every inserted field so per-field LWW has a real baseline.
|
|
2196
|
+
_field_versions: serializeFieldVersions(
|
|
2197
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
2198
|
+
)
|
|
2027
2199
|
};
|
|
2028
2200
|
const insertQuery = buildInsertQuery(collection, record);
|
|
2029
2201
|
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
2202
|
+
await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
|
|
2203
|
+
} else if (row._deleted === 1) {
|
|
2204
|
+
if (isIncomingNewerThanRow(materializeTimestamp, row)) {
|
|
2205
|
+
const fieldChanges = {
|
|
2206
|
+
...serializedData,
|
|
2207
|
+
_deleted: 0,
|
|
2208
|
+
_field_versions: serializeFieldVersions(
|
|
2209
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
2210
|
+
)
|
|
2211
|
+
};
|
|
2212
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
2213
|
+
collection,
|
|
2214
|
+
op.recordId,
|
|
2215
|
+
fieldChanges,
|
|
2216
|
+
remoteVersion,
|
|
2217
|
+
wallTime,
|
|
2218
|
+
{ maxCreatedAt: wallTime }
|
|
2219
|
+
);
|
|
2220
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
2221
|
+
}
|
|
2222
|
+
} else {
|
|
2223
|
+
const currentVersions = parseFieldVersions(row._field_versions);
|
|
2224
|
+
const rowVersion = typeof row._version === "string" ? row._version : void 0;
|
|
2225
|
+
const fieldChanges = {};
|
|
2226
|
+
let fieldVersionsJson;
|
|
2227
|
+
if (options?.forceMaterialize) {
|
|
2228
|
+
const merged = { ...currentVersions };
|
|
2229
|
+
for (const field of Object.keys(serializedData)) {
|
|
2230
|
+
fieldChanges[field] = serializedData[field];
|
|
2231
|
+
merged[field] = remoteVersion;
|
|
2232
|
+
}
|
|
2233
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
2234
|
+
} else {
|
|
2235
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
2236
|
+
currentVersions,
|
|
2237
|
+
Object.keys(serializedData),
|
|
2238
|
+
remoteVersion,
|
|
2239
|
+
rowVersion
|
|
2240
|
+
);
|
|
2241
|
+
for (const field of winners) {
|
|
2242
|
+
fieldChanges[field] = serializedData[field];
|
|
2243
|
+
}
|
|
2244
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
2245
|
+
}
|
|
2246
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
2247
|
+
collection,
|
|
2248
|
+
op.recordId,
|
|
2249
|
+
{ ...fieldChanges, _field_versions: fieldVersionsJson },
|
|
2250
|
+
remoteVersion,
|
|
2251
|
+
wallTime,
|
|
2252
|
+
{ maxCreatedAt: wallTime }
|
|
2253
|
+
);
|
|
2254
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
2030
2255
|
}
|
|
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
|
|
2256
|
+
} else if (op.type === "update" && materializeSource) {
|
|
2257
|
+
const serializedChanges = serializeRecord(materializeSource, definition.fields);
|
|
2258
|
+
const changedFields = Object.keys(serializedChanges);
|
|
2259
|
+
const currentRows = await tx.query(
|
|
2260
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2261
|
+
[op.recordId]
|
|
2046
2262
|
);
|
|
2047
|
-
|
|
2263
|
+
const currentRow = currentRows[0];
|
|
2264
|
+
checkGuard(currentRow);
|
|
2265
|
+
if (currentRow) {
|
|
2266
|
+
const currentVersions = parseFieldVersions(currentRow._field_versions);
|
|
2267
|
+
const rowVersion = typeof currentRow._version === "string" ? currentRow._version : void 0;
|
|
2268
|
+
if (options?.forceMaterialize) {
|
|
2269
|
+
const merged = { ...currentVersions };
|
|
2270
|
+
for (const field of changedFields) {
|
|
2271
|
+
merged[field] = remoteVersion;
|
|
2272
|
+
}
|
|
2273
|
+
const fieldChanges = {
|
|
2274
|
+
...serializedChanges,
|
|
2275
|
+
_field_versions: serializeFieldVersions(merged)
|
|
2276
|
+
};
|
|
2277
|
+
if (options?.reactivateIfDeleted) {
|
|
2278
|
+
fieldChanges._deleted = 0;
|
|
2279
|
+
}
|
|
2280
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
2281
|
+
collection,
|
|
2282
|
+
op.recordId,
|
|
2283
|
+
fieldChanges,
|
|
2284
|
+
remoteVersion,
|
|
2285
|
+
wallTime
|
|
2286
|
+
);
|
|
2287
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
2288
|
+
} else {
|
|
2289
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
2290
|
+
currentVersions,
|
|
2291
|
+
changedFields,
|
|
2292
|
+
remoteVersion,
|
|
2293
|
+
rowVersion
|
|
2294
|
+
);
|
|
2295
|
+
if (winners.length > 0 || options?.reactivateIfDeleted) {
|
|
2296
|
+
const winningChanges = {
|
|
2297
|
+
_field_versions: serializeFieldVersions(merged)
|
|
2298
|
+
};
|
|
2299
|
+
for (const field of winners) {
|
|
2300
|
+
winningChanges[field] = serializedChanges[field];
|
|
2301
|
+
}
|
|
2302
|
+
if (options?.reactivateIfDeleted) {
|
|
2303
|
+
winningChanges._deleted = 0;
|
|
2304
|
+
}
|
|
2305
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
2306
|
+
collection,
|
|
2307
|
+
op.recordId,
|
|
2308
|
+
winningChanges,
|
|
2309
|
+
remoteVersion,
|
|
2310
|
+
wallTime
|
|
2311
|
+
);
|
|
2312
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2048
2316
|
} else if (op.type === "delete") {
|
|
2049
2317
|
const deleteQuery = buildLwwSoftDeleteQuery(
|
|
2050
2318
|
collection,
|
|
@@ -2072,6 +2340,67 @@ var Store = class {
|
|
|
2072
2340
|
this.subscriptionManager.notify(collection, op);
|
|
2073
2341
|
return "applied";
|
|
2074
2342
|
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Materialize update/delete operations that were logged for a record BEFORE
|
|
2345
|
+
* its insert arrived (reordered delivery). Runs inside the insert's write
|
|
2346
|
+
* transaction, folding each orphan in timestamp order through the exact
|
|
2347
|
+
* per-field LWW / LWW-delete rules the normal apply paths use, so the final
|
|
2348
|
+
* row state equals what a device that received the operations in causal
|
|
2349
|
+
* order computed. Called only when the insert created a previously-absent
|
|
2350
|
+
* row — in that case every logged update/delete for the record is
|
|
2351
|
+
* necessarily an orphan (rows are soft-deleted, never removed, so a row
|
|
2352
|
+
* that is absent now was never materialized).
|
|
2353
|
+
*/
|
|
2354
|
+
async foldOrphanedOperations(tx, collection, definition, recordId) {
|
|
2355
|
+
const orphanRows = await tx.query(
|
|
2356
|
+
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
|
|
2357
|
+
[recordId]
|
|
2358
|
+
);
|
|
2359
|
+
if (orphanRows.length === 0) {
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const orphans = orphanRows.map((r) => deserializeOperationWithCollection(r, collection)).sort((a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp));
|
|
2363
|
+
for (const orphan of orphans) {
|
|
2364
|
+
const orphanVersion = serializeRowVersion(orphan.timestamp);
|
|
2365
|
+
const orphanWall = orphan.timestamp.wallTime;
|
|
2366
|
+
if (orphan.type === "update" && orphan.data) {
|
|
2367
|
+
const orphanChanges = serializeRecord(orphan.data, definition.fields);
|
|
2368
|
+
const rows = await tx.query(
|
|
2369
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2370
|
+
[recordId]
|
|
2371
|
+
);
|
|
2372
|
+
const current = rows[0];
|
|
2373
|
+
if (!current) {
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
2377
|
+
parseFieldVersions(current._field_versions),
|
|
2378
|
+
Object.keys(orphanChanges),
|
|
2379
|
+
orphanVersion,
|
|
2380
|
+
typeof current._version === "string" ? current._version : void 0
|
|
2381
|
+
);
|
|
2382
|
+
if (winners.length > 0) {
|
|
2383
|
+
const winningChanges = {
|
|
2384
|
+
_field_versions: serializeFieldVersions(merged)
|
|
2385
|
+
};
|
|
2386
|
+
for (const field of winners) {
|
|
2387
|
+
winningChanges[field] = orphanChanges[field];
|
|
2388
|
+
}
|
|
2389
|
+
const query = buildFieldFastForwardUpdateQuery(
|
|
2390
|
+
collection,
|
|
2391
|
+
recordId,
|
|
2392
|
+
winningChanges,
|
|
2393
|
+
orphanVersion,
|
|
2394
|
+
orphanWall
|
|
2395
|
+
);
|
|
2396
|
+
await tx.execute(query.sql, query.params);
|
|
2397
|
+
}
|
|
2398
|
+
} else if (orphan.type === "delete") {
|
|
2399
|
+
const query = buildLwwSoftDeleteQuery(collection, recordId, orphanWall, orphanVersion);
|
|
2400
|
+
await tx.execute(query.sql, query.params);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2075
2404
|
/**
|
|
2076
2405
|
* Get operations from a node within a sequence number range.
|
|
2077
2406
|
* Implements the OperationLog interface for computeDelta.
|
|
@@ -2157,7 +2486,7 @@ var Store = class {
|
|
|
2157
2486
|
*/
|
|
2158
2487
|
async exportAudit(options) {
|
|
2159
2488
|
this.ensureOpen();
|
|
2160
|
-
const { exportAudit: doExport } = await import("./export-audit-
|
|
2489
|
+
const { exportAudit: doExport } = await import("./export-audit-VPGQWNR6.js");
|
|
2161
2490
|
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
2162
2491
|
}
|
|
2163
2492
|
/**
|
|
@@ -2227,6 +2556,31 @@ var Store = class {
|
|
|
2227
2556
|
deleted: row._deleted === 1
|
|
2228
2557
|
};
|
|
2229
2558
|
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Raw version state of a materialized row, for optimistic-concurrency guarded
|
|
2561
|
+
* applies: capture this snapshot before computing a merge, pass it as
|
|
2562
|
+
* `guardRowState`, and the apply refuses to write if the row changed since.
|
|
2563
|
+
* Returns null when no row exists (which is itself a valid guard state — the
|
|
2564
|
+
* apply then requires the row to still be absent).
|
|
2565
|
+
*/
|
|
2566
|
+
async getRowVersionState(collection, recordId) {
|
|
2567
|
+
this.ensureOpen();
|
|
2568
|
+
if (!this.schema.collections[collection]) {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
const rows = await this.adapter.query(
|
|
2572
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
2573
|
+
[recordId]
|
|
2574
|
+
);
|
|
2575
|
+
const row = rows[0];
|
|
2576
|
+
if (!row) {
|
|
2577
|
+
return null;
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
version: typeof row._version === "string" ? row._version : null,
|
|
2581
|
+
fieldVersions: typeof row._field_versions === "string" ? row._field_versions : null
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2230
2584
|
/**
|
|
2231
2585
|
* Latest operation from this device for a record (used for delete-vs-update merge on sync).
|
|
2232
2586
|
*/
|
|
@@ -2269,6 +2623,30 @@ var Store = class {
|
|
|
2269
2623
|
(nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
|
|
2270
2624
|
);
|
|
2271
2625
|
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Re-stamp never-acknowledged local operations after a fast device clock was
|
|
2628
|
+
* corrected, so sync can resume immediately. Safe because unacknowledged
|
|
2629
|
+
* operations are private to this device (like unpushed git commits);
|
|
2630
|
+
* acknowledged/shared operations are immutable and never touched.
|
|
2631
|
+
*
|
|
2632
|
+
* After the rewrite the store's HLC is advanced past the highest new
|
|
2633
|
+
* timestamp so subsequent local writes keep sorting after the rebased ops.
|
|
2634
|
+
* No data-change notifications are emitted: materialized values are
|
|
2635
|
+
* unchanged, only version stamps move.
|
|
2636
|
+
*/
|
|
2637
|
+
async rebaseUnsyncedOperations(unsyncedOpIds, correctedNowMs) {
|
|
2638
|
+
this.ensureOpen();
|
|
2639
|
+
const result = await rebaseUnsyncedOperationsInLog(
|
|
2640
|
+
this.adapter,
|
|
2641
|
+
this.schema,
|
|
2642
|
+
unsyncedOpIds,
|
|
2643
|
+
correctedNowMs
|
|
2644
|
+
);
|
|
2645
|
+
if (result.newMaxTimestamp && this.clock) {
|
|
2646
|
+
this.clock.advanceTo(result.newMaxTimestamp);
|
|
2647
|
+
}
|
|
2648
|
+
return result;
|
|
2649
|
+
}
|
|
2272
2650
|
/**
|
|
2273
2651
|
* Count of local operations ahead of the server version vector.
|
|
2274
2652
|
*/
|
|
@@ -2318,7 +2696,7 @@ var Store = class {
|
|
|
2318
2696
|
let latest = null;
|
|
2319
2697
|
for (const row of rows) {
|
|
2320
2698
|
const op = deserializeOperationWithCollection(row, collection);
|
|
2321
|
-
if (!latest ||
|
|
2699
|
+
if (!latest || HybridLogicalClock3.compare(op.timestamp, latest.timestamp) > 0) {
|
|
2322
2700
|
latest = op;
|
|
2323
2701
|
}
|
|
2324
2702
|
}
|
|
@@ -2402,7 +2780,7 @@ var Store = class {
|
|
|
2402
2780
|
*/
|
|
2403
2781
|
async exportBackup(options) {
|
|
2404
2782
|
this.ensureOpen();
|
|
2405
|
-
const { exportBackup: doExport } = await import("./backup-
|
|
2783
|
+
const { exportBackup: doExport } = await import("./backup-5XZ7THNJ.js");
|
|
2406
2784
|
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
2407
2785
|
}
|
|
2408
2786
|
/**
|
|
@@ -2414,7 +2792,7 @@ var Store = class {
|
|
|
2414
2792
|
*/
|
|
2415
2793
|
async importBackup(data, options) {
|
|
2416
2794
|
this.ensureOpen();
|
|
2417
|
-
const { restoreBackup: doRestore } = await import("./backup-
|
|
2795
|
+
const { restoreBackup: doRestore } = await import("./backup-5XZ7THNJ.js");
|
|
2418
2796
|
return doRestore(this.adapter, this.schema, data, options);
|
|
2419
2797
|
}
|
|
2420
2798
|
/**
|
|
@@ -3050,6 +3428,7 @@ export {
|
|
|
3050
3428
|
Collection,
|
|
3051
3429
|
InvalidStateTransitionError,
|
|
3052
3430
|
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
3431
|
+
OptimisticLockError,
|
|
3053
3432
|
PersistenceError,
|
|
3054
3433
|
QueryBuilder,
|
|
3055
3434
|
QueryError,
|