@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.cjs
CHANGED
|
@@ -30,6 +30,38 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/serialization/op-data-encoding.ts
|
|
34
|
+
function encodeRichtextFieldsForOpData(data, fields) {
|
|
35
|
+
const result = { ...data };
|
|
36
|
+
for (const [key, value] of Object.entries(data)) {
|
|
37
|
+
if (fields[key]?.kind !== "richtext") {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
41
|
+
result[key] = (0, import_core4.encodeBytesForOpData)(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function decodeRichtextFieldsFromOpData(data, fields) {
|
|
47
|
+
const result = { ...data };
|
|
48
|
+
for (const [key, value] of Object.entries(data)) {
|
|
49
|
+
if (fields[key]?.kind !== "richtext" || value === null || value === void 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
result[key] = (0, import_core4.decodeBytesFromOpData)(value);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
var import_core4, decodeRichtextOpDataValue;
|
|
57
|
+
var init_op_data_encoding = __esm({
|
|
58
|
+
"src/serialization/op-data-encoding.ts"() {
|
|
59
|
+
"use strict";
|
|
60
|
+
import_core4 = require("@korajs/core");
|
|
61
|
+
decodeRichtextOpDataValue = import_core4.decodeBytesFromOpData;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
33
65
|
// src/serialization/richtext-serializer.ts
|
|
34
66
|
function encodeRichtext(value) {
|
|
35
67
|
if (value === null || value === void 0) {
|
|
@@ -46,6 +78,10 @@ function encodeRichtext(value) {
|
|
|
46
78
|
if (value instanceof ArrayBuffer) {
|
|
47
79
|
return new Uint8Array(value);
|
|
48
80
|
}
|
|
81
|
+
if ((0, import_core4.isKoraBytesValue)(value) || (0, import_core4.isLegacyNumericByteObject)(value)) {
|
|
82
|
+
const decoded = decodeRichtextOpDataValue(value);
|
|
83
|
+
return decoded instanceof Uint8Array ? decoded : null;
|
|
84
|
+
}
|
|
49
85
|
throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
|
|
50
86
|
}
|
|
51
87
|
function decodeRichtext(value) {
|
|
@@ -84,6 +120,7 @@ var init_richtext_serializer = __esm({
|
|
|
84
120
|
"src/serialization/richtext-serializer.ts"() {
|
|
85
121
|
"use strict";
|
|
86
122
|
Y = __toESM(require("yjs"), 1);
|
|
123
|
+
init_op_data_encoding();
|
|
87
124
|
TEXT_KEY = "content";
|
|
88
125
|
}
|
|
89
126
|
});
|
|
@@ -147,7 +184,7 @@ function serializeOperation(op) {
|
|
|
147
184
|
record_id: op.recordId,
|
|
148
185
|
data: dataPayload ? JSON.stringify(dataPayload) : null,
|
|
149
186
|
previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
|
|
150
|
-
timestamp:
|
|
187
|
+
timestamp: import_core5.HybridLogicalClock.serialize(op.timestamp),
|
|
151
188
|
sequence_number: op.sequenceNumber,
|
|
152
189
|
causal_deps: JSON.stringify(op.causalDeps),
|
|
153
190
|
schema_version: op.schemaVersion
|
|
@@ -181,7 +218,7 @@ function deserializeOperation(row) {
|
|
|
181
218
|
recordId: row.record_id,
|
|
182
219
|
data,
|
|
183
220
|
previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
|
|
184
|
-
timestamp:
|
|
221
|
+
timestamp: import_core5.HybridLogicalClock.deserialize(row.timestamp),
|
|
185
222
|
sequenceNumber: row.sequence_number,
|
|
186
223
|
causalDeps: JSON.parse(row.causal_deps),
|
|
187
224
|
schemaVersion: row.schema_version,
|
|
@@ -224,11 +261,11 @@ function deserializeValue(value, descriptor) {
|
|
|
224
261
|
return value;
|
|
225
262
|
}
|
|
226
263
|
}
|
|
227
|
-
var
|
|
264
|
+
var import_core5, ATOMIC_OPS_KEY, TX_ID_KEY, MUTATION_NAME_KEY;
|
|
228
265
|
var init_serializer = __esm({
|
|
229
266
|
"src/serialization/serializer.ts"() {
|
|
230
267
|
"use strict";
|
|
231
|
-
|
|
268
|
+
import_core5 = require("@korajs/core");
|
|
232
269
|
init_richtext_serializer();
|
|
233
270
|
ATOMIC_OPS_KEY = "__kora_atomic_ops__";
|
|
234
271
|
TX_ID_KEY = "__kora_tx_id__";
|
|
@@ -609,11 +646,11 @@ async function computeSha256(data) {
|
|
|
609
646
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
610
647
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
611
648
|
}
|
|
612
|
-
var
|
|
649
|
+
var import_core6, BACKUP_VERSION;
|
|
613
650
|
var init_backup = __esm({
|
|
614
651
|
"src/backup/backup.ts"() {
|
|
615
652
|
"use strict";
|
|
616
|
-
|
|
653
|
+
import_core6 = require("@korajs/core");
|
|
617
654
|
init_serializer();
|
|
618
655
|
BACKUP_VERSION = 1;
|
|
619
656
|
}
|
|
@@ -670,7 +707,7 @@ __export(audit_trace_store_exports, {
|
|
|
670
707
|
});
|
|
671
708
|
function persistedAuditTraceFromEvent(event) {
|
|
672
709
|
return {
|
|
673
|
-
id: (0,
|
|
710
|
+
id: (0, import_core18.generateUUIDv7)(),
|
|
674
711
|
recordedAt: Date.now(),
|
|
675
712
|
eventType: event.type,
|
|
676
713
|
constraint: event.type === "constraint:violated" ? event.constraint : void 0,
|
|
@@ -736,11 +773,11 @@ function auditTraceIndexFields(trace) {
|
|
|
736
773
|
recordId: trace.operationA.recordId
|
|
737
774
|
};
|
|
738
775
|
}
|
|
739
|
-
var
|
|
776
|
+
var import_core18;
|
|
740
777
|
var init_audit_trace_store = __esm({
|
|
741
778
|
"src/audit/audit-trace-store.ts"() {
|
|
742
779
|
"use strict";
|
|
743
|
-
|
|
780
|
+
import_core18 = require("@korajs/core");
|
|
744
781
|
init_audit_json();
|
|
745
782
|
}
|
|
746
783
|
});
|
|
@@ -940,6 +977,7 @@ __export(index_exports, {
|
|
|
940
977
|
Collection: () => Collection,
|
|
941
978
|
InvalidStateTransitionError: () => InvalidStateTransitionError,
|
|
942
979
|
LAST_ACKED_SERVER_VECTOR_META_KEY: () => LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
980
|
+
OptimisticLockError: () => OptimisticLockError,
|
|
943
981
|
PersistenceError: () => PersistenceError,
|
|
944
982
|
QueryBuilder: () => QueryBuilder,
|
|
945
983
|
QueryError: () => QueryError,
|
|
@@ -1000,6 +1038,16 @@ var RecordNotFoundError = class extends import_core.KoraError {
|
|
|
1000
1038
|
this.name = "RecordNotFoundError";
|
|
1001
1039
|
}
|
|
1002
1040
|
};
|
|
1041
|
+
var OptimisticLockError = class extends import_core.KoraError {
|
|
1042
|
+
constructor(collection, recordId) {
|
|
1043
|
+
super(
|
|
1044
|
+
`Row version changed while merging record "${recordId}" in "${collection}"; retry with fresh state`,
|
|
1045
|
+
"OPTIMISTIC_LOCK",
|
|
1046
|
+
{ collection, recordId }
|
|
1047
|
+
);
|
|
1048
|
+
this.name = "OptimisticLockError";
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1003
1051
|
var AdapterError = class extends import_core.KoraError {
|
|
1004
1052
|
constructor(message, context) {
|
|
1005
1053
|
super(message, "ADAPTER_ERROR", context);
|
|
@@ -1176,27 +1224,27 @@ async function saveDeltaCursor(adapter, cursor) {
|
|
|
1176
1224
|
}
|
|
1177
1225
|
|
|
1178
1226
|
// src/store/store.ts
|
|
1179
|
-
var
|
|
1227
|
+
var import_core19 = require("@korajs/core");
|
|
1180
1228
|
init_backup();
|
|
1181
1229
|
|
|
1182
1230
|
// src/mutations/execute-delete.ts
|
|
1183
|
-
var
|
|
1231
|
+
var import_core8 = require("@korajs/core");
|
|
1184
1232
|
|
|
1185
1233
|
// src/lww/row-version.ts
|
|
1186
|
-
var
|
|
1234
|
+
var import_core7 = require("@korajs/core");
|
|
1187
1235
|
var EMPTY_ROW_VERSION = "";
|
|
1188
1236
|
function serializeRowVersion(timestamp) {
|
|
1189
|
-
return
|
|
1237
|
+
return import_core7.HybridLogicalClock.serialize(timestamp);
|
|
1190
1238
|
}
|
|
1191
1239
|
function rowVersionFromRecord(row) {
|
|
1192
1240
|
const version = row._version;
|
|
1193
1241
|
if (typeof version === "string" && version.length > 0) {
|
|
1194
|
-
return
|
|
1242
|
+
return import_core7.HybridLogicalClock.deserialize(version);
|
|
1195
1243
|
}
|
|
1196
1244
|
return { wallTime: row._updated_at, logical: 0, nodeId: "" };
|
|
1197
1245
|
}
|
|
1198
1246
|
function isIncomingNewerThanRow(incoming, row) {
|
|
1199
|
-
return
|
|
1247
|
+
return import_core7.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
|
|
1200
1248
|
}
|
|
1201
1249
|
function lwwVersionWhereClause(remoteVersion) {
|
|
1202
1250
|
return {
|
|
@@ -1256,6 +1304,28 @@ function buildUpdateQuery(collection, id, changes) {
|
|
|
1256
1304
|
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1257
1305
|
return { sql, params };
|
|
1258
1306
|
}
|
|
1307
|
+
function buildFieldFastForwardUpdateQuery(collection, id, fieldChanges, remoteVersion, wallTime, options) {
|
|
1308
|
+
const fieldClauses = Object.keys(fieldChanges).map((col) => `${col} = ?`);
|
|
1309
|
+
const setClauses = [
|
|
1310
|
+
...fieldClauses,
|
|
1311
|
+
"_version = CASE WHEN _version >= ? THEN _version ELSE ? END",
|
|
1312
|
+
"_updated_at = CASE WHEN _updated_at >= ? THEN _updated_at ELSE ? END"
|
|
1313
|
+
];
|
|
1314
|
+
const params = [
|
|
1315
|
+
...Object.values(fieldChanges),
|
|
1316
|
+
remoteVersion,
|
|
1317
|
+
remoteVersion,
|
|
1318
|
+
wallTime,
|
|
1319
|
+
wallTime
|
|
1320
|
+
];
|
|
1321
|
+
if (options?.maxCreatedAt !== void 0) {
|
|
1322
|
+
setClauses.push("_created_at = CASE WHEN _created_at >= ? THEN _created_at ELSE ? END");
|
|
1323
|
+
params.push(options.maxCreatedAt, options.maxCreatedAt);
|
|
1324
|
+
}
|
|
1325
|
+
params.push(id);
|
|
1326
|
+
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1327
|
+
return { sql, params };
|
|
1328
|
+
}
|
|
1259
1329
|
function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1260
1330
|
if (version !== void 0) {
|
|
1261
1331
|
return {
|
|
@@ -1268,13 +1338,6 @@ function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
|
1268
1338
|
params: [updatedAt, id]
|
|
1269
1339
|
};
|
|
1270
1340
|
}
|
|
1271
|
-
function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
|
|
1272
|
-
const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
|
|
1273
|
-
const lww = lwwVersionWhereClause(remoteVersion);
|
|
1274
|
-
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
|
|
1275
|
-
const params = [...Object.values(changes), id, ...lww.params];
|
|
1276
|
-
return { sql, params };
|
|
1277
|
-
}
|
|
1278
1341
|
function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1279
1342
|
const lww = lwwVersionWhereClause(version);
|
|
1280
1343
|
return {
|
|
@@ -1439,7 +1502,7 @@ async function executeDelete(ctx, id, options) {
|
|
|
1439
1502
|
await ctx.adapter.transaction(async (tx) => {
|
|
1440
1503
|
if (!operation) {
|
|
1441
1504
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1442
|
-
operation = await (0,
|
|
1505
|
+
operation = await (0, import_core8.createOperation)(
|
|
1443
1506
|
{
|
|
1444
1507
|
nodeId: ctx.nodeId,
|
|
1445
1508
|
type: "delete",
|
|
@@ -1486,11 +1549,65 @@ async function executeDelete(ctx, id, options) {
|
|
|
1486
1549
|
}
|
|
1487
1550
|
|
|
1488
1551
|
// src/mutations/execute-insert.ts
|
|
1489
|
-
var
|
|
1552
|
+
var import_core9 = require("@korajs/core");
|
|
1553
|
+
|
|
1554
|
+
// src/lww/field-versions.ts
|
|
1555
|
+
function parseFieldVersions(raw) {
|
|
1556
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1557
|
+
return {};
|
|
1558
|
+
}
|
|
1559
|
+
try {
|
|
1560
|
+
const parsed = JSON.parse(raw);
|
|
1561
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1562
|
+
return {};
|
|
1563
|
+
}
|
|
1564
|
+
const result = {};
|
|
1565
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1566
|
+
if (typeof value === "string") {
|
|
1567
|
+
result[key] = value;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return result;
|
|
1571
|
+
} catch {
|
|
1572
|
+
return {};
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function serializeFieldVersions(versions) {
|
|
1576
|
+
return JSON.stringify(versions);
|
|
1577
|
+
}
|
|
1578
|
+
function fieldVersionsForFields(fieldNames, version) {
|
|
1579
|
+
const versions = {};
|
|
1580
|
+
for (const field of fieldNames) {
|
|
1581
|
+
versions[field] = version;
|
|
1582
|
+
}
|
|
1583
|
+
return versions;
|
|
1584
|
+
}
|
|
1585
|
+
function effectiveFieldVersion(current, field, rowVersion) {
|
|
1586
|
+
const own = current[field];
|
|
1587
|
+
if (own !== void 0) {
|
|
1588
|
+
return own;
|
|
1589
|
+
}
|
|
1590
|
+
return rowVersion ?? EMPTY_ROW_VERSION;
|
|
1591
|
+
}
|
|
1592
|
+
function resolvePerFieldLww(current, incomingFields, incomingVersion, rowVersion) {
|
|
1593
|
+
const winners = [];
|
|
1594
|
+
const merged = { ...current };
|
|
1595
|
+
for (const field of incomingFields) {
|
|
1596
|
+
const stored = effectiveFieldVersion(current, field, rowVersion);
|
|
1597
|
+
if (incomingVersion > stored) {
|
|
1598
|
+
winners.push(field);
|
|
1599
|
+
merged[field] = incomingVersion;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { winners, merged };
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/mutations/execute-insert.ts
|
|
1606
|
+
init_op_data_encoding();
|
|
1490
1607
|
init_serializer();
|
|
1491
1608
|
async function executeInsert(ctx, data) {
|
|
1492
|
-
const validated = (0,
|
|
1493
|
-
const recordId = (0,
|
|
1609
|
+
const validated = (0, import_core9.validateRecord)(ctx.collection, ctx.definition, data, "insert");
|
|
1610
|
+
const recordId = (0, import_core9.generateUUIDv7)();
|
|
1494
1611
|
for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
|
|
1495
1612
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
1496
1613
|
validated[fieldName] = Date.now();
|
|
@@ -1501,13 +1618,16 @@ async function executeInsert(ctx, data) {
|
|
|
1501
1618
|
let record;
|
|
1502
1619
|
await ctx.adapter.transaction(async (tx) => {
|
|
1503
1620
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1504
|
-
operation = await (0,
|
|
1621
|
+
operation = await (0, import_core9.createOperation)(
|
|
1505
1622
|
{
|
|
1506
1623
|
nodeId: ctx.nodeId,
|
|
1507
1624
|
type: "insert",
|
|
1508
1625
|
collection: ctx.collection,
|
|
1509
1626
|
recordId,
|
|
1510
|
-
|
|
1627
|
+
// Binary richtext values are tagged as canonical JSON BEFORE the
|
|
1628
|
+
// operation is content-hashed, so the hash input, persisted JSON,
|
|
1629
|
+
// and wire payload are the identical value.
|
|
1630
|
+
data: encodeRichtextFieldsForOpData(validated, ctx.definition.fields),
|
|
1511
1631
|
previousData: null,
|
|
1512
1632
|
sequenceNumber,
|
|
1513
1633
|
causalDeps,
|
|
@@ -1523,7 +1643,12 @@ async function executeInsert(ctx, data) {
|
|
|
1523
1643
|
...serializedData,
|
|
1524
1644
|
_created_at: operation.timestamp.wallTime,
|
|
1525
1645
|
_updated_at: operation.timestamp.wallTime,
|
|
1526
|
-
_version: version
|
|
1646
|
+
_version: version,
|
|
1647
|
+
// Stamp every inserted field with this operation's version so later
|
|
1648
|
+
// per-field LWW compares against a real writer, not the fallback.
|
|
1649
|
+
_field_versions: serializeFieldVersions(
|
|
1650
|
+
fieldVersionsForFields(Object.keys(serializedData), version)
|
|
1651
|
+
)
|
|
1527
1652
|
};
|
|
1528
1653
|
const builtInsert = buildInsertQuery(ctx.collection, record);
|
|
1529
1654
|
const opInsert = buildInsertQuery(
|
|
@@ -1543,12 +1668,13 @@ async function executeInsert(ctx, data) {
|
|
|
1543
1668
|
}
|
|
1544
1669
|
|
|
1545
1670
|
// src/mutations/execute-update.ts
|
|
1546
|
-
var
|
|
1671
|
+
var import_core11 = require("@korajs/core");
|
|
1672
|
+
init_op_data_encoding();
|
|
1547
1673
|
init_serializer();
|
|
1548
1674
|
|
|
1549
1675
|
// src/state-machine/state-validator.ts
|
|
1550
|
-
var
|
|
1551
|
-
var InvalidStateTransitionError = class extends
|
|
1676
|
+
var import_core10 = require("@korajs/core");
|
|
1677
|
+
var InvalidStateTransitionError = class extends import_core10.KoraError {
|
|
1552
1678
|
constructor(collection, recordId, field, fromState, toState, allowedStates) {
|
|
1553
1679
|
super(
|
|
1554
1680
|
`Invalid state transition in collection "${collection}": cannot transition field "${field}" from "${fromState}" to "${toState}". Allowed transitions from "${fromState}": ${allowedStates.length > 0 ? allowedStates.join(", ") : "(none -- terminal state)"}`,
|
|
@@ -1582,7 +1708,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
|
|
|
1582
1708
|
collection: collectionName,
|
|
1583
1709
|
transitions: stateMachine.transitions
|
|
1584
1710
|
};
|
|
1585
|
-
const transitionResult = (0,
|
|
1711
|
+
const transitionResult = (0, import_core10.validateTransition)(constraint, currentState, newState);
|
|
1586
1712
|
if (transitionResult.valid) {
|
|
1587
1713
|
return { valid: true, allowedStates: transitionResult.allowedTargets };
|
|
1588
1714
|
}
|
|
@@ -1638,7 +1764,7 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1638
1764
|
if (!currentRow) {
|
|
1639
1765
|
throw new RecordNotFoundError(ctx.collection, id);
|
|
1640
1766
|
}
|
|
1641
|
-
let validated = (0,
|
|
1767
|
+
let validated = (0, import_core11.validateRecord)(ctx.collection, ctx.definition, data, "update");
|
|
1642
1768
|
const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
|
|
1643
1769
|
validated = validateUpdateStateMachine(
|
|
1644
1770
|
ctx.collection,
|
|
@@ -1656,9 +1782,9 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1656
1782
|
for (const key of Object.keys(validated)) {
|
|
1657
1783
|
const value = validated[key];
|
|
1658
1784
|
previousData[key] = currentRecord[key];
|
|
1659
|
-
if ((0,
|
|
1660
|
-
resolvedData[key] = (0,
|
|
1661
|
-
atomicOps[key] = (0,
|
|
1785
|
+
if ((0, import_core11.isAtomicOp)(value)) {
|
|
1786
|
+
resolvedData[key] = (0, import_core11.resolveAtomicOp)(currentRecord[key], value);
|
|
1787
|
+
atomicOps[key] = (0, import_core11.toAtomicOp)(value);
|
|
1662
1788
|
} else {
|
|
1663
1789
|
resolvedData[key] = value;
|
|
1664
1790
|
}
|
|
@@ -1668,14 +1794,17 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1668
1794
|
let operation;
|
|
1669
1795
|
await ctx.adapter.transaction(async (tx) => {
|
|
1670
1796
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1671
|
-
operation = await (0,
|
|
1797
|
+
operation = await (0, import_core11.createOperation)(
|
|
1672
1798
|
{
|
|
1673
1799
|
nodeId: ctx.nodeId,
|
|
1674
1800
|
type: "update",
|
|
1675
1801
|
collection: ctx.collection,
|
|
1676
1802
|
recordId: id,
|
|
1677
|
-
|
|
1678
|
-
|
|
1803
|
+
// Binary richtext values (new and previous) are tagged as canonical
|
|
1804
|
+
// JSON BEFORE the operation is content-hashed, so the hash input,
|
|
1805
|
+
// persisted JSON, and wire payload are the identical value.
|
|
1806
|
+
data: encodeRichtextFieldsForOpData(resolvedData, ctx.definition.fields),
|
|
1807
|
+
previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
|
|
1679
1808
|
sequenceNumber,
|
|
1680
1809
|
causalDeps,
|
|
1681
1810
|
schemaVersion: ctx.schema.version,
|
|
@@ -1686,10 +1815,15 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1686
1815
|
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1687
1816
|
const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
|
|
1688
1817
|
const version = serializeRowVersion(operation.timestamp);
|
|
1818
|
+
const mergedFieldVersions = parseFieldVersions(currentRow._field_versions);
|
|
1819
|
+
for (const changedField of Object.keys(serializedChanges)) {
|
|
1820
|
+
mergedFieldVersions[changedField] = version;
|
|
1821
|
+
}
|
|
1689
1822
|
const updateQuery = buildUpdateQuery(ctx.collection, id, {
|
|
1690
1823
|
...serializedChanges,
|
|
1691
1824
|
_updated_at: operation.timestamp.wallTime,
|
|
1692
|
-
_version: version
|
|
1825
|
+
_version: version,
|
|
1826
|
+
_field_versions: serializeFieldVersions(mergedFieldVersions)
|
|
1693
1827
|
});
|
|
1694
1828
|
const opInsert = buildInsertQuery(
|
|
1695
1829
|
`_kora_ops_${ctx.collection}`,
|
|
@@ -2089,7 +2223,7 @@ var QueryError2 = class extends Error {
|
|
|
2089
2223
|
};
|
|
2090
2224
|
|
|
2091
2225
|
// src/relations/relation-enforcer.ts
|
|
2092
|
-
var
|
|
2226
|
+
var import_core12 = require("@korajs/core");
|
|
2093
2227
|
init_serializer();
|
|
2094
2228
|
|
|
2095
2229
|
// src/relations/relation-lookup.ts
|
|
@@ -2114,7 +2248,7 @@ function getIncomingRelations(lookup, collection) {
|
|
|
2114
2248
|
}
|
|
2115
2249
|
|
|
2116
2250
|
// src/relations/relation-enforcer.ts
|
|
2117
|
-
var ReferentialIntegrityError = class extends
|
|
2251
|
+
var ReferentialIntegrityError = class extends import_core12.KoraError {
|
|
2118
2252
|
constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
|
|
2119
2253
|
super(
|
|
2120
2254
|
`Cannot delete record "${recordId}" from "${collection}": ${referencingCount} record(s) in "${referencingCollection}" reference it via relation "${relationName}" with onDelete: 'restrict'. Delete or reassign the referencing records first.`,
|
|
@@ -2203,7 +2337,7 @@ var RelationEnforcer = class {
|
|
|
2203
2337
|
for (const row of referencingRows) {
|
|
2204
2338
|
const now = Date.now();
|
|
2205
2339
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2206
|
-
const operation = await (0,
|
|
2340
|
+
const operation = await (0, import_core12.createOperation)(
|
|
2207
2341
|
{
|
|
2208
2342
|
nodeId: this.nodeId,
|
|
2209
2343
|
type: "delete",
|
|
@@ -2252,7 +2386,7 @@ var RelationEnforcer = class {
|
|
|
2252
2386
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2253
2387
|
const updateData = { [foreignKeyField]: null };
|
|
2254
2388
|
const previousData = { [foreignKeyField]: deletedRecordId };
|
|
2255
|
-
const operation = await (0,
|
|
2389
|
+
const operation = await (0, import_core12.createOperation)(
|
|
2256
2390
|
{
|
|
2257
2391
|
nodeId: this.nodeId,
|
|
2258
2392
|
type: "update",
|
|
@@ -2313,8 +2447,9 @@ var RelationEnforcer = class {
|
|
|
2313
2447
|
};
|
|
2314
2448
|
|
|
2315
2449
|
// src/replay/replay-to.ts
|
|
2316
|
-
var
|
|
2450
|
+
var import_core13 = require("@korajs/core");
|
|
2317
2451
|
var import_internal = require("@korajs/core/internal");
|
|
2452
|
+
init_op_data_encoding();
|
|
2318
2453
|
function collectCausalClosure(allOps, targetOperationId) {
|
|
2319
2454
|
const opMap = /* @__PURE__ */ new Map();
|
|
2320
2455
|
for (const op of allOps) {
|
|
@@ -2322,7 +2457,7 @@ function collectCausalClosure(allOps, targetOperationId) {
|
|
|
2322
2457
|
}
|
|
2323
2458
|
const target = opMap.get(targetOperationId);
|
|
2324
2459
|
if (!target) {
|
|
2325
|
-
throw new
|
|
2460
|
+
throw new import_core13.OperationError(
|
|
2326
2461
|
`Operation "${targetOperationId}" not found in the local operation log`,
|
|
2327
2462
|
{
|
|
2328
2463
|
operationId: targetOperationId
|
|
@@ -2354,7 +2489,7 @@ function buildReplaySnapshot(schema, allOps, targetOperationId) {
|
|
|
2354
2489
|
const operationsApplied = collectCausalClosure(allOps, targetOperationId);
|
|
2355
2490
|
const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
|
|
2356
2491
|
if (!targetOperation) {
|
|
2357
|
-
throw new
|
|
2492
|
+
throw new import_core13.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
|
|
2358
2493
|
operationId: targetOperationId
|
|
2359
2494
|
});
|
|
2360
2495
|
}
|
|
@@ -2399,7 +2534,9 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2399
2534
|
}
|
|
2400
2535
|
colMap.set(op.recordId, {
|
|
2401
2536
|
id: op.recordId,
|
|
2402
|
-
|
|
2537
|
+
// op.data stores binary richtext as tagged JSON; snapshots must
|
|
2538
|
+
// expose record-shaped values (Uint8Array/string).
|
|
2539
|
+
fields: decodeRichtextFieldsFromOpData(op.data, definition.fields),
|
|
2403
2540
|
deleted: false,
|
|
2404
2541
|
createdAt: wallTime,
|
|
2405
2542
|
updatedAt: wallTime
|
|
@@ -2410,9 +2547,10 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2410
2547
|
if (!op.data) {
|
|
2411
2548
|
return;
|
|
2412
2549
|
}
|
|
2550
|
+
const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
|
|
2413
2551
|
const existing = colMap.get(op.recordId);
|
|
2414
2552
|
if (existing && !existing.deleted) {
|
|
2415
|
-
existing.fields = { ...existing.fields, ...
|
|
2553
|
+
existing.fields = { ...existing.fields, ...decodedData };
|
|
2416
2554
|
existing.updatedAt = wallTime;
|
|
2417
2555
|
break;
|
|
2418
2556
|
}
|
|
@@ -2421,7 +2559,7 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2421
2559
|
}
|
|
2422
2560
|
colMap.set(op.recordId, {
|
|
2423
2561
|
id: op.recordId,
|
|
2424
|
-
fields:
|
|
2562
|
+
fields: decodedData,
|
|
2425
2563
|
deleted: false,
|
|
2426
2564
|
createdAt: wallTime,
|
|
2427
2565
|
updatedAt: wallTime
|
|
@@ -2477,7 +2615,7 @@ function toCollectionRecord(record, definition) {
|
|
|
2477
2615
|
}
|
|
2478
2616
|
|
|
2479
2617
|
// src/sequences/sequence-manager.ts
|
|
2480
|
-
var
|
|
2618
|
+
var import_core14 = require("@korajs/core");
|
|
2481
2619
|
var SequenceManager = class {
|
|
2482
2620
|
adapter;
|
|
2483
2621
|
nodeId;
|
|
@@ -2495,7 +2633,7 @@ var SequenceManager = class {
|
|
|
2495
2633
|
async next(name, config) {
|
|
2496
2634
|
const scope = config?.scope ?? "";
|
|
2497
2635
|
const startAt = config?.startAt ?? 1;
|
|
2498
|
-
const format = config?.format ?? (0,
|
|
2636
|
+
const format = config?.format ?? (0, import_core14.defaultSequenceFormat)(name);
|
|
2499
2637
|
let counter = 0;
|
|
2500
2638
|
await this.adapter.transaction(async (tx) => {
|
|
2501
2639
|
const rows = await tx.query(
|
|
@@ -2517,7 +2655,7 @@ var SequenceManager = class {
|
|
|
2517
2655
|
);
|
|
2518
2656
|
}
|
|
2519
2657
|
});
|
|
2520
|
-
return (0,
|
|
2658
|
+
return (0, import_core14.formatSequenceValue)(format, counter, this.nodeId);
|
|
2521
2659
|
}
|
|
2522
2660
|
/**
|
|
2523
2661
|
* Get the current counter value without incrementing.
|
|
@@ -2975,8 +3113,141 @@ var SubscriptionManager = class {
|
|
|
2975
3113
|
}
|
|
2976
3114
|
};
|
|
2977
3115
|
|
|
3116
|
+
// src/sync/rebase-unsynced-operations.ts
|
|
3117
|
+
var import_core15 = require("@korajs/core");
|
|
3118
|
+
var import_internal2 = require("@korajs/core/internal");
|
|
3119
|
+
init_serializer();
|
|
3120
|
+
var LOGICAL_COUNTER_LIMIT = import_core15.MAX_LOGICAL + 1;
|
|
3121
|
+
function emptyResult() {
|
|
3122
|
+
return { operations: [], idMapping: {}, rebasedCount: 0, newMaxTimestamp: null };
|
|
3123
|
+
}
|
|
3124
|
+
async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, correctedNowMs) {
|
|
3125
|
+
if (unsyncedOpIds.length === 0) {
|
|
3126
|
+
return emptyResult();
|
|
3127
|
+
}
|
|
3128
|
+
const rebaseIds = new Set(unsyncedOpIds);
|
|
3129
|
+
const rebased = [];
|
|
3130
|
+
let maxOtherSerializedTs = null;
|
|
3131
|
+
for (const collectionName of Object.keys(schema.collections)) {
|
|
3132
|
+
const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
|
|
3133
|
+
for (const row of rows) {
|
|
3134
|
+
if (rebaseIds.has(row.id)) {
|
|
3135
|
+
rebased.push(deserializeOperationWithCollection(row, collectionName));
|
|
3136
|
+
} else if (maxOtherSerializedTs === null || row.timestamp > maxOtherSerializedTs) {
|
|
3137
|
+
maxOtherSerializedTs = row.timestamp;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (rebased.length === 0) {
|
|
3142
|
+
return emptyResult();
|
|
3143
|
+
}
|
|
3144
|
+
const maxOtherWallTime = maxOtherSerializedTs === null ? Number.NEGATIVE_INFINITY : import_core15.HybridLogicalClock.deserialize(maxOtherSerializedTs).wallTime;
|
|
3145
|
+
const baseWallTime = Math.max(correctedNowMs, maxOtherWallTime + 1);
|
|
3146
|
+
rebased.sort((a, b) => import_core15.HybridLogicalClock.compare(a.timestamp, b.timestamp));
|
|
3147
|
+
const newTimestamps = rebased.map((op, i) => ({
|
|
3148
|
+
// Spill into wallTime past the 5-digit logical serialization limit so the
|
|
3149
|
+
// serialized order stays correct even for enormous offline queues.
|
|
3150
|
+
wallTime: baseWallTime + Math.floor(i / LOGICAL_COUNTER_LIMIT),
|
|
3151
|
+
logical: i % LOGICAL_COUNTER_LIMIT,
|
|
3152
|
+
nodeId: op.nodeId
|
|
3153
|
+
}));
|
|
3154
|
+
const idMapping = {};
|
|
3155
|
+
const newIds = [];
|
|
3156
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
3157
|
+
const op = rebased[i];
|
|
3158
|
+
const ts = newTimestamps[i];
|
|
3159
|
+
if (!op || !ts) {
|
|
3160
|
+
continue;
|
|
3161
|
+
}
|
|
3162
|
+
const input = {
|
|
3163
|
+
nodeId: op.nodeId,
|
|
3164
|
+
type: op.type,
|
|
3165
|
+
collection: op.collection,
|
|
3166
|
+
recordId: op.recordId,
|
|
3167
|
+
data: op.data,
|
|
3168
|
+
previousData: op.previousData,
|
|
3169
|
+
sequenceNumber: op.sequenceNumber,
|
|
3170
|
+
causalDeps: op.causalDeps,
|
|
3171
|
+
schemaVersion: op.schemaVersion,
|
|
3172
|
+
...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
|
|
3173
|
+
};
|
|
3174
|
+
const newId = await (0, import_internal2.computeOperationId)(input, import_core15.HybridLogicalClock.serialize(ts));
|
|
3175
|
+
idMapping[op.id] = newId;
|
|
3176
|
+
newIds.push(newId);
|
|
3177
|
+
}
|
|
3178
|
+
const newOperations = [];
|
|
3179
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
3180
|
+
const op = rebased[i];
|
|
3181
|
+
const ts = newTimestamps[i];
|
|
3182
|
+
const newId = newIds[i];
|
|
3183
|
+
if (!op || !ts || newId === void 0) {
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
newOperations.push({
|
|
3187
|
+
...op,
|
|
3188
|
+
id: newId,
|
|
3189
|
+
timestamp: ts,
|
|
3190
|
+
causalDeps: op.causalDeps.map((dep) => idMapping[dep] ?? dep)
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
await adapter.transaction(async (tx) => {
|
|
3194
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
3195
|
+
const oldOp = rebased[i];
|
|
3196
|
+
const newOp = newOperations[i];
|
|
3197
|
+
if (!oldOp || !newOp) {
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
const opsTable = `_kora_ops_${oldOp.collection}`;
|
|
3201
|
+
await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
|
|
3202
|
+
const opInsert = buildInsertQuery(
|
|
3203
|
+
opsTable,
|
|
3204
|
+
serializeOperation(newOp)
|
|
3205
|
+
);
|
|
3206
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
3207
|
+
const oldVersion = serializeRowVersion(oldOp.timestamp);
|
|
3208
|
+
const newVersion = serializeRowVersion(newOp.timestamp);
|
|
3209
|
+
await tx.execute(
|
|
3210
|
+
`UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
|
|
3211
|
+
[newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
|
|
3212
|
+
);
|
|
3213
|
+
const rows = await tx.query(
|
|
3214
|
+
`SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
|
|
3215
|
+
[oldOp.recordId]
|
|
3216
|
+
);
|
|
3217
|
+
const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
|
|
3218
|
+
let changed = false;
|
|
3219
|
+
for (const [field, version] of Object.entries(fieldVersions)) {
|
|
3220
|
+
if (version === oldVersion) {
|
|
3221
|
+
fieldVersions[field] = newVersion;
|
|
3222
|
+
changed = true;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
if (changed) {
|
|
3226
|
+
await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
|
|
3227
|
+
serializeFieldVersions(fieldVersions),
|
|
3228
|
+
oldOp.recordId
|
|
3229
|
+
]);
|
|
3230
|
+
}
|
|
3231
|
+
if (oldOp.type === "insert") {
|
|
3232
|
+
await tx.execute(
|
|
3233
|
+
`UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
|
|
3234
|
+
[newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
const newMaxTimestamp = newTimestamps[newTimestamps.length - 1] ?? null;
|
|
3240
|
+
return {
|
|
3241
|
+
operations: newOperations,
|
|
3242
|
+
idMapping,
|
|
3243
|
+
rebasedCount: newOperations.length,
|
|
3244
|
+
newMaxTimestamp
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
|
|
2978
3248
|
// src/transaction/transaction-context.ts
|
|
2979
|
-
var
|
|
3249
|
+
var import_core16 = require("@korajs/core");
|
|
3250
|
+
init_op_data_encoding();
|
|
2980
3251
|
init_serializer();
|
|
2981
3252
|
var TransactionContext = class {
|
|
2982
3253
|
transactionId;
|
|
@@ -2987,7 +3258,7 @@ var TransactionContext = class {
|
|
|
2987
3258
|
config;
|
|
2988
3259
|
constructor(config) {
|
|
2989
3260
|
this.config = config;
|
|
2990
|
-
this.transactionId = (0,
|
|
3261
|
+
this.transactionId = (0, import_core16.generateUUIDv7)();
|
|
2991
3262
|
}
|
|
2992
3263
|
/**
|
|
2993
3264
|
* Set a human-readable mutation name for this transaction.
|
|
@@ -3102,8 +3373,8 @@ var TransactionContext = class {
|
|
|
3102
3373
|
}
|
|
3103
3374
|
async insert(collectionName, definition, data) {
|
|
3104
3375
|
this.ensureActive();
|
|
3105
|
-
const validated = (0,
|
|
3106
|
-
const recordId = (0,
|
|
3376
|
+
const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "insert");
|
|
3377
|
+
const recordId = (0, import_core16.generateUUIDv7)();
|
|
3107
3378
|
const now = Date.now();
|
|
3108
3379
|
for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
|
|
3109
3380
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
@@ -3112,13 +3383,15 @@ var TransactionContext = class {
|
|
|
3112
3383
|
}
|
|
3113
3384
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3114
3385
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3115
|
-
const operation = await (0,
|
|
3386
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3116
3387
|
{
|
|
3117
3388
|
nodeId: this.config.nodeId,
|
|
3118
3389
|
type: "insert",
|
|
3119
3390
|
collection: collectionName,
|
|
3120
3391
|
recordId,
|
|
3121
|
-
|
|
3392
|
+
// Binary richtext values are tagged as canonical JSON before the
|
|
3393
|
+
// operation is content-hashed (see encodeRichtextFieldsForOpData).
|
|
3394
|
+
data: encodeRichtextFieldsForOpData(validated, definition.fields),
|
|
3122
3395
|
previousData: null,
|
|
3123
3396
|
sequenceNumber,
|
|
3124
3397
|
causalDeps,
|
|
@@ -3165,7 +3438,7 @@ var TransactionContext = class {
|
|
|
3165
3438
|
if (!currentRecord) {
|
|
3166
3439
|
throw new RecordNotFoundError(collectionName, id);
|
|
3167
3440
|
}
|
|
3168
|
-
const validated = (0,
|
|
3441
|
+
const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "update");
|
|
3169
3442
|
const now = Date.now();
|
|
3170
3443
|
const previousData = {};
|
|
3171
3444
|
const resolvedData = {};
|
|
@@ -3173,9 +3446,9 @@ var TransactionContext = class {
|
|
|
3173
3446
|
for (const key of Object.keys(validated)) {
|
|
3174
3447
|
const value = validated[key];
|
|
3175
3448
|
previousData[key] = currentRecord[key];
|
|
3176
|
-
if ((0,
|
|
3177
|
-
resolvedData[key] = (0,
|
|
3178
|
-
atomicOps[key] = (0,
|
|
3449
|
+
if ((0, import_core16.isAtomicOp)(value)) {
|
|
3450
|
+
resolvedData[key] = (0, import_core16.resolveAtomicOp)(currentRecord[key], value);
|
|
3451
|
+
atomicOps[key] = (0, import_core16.toAtomicOp)(value);
|
|
3179
3452
|
} else {
|
|
3180
3453
|
resolvedData[key] = value;
|
|
3181
3454
|
}
|
|
@@ -3183,14 +3456,16 @@ var TransactionContext = class {
|
|
|
3183
3456
|
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
3184
3457
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3185
3458
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3186
|
-
const operation = await (0,
|
|
3459
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3187
3460
|
{
|
|
3188
3461
|
nodeId: this.config.nodeId,
|
|
3189
3462
|
type: "update",
|
|
3190
3463
|
collection: collectionName,
|
|
3191
3464
|
recordId: id,
|
|
3192
|
-
|
|
3193
|
-
|
|
3465
|
+
// Binary richtext values (new and previous) are tagged as canonical
|
|
3466
|
+
// JSON before the operation is content-hashed.
|
|
3467
|
+
data: encodeRichtextFieldsForOpData(resolvedData, definition.fields),
|
|
3468
|
+
previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
|
|
3194
3469
|
sequenceNumber,
|
|
3195
3470
|
causalDeps,
|
|
3196
3471
|
schemaVersion: this.config.schema.version,
|
|
@@ -3236,7 +3511,7 @@ var TransactionContext = class {
|
|
|
3236
3511
|
const now = Date.now();
|
|
3237
3512
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3238
3513
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3239
|
-
const operation = await (0,
|
|
3514
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3240
3515
|
{
|
|
3241
3516
|
nodeId: this.config.nodeId,
|
|
3242
3517
|
type: "delete",
|
|
@@ -3297,14 +3572,16 @@ var TransactionContext = class {
|
|
|
3297
3572
|
if (entry.operation.type === "insert" && entry.operation.data) {
|
|
3298
3573
|
record = {
|
|
3299
3574
|
id,
|
|
3300
|
-
|
|
3575
|
+
// op.data stores binary richtext as tagged JSON; the effective
|
|
3576
|
+
// record must expose record-shaped values (Uint8Array/string).
|
|
3577
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
3301
3578
|
createdAt: entry.operation.timestamp.wallTime,
|
|
3302
3579
|
updatedAt: entry.operation.timestamp.wallTime
|
|
3303
3580
|
};
|
|
3304
3581
|
} else if (entry.operation.type === "update" && entry.operation.data && record) {
|
|
3305
3582
|
record = {
|
|
3306
3583
|
...record,
|
|
3307
|
-
...entry.operation.data,
|
|
3584
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
3308
3585
|
updatedAt: entry.operation.timestamp.wallTime
|
|
3309
3586
|
};
|
|
3310
3587
|
} else if (entry.operation.type === "delete") {
|
|
@@ -3339,7 +3616,7 @@ var TransactionSequenceAllocator = class {
|
|
|
3339
3616
|
};
|
|
3340
3617
|
|
|
3341
3618
|
// src/store/tab-node-id.ts
|
|
3342
|
-
var
|
|
3619
|
+
var import_core17 = require("@korajs/core");
|
|
3343
3620
|
function perTabStorageKey(dbName) {
|
|
3344
3621
|
return `kora-tab-node-${dbName}`;
|
|
3345
3622
|
}
|
|
@@ -3360,7 +3637,7 @@ function resolvePerTabNodeId(dbName) {
|
|
|
3360
3637
|
if (existing) {
|
|
3361
3638
|
return existing;
|
|
3362
3639
|
}
|
|
3363
|
-
const nodeId = (0,
|
|
3640
|
+
const nodeId = (0, import_core17.generateUUIDv7)();
|
|
3364
3641
|
savePerTabNodeId(dbName, nodeId);
|
|
3365
3642
|
return nodeId;
|
|
3366
3643
|
}
|
|
@@ -3370,7 +3647,7 @@ var Store = class {
|
|
|
3370
3647
|
opened = false;
|
|
3371
3648
|
nodeId = "";
|
|
3372
3649
|
sequenceNumber = 0;
|
|
3373
|
-
versionVector = (0,
|
|
3650
|
+
versionVector = (0, import_core19.createVersionVector)();
|
|
3374
3651
|
clock = null;
|
|
3375
3652
|
collections = /* @__PURE__ */ new Map();
|
|
3376
3653
|
subscriptionManager;
|
|
@@ -3404,8 +3681,8 @@ var Store = class {
|
|
|
3404
3681
|
await this.adapter.open(this.schema);
|
|
3405
3682
|
await this.runMigrationsIfNeeded();
|
|
3406
3683
|
this.nodeId = await this.loadOrGenerateNodeId();
|
|
3407
|
-
this.clock = new
|
|
3408
|
-
this.causalTracker = new
|
|
3684
|
+
this.clock = new import_core19.HybridLogicalClock(this.nodeId);
|
|
3685
|
+
this.causalTracker = new import_core19.CausalTracker();
|
|
3409
3686
|
this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
|
|
3410
3687
|
this.sequenceNumber = await this.loadSequenceNumber();
|
|
3411
3688
|
this.versionVector = await this.loadVersionVector();
|
|
@@ -3491,6 +3768,14 @@ var Store = class {
|
|
|
3491
3768
|
/**
|
|
3492
3769
|
* Get the node ID for this store instance.
|
|
3493
3770
|
*/
|
|
3771
|
+
/**
|
|
3772
|
+
* Records the offset between server time and this device's clock
|
|
3773
|
+
* (serverTime - localTime), learned at sync handshake. Lets the store's HLC
|
|
3774
|
+
* validate remote timestamps correctly even when the local clock is wrong.
|
|
3775
|
+
*/
|
|
3776
|
+
setClockReferenceOffset(offsetMs) {
|
|
3777
|
+
this.clock?.setReferenceOffset(offsetMs);
|
|
3778
|
+
}
|
|
3494
3779
|
getNodeId() {
|
|
3495
3780
|
this.ensureOpen();
|
|
3496
3781
|
return this.nodeId;
|
|
@@ -3517,45 +3802,158 @@ var Store = class {
|
|
|
3517
3802
|
if (this.clock) {
|
|
3518
3803
|
this.clock.receive(op.timestamp);
|
|
3519
3804
|
}
|
|
3520
|
-
const
|
|
3521
|
-
const
|
|
3805
|
+
const materializeTimestamp = options?.materializeTimestamp ?? op.timestamp;
|
|
3806
|
+
const remoteVersion = serializeRowVersion(materializeTimestamp);
|
|
3807
|
+
const wallTime = materializeTimestamp.wallTime;
|
|
3808
|
+
const materializeSource = options?.materializeData ?? op.data;
|
|
3522
3809
|
await this.adapter.transaction(async (tx) => {
|
|
3523
|
-
|
|
3810
|
+
const checkGuard = (row) => {
|
|
3811
|
+
const guard = options?.guardRowState;
|
|
3812
|
+
if (!guard) {
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
const version = typeof row?._version === "string" ? row._version : null;
|
|
3816
|
+
const fieldVersions = typeof row?._field_versions === "string" ? row._field_versions : null;
|
|
3817
|
+
if (version !== guard.version || fieldVersions !== guard.fieldVersions) {
|
|
3818
|
+
throw new OptimisticLockError(collection, op.recordId);
|
|
3819
|
+
}
|
|
3820
|
+
};
|
|
3821
|
+
if (op.type === "insert" && materializeSource) {
|
|
3822
|
+
const serializedData = serializeRecord(materializeSource, definition.fields);
|
|
3524
3823
|
const existing2 = await tx.query(
|
|
3525
|
-
`SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
|
|
3824
|
+
`SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
|
|
3526
3825
|
[op.recordId]
|
|
3527
3826
|
);
|
|
3528
3827
|
const row = existing2[0];
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
const serializedData = serializeRecord(op.data, definition.fields);
|
|
3828
|
+
checkGuard(row);
|
|
3829
|
+
if (!row) {
|
|
3532
3830
|
const record = {
|
|
3533
3831
|
id: op.recordId,
|
|
3534
3832
|
...serializedData,
|
|
3535
3833
|
_created_at: wallTime,
|
|
3536
3834
|
_updated_at: wallTime,
|
|
3537
|
-
_version: remoteVersion
|
|
3835
|
+
_version: remoteVersion,
|
|
3836
|
+
// Stamp every inserted field so per-field LWW has a real baseline.
|
|
3837
|
+
_field_versions: serializeFieldVersions(
|
|
3838
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
3839
|
+
)
|
|
3538
3840
|
};
|
|
3539
3841
|
const insertQuery = buildInsertQuery(collection, record);
|
|
3540
3842
|
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
3843
|
+
await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
|
|
3844
|
+
} else if (row._deleted === 1) {
|
|
3845
|
+
if (isIncomingNewerThanRow(materializeTimestamp, row)) {
|
|
3846
|
+
const fieldChanges = {
|
|
3847
|
+
...serializedData,
|
|
3848
|
+
_deleted: 0,
|
|
3849
|
+
_field_versions: serializeFieldVersions(
|
|
3850
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
3851
|
+
)
|
|
3852
|
+
};
|
|
3853
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
3854
|
+
collection,
|
|
3855
|
+
op.recordId,
|
|
3856
|
+
fieldChanges,
|
|
3857
|
+
remoteVersion,
|
|
3858
|
+
wallTime,
|
|
3859
|
+
{ maxCreatedAt: wallTime }
|
|
3860
|
+
);
|
|
3861
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
3862
|
+
}
|
|
3863
|
+
} else {
|
|
3864
|
+
const currentVersions = parseFieldVersions(row._field_versions);
|
|
3865
|
+
const rowVersion = typeof row._version === "string" ? row._version : void 0;
|
|
3866
|
+
const fieldChanges = {};
|
|
3867
|
+
let fieldVersionsJson;
|
|
3868
|
+
if (options?.forceMaterialize) {
|
|
3869
|
+
const merged = { ...currentVersions };
|
|
3870
|
+
for (const field of Object.keys(serializedData)) {
|
|
3871
|
+
fieldChanges[field] = serializedData[field];
|
|
3872
|
+
merged[field] = remoteVersion;
|
|
3873
|
+
}
|
|
3874
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
3875
|
+
} else {
|
|
3876
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
3877
|
+
currentVersions,
|
|
3878
|
+
Object.keys(serializedData),
|
|
3879
|
+
remoteVersion,
|
|
3880
|
+
rowVersion
|
|
3881
|
+
);
|
|
3882
|
+
for (const field of winners) {
|
|
3883
|
+
fieldChanges[field] = serializedData[field];
|
|
3884
|
+
}
|
|
3885
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
3886
|
+
}
|
|
3887
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
3888
|
+
collection,
|
|
3889
|
+
op.recordId,
|
|
3890
|
+
{ ...fieldChanges, _field_versions: fieldVersionsJson },
|
|
3891
|
+
remoteVersion,
|
|
3892
|
+
wallTime,
|
|
3893
|
+
{ maxCreatedAt: wallTime }
|
|
3894
|
+
);
|
|
3895
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
3541
3896
|
}
|
|
3542
|
-
} else if (op.type === "update" &&
|
|
3543
|
-
const serializedChanges = serializeRecord(
|
|
3544
|
-
const
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
};
|
|
3549
|
-
if (options?.reactivateIfDeleted) {
|
|
3550
|
-
updatePayload._deleted = 0;
|
|
3551
|
-
}
|
|
3552
|
-
const updateQuery = buildLwwUpdateQuery(
|
|
3553
|
-
collection,
|
|
3554
|
-
op.recordId,
|
|
3555
|
-
updatePayload,
|
|
3556
|
-
remoteVersion
|
|
3897
|
+
} else if (op.type === "update" && materializeSource) {
|
|
3898
|
+
const serializedChanges = serializeRecord(materializeSource, definition.fields);
|
|
3899
|
+
const changedFields = Object.keys(serializedChanges);
|
|
3900
|
+
const currentRows = await tx.query(
|
|
3901
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
3902
|
+
[op.recordId]
|
|
3557
3903
|
);
|
|
3558
|
-
|
|
3904
|
+
const currentRow = currentRows[0];
|
|
3905
|
+
checkGuard(currentRow);
|
|
3906
|
+
if (currentRow) {
|
|
3907
|
+
const currentVersions = parseFieldVersions(currentRow._field_versions);
|
|
3908
|
+
const rowVersion = typeof currentRow._version === "string" ? currentRow._version : void 0;
|
|
3909
|
+
if (options?.forceMaterialize) {
|
|
3910
|
+
const merged = { ...currentVersions };
|
|
3911
|
+
for (const field of changedFields) {
|
|
3912
|
+
merged[field] = remoteVersion;
|
|
3913
|
+
}
|
|
3914
|
+
const fieldChanges = {
|
|
3915
|
+
...serializedChanges,
|
|
3916
|
+
_field_versions: serializeFieldVersions(merged)
|
|
3917
|
+
};
|
|
3918
|
+
if (options?.reactivateIfDeleted) {
|
|
3919
|
+
fieldChanges._deleted = 0;
|
|
3920
|
+
}
|
|
3921
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
3922
|
+
collection,
|
|
3923
|
+
op.recordId,
|
|
3924
|
+
fieldChanges,
|
|
3925
|
+
remoteVersion,
|
|
3926
|
+
wallTime
|
|
3927
|
+
);
|
|
3928
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
3929
|
+
} else {
|
|
3930
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
3931
|
+
currentVersions,
|
|
3932
|
+
changedFields,
|
|
3933
|
+
remoteVersion,
|
|
3934
|
+
rowVersion
|
|
3935
|
+
);
|
|
3936
|
+
if (winners.length > 0 || options?.reactivateIfDeleted) {
|
|
3937
|
+
const winningChanges = {
|
|
3938
|
+
_field_versions: serializeFieldVersions(merged)
|
|
3939
|
+
};
|
|
3940
|
+
for (const field of winners) {
|
|
3941
|
+
winningChanges[field] = serializedChanges[field];
|
|
3942
|
+
}
|
|
3943
|
+
if (options?.reactivateIfDeleted) {
|
|
3944
|
+
winningChanges._deleted = 0;
|
|
3945
|
+
}
|
|
3946
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
3947
|
+
collection,
|
|
3948
|
+
op.recordId,
|
|
3949
|
+
winningChanges,
|
|
3950
|
+
remoteVersion,
|
|
3951
|
+
wallTime
|
|
3952
|
+
);
|
|
3953
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3559
3957
|
} else if (op.type === "delete") {
|
|
3560
3958
|
const deleteQuery = buildLwwSoftDeleteQuery(
|
|
3561
3959
|
collection,
|
|
@@ -3583,6 +3981,67 @@ var Store = class {
|
|
|
3583
3981
|
this.subscriptionManager.notify(collection, op);
|
|
3584
3982
|
return "applied";
|
|
3585
3983
|
}
|
|
3984
|
+
/**
|
|
3985
|
+
* Materialize update/delete operations that were logged for a record BEFORE
|
|
3986
|
+
* its insert arrived (reordered delivery). Runs inside the insert's write
|
|
3987
|
+
* transaction, folding each orphan in timestamp order through the exact
|
|
3988
|
+
* per-field LWW / LWW-delete rules the normal apply paths use, so the final
|
|
3989
|
+
* row state equals what a device that received the operations in causal
|
|
3990
|
+
* order computed. Called only when the insert created a previously-absent
|
|
3991
|
+
* row — in that case every logged update/delete for the record is
|
|
3992
|
+
* necessarily an orphan (rows are soft-deleted, never removed, so a row
|
|
3993
|
+
* that is absent now was never materialized).
|
|
3994
|
+
*/
|
|
3995
|
+
async foldOrphanedOperations(tx, collection, definition, recordId) {
|
|
3996
|
+
const orphanRows = await tx.query(
|
|
3997
|
+
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
|
|
3998
|
+
[recordId]
|
|
3999
|
+
);
|
|
4000
|
+
if (orphanRows.length === 0) {
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
const orphans = orphanRows.map((r) => deserializeOperationWithCollection(r, collection)).sort((a, b) => import_core19.HybridLogicalClock.compare(a.timestamp, b.timestamp));
|
|
4004
|
+
for (const orphan of orphans) {
|
|
4005
|
+
const orphanVersion = serializeRowVersion(orphan.timestamp);
|
|
4006
|
+
const orphanWall = orphan.timestamp.wallTime;
|
|
4007
|
+
if (orphan.type === "update" && orphan.data) {
|
|
4008
|
+
const orphanChanges = serializeRecord(orphan.data, definition.fields);
|
|
4009
|
+
const rows = await tx.query(
|
|
4010
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
4011
|
+
[recordId]
|
|
4012
|
+
);
|
|
4013
|
+
const current = rows[0];
|
|
4014
|
+
if (!current) {
|
|
4015
|
+
continue;
|
|
4016
|
+
}
|
|
4017
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
4018
|
+
parseFieldVersions(current._field_versions),
|
|
4019
|
+
Object.keys(orphanChanges),
|
|
4020
|
+
orphanVersion,
|
|
4021
|
+
typeof current._version === "string" ? current._version : void 0
|
|
4022
|
+
);
|
|
4023
|
+
if (winners.length > 0) {
|
|
4024
|
+
const winningChanges = {
|
|
4025
|
+
_field_versions: serializeFieldVersions(merged)
|
|
4026
|
+
};
|
|
4027
|
+
for (const field of winners) {
|
|
4028
|
+
winningChanges[field] = orphanChanges[field];
|
|
4029
|
+
}
|
|
4030
|
+
const query = buildFieldFastForwardUpdateQuery(
|
|
4031
|
+
collection,
|
|
4032
|
+
recordId,
|
|
4033
|
+
winningChanges,
|
|
4034
|
+
orphanVersion,
|
|
4035
|
+
orphanWall
|
|
4036
|
+
);
|
|
4037
|
+
await tx.execute(query.sql, query.params);
|
|
4038
|
+
}
|
|
4039
|
+
} else if (orphan.type === "delete") {
|
|
4040
|
+
const query = buildLwwSoftDeleteQuery(collection, recordId, orphanWall, orphanVersion);
|
|
4041
|
+
await tx.execute(query.sql, query.params);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
3586
4045
|
/**
|
|
3587
4046
|
* Get operations from a node within a sequence number range.
|
|
3588
4047
|
* Implements the OperationLog interface for computeDelta.
|
|
@@ -3738,6 +4197,31 @@ var Store = class {
|
|
|
3738
4197
|
deleted: row._deleted === 1
|
|
3739
4198
|
};
|
|
3740
4199
|
}
|
|
4200
|
+
/**
|
|
4201
|
+
* Raw version state of a materialized row, for optimistic-concurrency guarded
|
|
4202
|
+
* applies: capture this snapshot before computing a merge, pass it as
|
|
4203
|
+
* `guardRowState`, and the apply refuses to write if the row changed since.
|
|
4204
|
+
* Returns null when no row exists (which is itself a valid guard state — the
|
|
4205
|
+
* apply then requires the row to still be absent).
|
|
4206
|
+
*/
|
|
4207
|
+
async getRowVersionState(collection, recordId) {
|
|
4208
|
+
this.ensureOpen();
|
|
4209
|
+
if (!this.schema.collections[collection]) {
|
|
4210
|
+
return null;
|
|
4211
|
+
}
|
|
4212
|
+
const rows = await this.adapter.query(
|
|
4213
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
4214
|
+
[recordId]
|
|
4215
|
+
);
|
|
4216
|
+
const row = rows[0];
|
|
4217
|
+
if (!row) {
|
|
4218
|
+
return null;
|
|
4219
|
+
}
|
|
4220
|
+
return {
|
|
4221
|
+
version: typeof row._version === "string" ? row._version : null,
|
|
4222
|
+
fieldVersions: typeof row._field_versions === "string" ? row._field_versions : null
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
3741
4225
|
/**
|
|
3742
4226
|
* Latest operation from this device for a record (used for delete-vs-update merge on sync).
|
|
3743
4227
|
*/
|
|
@@ -3780,6 +4264,30 @@ var Store = class {
|
|
|
3780
4264
|
(nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
|
|
3781
4265
|
);
|
|
3782
4266
|
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Re-stamp never-acknowledged local operations after a fast device clock was
|
|
4269
|
+
* corrected, so sync can resume immediately. Safe because unacknowledged
|
|
4270
|
+
* operations are private to this device (like unpushed git commits);
|
|
4271
|
+
* acknowledged/shared operations are immutable and never touched.
|
|
4272
|
+
*
|
|
4273
|
+
* After the rewrite the store's HLC is advanced past the highest new
|
|
4274
|
+
* timestamp so subsequent local writes keep sorting after the rebased ops.
|
|
4275
|
+
* No data-change notifications are emitted: materialized values are
|
|
4276
|
+
* unchanged, only version stamps move.
|
|
4277
|
+
*/
|
|
4278
|
+
async rebaseUnsyncedOperations(unsyncedOpIds, correctedNowMs) {
|
|
4279
|
+
this.ensureOpen();
|
|
4280
|
+
const result = await rebaseUnsyncedOperationsInLog(
|
|
4281
|
+
this.adapter,
|
|
4282
|
+
this.schema,
|
|
4283
|
+
unsyncedOpIds,
|
|
4284
|
+
correctedNowMs
|
|
4285
|
+
);
|
|
4286
|
+
if (result.newMaxTimestamp && this.clock) {
|
|
4287
|
+
this.clock.advanceTo(result.newMaxTimestamp);
|
|
4288
|
+
}
|
|
4289
|
+
return result;
|
|
4290
|
+
}
|
|
3783
4291
|
/**
|
|
3784
4292
|
* Count of local operations ahead of the server version vector.
|
|
3785
4293
|
*/
|
|
@@ -3794,7 +4302,7 @@ var Store = class {
|
|
|
3794
4302
|
async compact(strategy) {
|
|
3795
4303
|
this.ensureOpen();
|
|
3796
4304
|
if (strategy.mode === "never") {
|
|
3797
|
-
return compactOperationLog(this.adapter, this.schema, strategy, (0,
|
|
4305
|
+
return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core19.createVersionVector)());
|
|
3798
4306
|
}
|
|
3799
4307
|
const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
|
|
3800
4308
|
return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
|
|
@@ -3829,7 +4337,7 @@ var Store = class {
|
|
|
3829
4337
|
let latest = null;
|
|
3830
4338
|
for (const row of rows) {
|
|
3831
4339
|
const op = deserializeOperationWithCollection(row, collection);
|
|
3832
|
-
if (!latest ||
|
|
4340
|
+
if (!latest || import_core19.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
|
|
3833
4341
|
latest = op;
|
|
3834
4342
|
}
|
|
3835
4343
|
}
|
|
@@ -3972,7 +4480,7 @@ var Store = class {
|
|
|
3972
4480
|
for (let v = storedVersion + 1; v <= targetVersion; v++) {
|
|
3973
4481
|
const migration = migrations[v];
|
|
3974
4482
|
if (!migration) continue;
|
|
3975
|
-
const sqlStatements = (0,
|
|
4483
|
+
const sqlStatements = (0, import_core19.migrationStepsToSQL)(migration.steps);
|
|
3976
4484
|
for (const sql of sqlStatements) {
|
|
3977
4485
|
try {
|
|
3978
4486
|
await this.adapter.execute(sql);
|
|
@@ -4050,7 +4558,7 @@ var Store = class {
|
|
|
4050
4558
|
if (rows[0]) {
|
|
4051
4559
|
return rows[0].value;
|
|
4052
4560
|
}
|
|
4053
|
-
const newNodeId = (0,
|
|
4561
|
+
const newNodeId = (0, import_core19.generateUUIDv7)();
|
|
4054
4562
|
await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
|
|
4055
4563
|
newNodeId
|
|
4056
4564
|
]);
|
|
@@ -4067,7 +4575,7 @@ var Store = class {
|
|
|
4067
4575
|
const rows = await this.adapter.query(
|
|
4068
4576
|
"SELECT node_id, sequence_number FROM _kora_version_vector"
|
|
4069
4577
|
);
|
|
4070
|
-
const vector = (0,
|
|
4578
|
+
const vector = (0, import_core19.createVersionVector)();
|
|
4071
4579
|
for (const row of rows) {
|
|
4072
4580
|
vector.set(row.node_id, row.sequence_number);
|
|
4073
4581
|
}
|
|
@@ -4141,12 +4649,12 @@ var QueryStore = class {
|
|
|
4141
4649
|
};
|
|
4142
4650
|
|
|
4143
4651
|
// src/reactivity/assert-query-ready.ts
|
|
4144
|
-
var
|
|
4652
|
+
var import_core20 = require("@korajs/core");
|
|
4145
4653
|
var LEGACY_PENDING_COLLECTION = "__pending__";
|
|
4146
4654
|
function assertQueryReady(query) {
|
|
4147
4655
|
const descriptor = query.getDescriptor();
|
|
4148
4656
|
if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
|
|
4149
|
-
throw new
|
|
4657
|
+
throw new import_core20.AppNotReadyError(
|
|
4150
4658
|
"Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
|
|
4151
4659
|
);
|
|
4152
4660
|
}
|
|
@@ -4574,6 +5082,7 @@ init_export_audit();
|
|
|
4574
5082
|
Collection,
|
|
4575
5083
|
InvalidStateTransitionError,
|
|
4576
5084
|
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
5085
|
+
OptimisticLockError,
|
|
4577
5086
|
PersistenceError,
|
|
4578
5087
|
QueryBuilder,
|
|
4579
5088
|
QueryError,
|