@korajs/store 0.5.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 +1102 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +219 -7
- package/dist/index.d.ts +219 -7
- package/dist/index.js +905 -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,9 +977,12 @@ __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,
|
|
984
|
+
QueryStore: () => QueryStore,
|
|
985
|
+
QueryStoreCache: () => QueryStoreCache,
|
|
946
986
|
RecordNotFoundError: () => RecordNotFoundError,
|
|
947
987
|
SequenceManager: () => SequenceManager,
|
|
948
988
|
Store: () => Store,
|
|
@@ -952,14 +992,18 @@ __export(index_exports, {
|
|
|
952
992
|
TransactionContext: () => TransactionContext,
|
|
953
993
|
WorkerInitError: () => WorkerInitError,
|
|
954
994
|
WorkerTimeoutError: () => WorkerTimeoutError,
|
|
995
|
+
asRichTextSyncEngine: () => asRichTextSyncEngine,
|
|
996
|
+
assertQueryReady: () => assertQueryReady,
|
|
955
997
|
collectOperationsAheadOfServer: () => collectOperationsAheadOfServer,
|
|
956
998
|
compactOperationLog: () => compactOperationLog,
|
|
957
999
|
computeAckCompactionWatermark: () => computeAckCompactionWatermark,
|
|
1000
|
+
createRichTextController: () => createRichTextController,
|
|
958
1001
|
decodeAuditExport: () => decodeAuditExport,
|
|
959
1002
|
decodeRichtext: () => decodeRichtext,
|
|
960
1003
|
deserializeVersionVectorFromMeta: () => deserializeVersionVectorFromMeta,
|
|
961
1004
|
encodeRichtext: () => encodeRichtext,
|
|
962
1005
|
exportBackup: () => exportBackup,
|
|
1006
|
+
getSharedQueryStoreCache: () => getSharedQueryStoreCache,
|
|
963
1007
|
mergeVersionVectors: () => mergeVersionVectors,
|
|
964
1008
|
persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
|
|
965
1009
|
pluralize: () => pluralize,
|
|
@@ -994,6 +1038,16 @@ var RecordNotFoundError = class extends import_core.KoraError {
|
|
|
994
1038
|
this.name = "RecordNotFoundError";
|
|
995
1039
|
}
|
|
996
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
|
+
};
|
|
997
1051
|
var AdapterError = class extends import_core.KoraError {
|
|
998
1052
|
constructor(message, context) {
|
|
999
1053
|
super(message, "ADAPTER_ERROR", context);
|
|
@@ -1170,27 +1224,27 @@ async function saveDeltaCursor(adapter, cursor) {
|
|
|
1170
1224
|
}
|
|
1171
1225
|
|
|
1172
1226
|
// src/store/store.ts
|
|
1173
|
-
var
|
|
1227
|
+
var import_core19 = require("@korajs/core");
|
|
1174
1228
|
init_backup();
|
|
1175
1229
|
|
|
1176
1230
|
// src/mutations/execute-delete.ts
|
|
1177
|
-
var
|
|
1231
|
+
var import_core8 = require("@korajs/core");
|
|
1178
1232
|
|
|
1179
1233
|
// src/lww/row-version.ts
|
|
1180
|
-
var
|
|
1234
|
+
var import_core7 = require("@korajs/core");
|
|
1181
1235
|
var EMPTY_ROW_VERSION = "";
|
|
1182
1236
|
function serializeRowVersion(timestamp) {
|
|
1183
|
-
return
|
|
1237
|
+
return import_core7.HybridLogicalClock.serialize(timestamp);
|
|
1184
1238
|
}
|
|
1185
1239
|
function rowVersionFromRecord(row) {
|
|
1186
1240
|
const version = row._version;
|
|
1187
1241
|
if (typeof version === "string" && version.length > 0) {
|
|
1188
|
-
return
|
|
1242
|
+
return import_core7.HybridLogicalClock.deserialize(version);
|
|
1189
1243
|
}
|
|
1190
1244
|
return { wallTime: row._updated_at, logical: 0, nodeId: "" };
|
|
1191
1245
|
}
|
|
1192
1246
|
function isIncomingNewerThanRow(incoming, row) {
|
|
1193
|
-
return
|
|
1247
|
+
return import_core7.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
|
|
1194
1248
|
}
|
|
1195
1249
|
function lwwVersionWhereClause(remoteVersion) {
|
|
1196
1250
|
return {
|
|
@@ -1250,6 +1304,28 @@ function buildUpdateQuery(collection, id, changes) {
|
|
|
1250
1304
|
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1251
1305
|
return { sql, params };
|
|
1252
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
|
+
}
|
|
1253
1329
|
function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1254
1330
|
if (version !== void 0) {
|
|
1255
1331
|
return {
|
|
@@ -1262,13 +1338,6 @@ function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
|
1262
1338
|
params: [updatedAt, id]
|
|
1263
1339
|
};
|
|
1264
1340
|
}
|
|
1265
|
-
function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
|
|
1266
|
-
const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
|
|
1267
|
-
const lww = lwwVersionWhereClause(remoteVersion);
|
|
1268
|
-
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
|
|
1269
|
-
const params = [...Object.values(changes), id, ...lww.params];
|
|
1270
|
-
return { sql, params };
|
|
1271
|
-
}
|
|
1272
1341
|
function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1273
1342
|
const lww = lwwVersionWhereClause(version);
|
|
1274
1343
|
return {
|
|
@@ -1433,7 +1502,7 @@ async function executeDelete(ctx, id, options) {
|
|
|
1433
1502
|
await ctx.adapter.transaction(async (tx) => {
|
|
1434
1503
|
if (!operation) {
|
|
1435
1504
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1436
|
-
operation = await (0,
|
|
1505
|
+
operation = await (0, import_core8.createOperation)(
|
|
1437
1506
|
{
|
|
1438
1507
|
nodeId: ctx.nodeId,
|
|
1439
1508
|
type: "delete",
|
|
@@ -1480,11 +1549,65 @@ async function executeDelete(ctx, id, options) {
|
|
|
1480
1549
|
}
|
|
1481
1550
|
|
|
1482
1551
|
// src/mutations/execute-insert.ts
|
|
1483
|
-
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();
|
|
1484
1607
|
init_serializer();
|
|
1485
1608
|
async function executeInsert(ctx, data) {
|
|
1486
|
-
const validated = (0,
|
|
1487
|
-
const recordId = (0,
|
|
1609
|
+
const validated = (0, import_core9.validateRecord)(ctx.collection, ctx.definition, data, "insert");
|
|
1610
|
+
const recordId = (0, import_core9.generateUUIDv7)();
|
|
1488
1611
|
for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
|
|
1489
1612
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
1490
1613
|
validated[fieldName] = Date.now();
|
|
@@ -1495,13 +1618,16 @@ async function executeInsert(ctx, data) {
|
|
|
1495
1618
|
let record;
|
|
1496
1619
|
await ctx.adapter.transaction(async (tx) => {
|
|
1497
1620
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1498
|
-
operation = await (0,
|
|
1621
|
+
operation = await (0, import_core9.createOperation)(
|
|
1499
1622
|
{
|
|
1500
1623
|
nodeId: ctx.nodeId,
|
|
1501
1624
|
type: "insert",
|
|
1502
1625
|
collection: ctx.collection,
|
|
1503
1626
|
recordId,
|
|
1504
|
-
|
|
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),
|
|
1505
1631
|
previousData: null,
|
|
1506
1632
|
sequenceNumber,
|
|
1507
1633
|
causalDeps,
|
|
@@ -1517,7 +1643,12 @@ async function executeInsert(ctx, data) {
|
|
|
1517
1643
|
...serializedData,
|
|
1518
1644
|
_created_at: operation.timestamp.wallTime,
|
|
1519
1645
|
_updated_at: operation.timestamp.wallTime,
|
|
1520
|
-
_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
|
+
)
|
|
1521
1652
|
};
|
|
1522
1653
|
const builtInsert = buildInsertQuery(ctx.collection, record);
|
|
1523
1654
|
const opInsert = buildInsertQuery(
|
|
@@ -1537,12 +1668,13 @@ async function executeInsert(ctx, data) {
|
|
|
1537
1668
|
}
|
|
1538
1669
|
|
|
1539
1670
|
// src/mutations/execute-update.ts
|
|
1540
|
-
var
|
|
1671
|
+
var import_core11 = require("@korajs/core");
|
|
1672
|
+
init_op_data_encoding();
|
|
1541
1673
|
init_serializer();
|
|
1542
1674
|
|
|
1543
1675
|
// src/state-machine/state-validator.ts
|
|
1544
|
-
var
|
|
1545
|
-
var InvalidStateTransitionError = class extends
|
|
1676
|
+
var import_core10 = require("@korajs/core");
|
|
1677
|
+
var InvalidStateTransitionError = class extends import_core10.KoraError {
|
|
1546
1678
|
constructor(collection, recordId, field, fromState, toState, allowedStates) {
|
|
1547
1679
|
super(
|
|
1548
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)"}`,
|
|
@@ -1576,7 +1708,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
|
|
|
1576
1708
|
collection: collectionName,
|
|
1577
1709
|
transitions: stateMachine.transitions
|
|
1578
1710
|
};
|
|
1579
|
-
const transitionResult = (0,
|
|
1711
|
+
const transitionResult = (0, import_core10.validateTransition)(constraint, currentState, newState);
|
|
1580
1712
|
if (transitionResult.valid) {
|
|
1581
1713
|
return { valid: true, allowedStates: transitionResult.allowedTargets };
|
|
1582
1714
|
}
|
|
@@ -1632,7 +1764,7 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1632
1764
|
if (!currentRow) {
|
|
1633
1765
|
throw new RecordNotFoundError(ctx.collection, id);
|
|
1634
1766
|
}
|
|
1635
|
-
let validated = (0,
|
|
1767
|
+
let validated = (0, import_core11.validateRecord)(ctx.collection, ctx.definition, data, "update");
|
|
1636
1768
|
const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
|
|
1637
1769
|
validated = validateUpdateStateMachine(
|
|
1638
1770
|
ctx.collection,
|
|
@@ -1650,9 +1782,9 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1650
1782
|
for (const key of Object.keys(validated)) {
|
|
1651
1783
|
const value = validated[key];
|
|
1652
1784
|
previousData[key] = currentRecord[key];
|
|
1653
|
-
if ((0,
|
|
1654
|
-
resolvedData[key] = (0,
|
|
1655
|
-
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);
|
|
1656
1788
|
} else {
|
|
1657
1789
|
resolvedData[key] = value;
|
|
1658
1790
|
}
|
|
@@ -1662,14 +1794,17 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1662
1794
|
let operation;
|
|
1663
1795
|
await ctx.adapter.transaction(async (tx) => {
|
|
1664
1796
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1665
|
-
operation = await (0,
|
|
1797
|
+
operation = await (0, import_core11.createOperation)(
|
|
1666
1798
|
{
|
|
1667
1799
|
nodeId: ctx.nodeId,
|
|
1668
1800
|
type: "update",
|
|
1669
1801
|
collection: ctx.collection,
|
|
1670
1802
|
recordId: id,
|
|
1671
|
-
|
|
1672
|
-
|
|
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),
|
|
1673
1808
|
sequenceNumber,
|
|
1674
1809
|
causalDeps,
|
|
1675
1810
|
schemaVersion: ctx.schema.version,
|
|
@@ -1680,10 +1815,15 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1680
1815
|
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1681
1816
|
const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
|
|
1682
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
|
+
}
|
|
1683
1822
|
const updateQuery = buildUpdateQuery(ctx.collection, id, {
|
|
1684
1823
|
...serializedChanges,
|
|
1685
1824
|
_updated_at: operation.timestamp.wallTime,
|
|
1686
|
-
_version: version
|
|
1825
|
+
_version: version,
|
|
1826
|
+
_field_versions: serializeFieldVersions(mergedFieldVersions)
|
|
1687
1827
|
});
|
|
1688
1828
|
const opInsert = buildInsertQuery(
|
|
1689
1829
|
`_kora_ops_${ctx.collection}`,
|
|
@@ -2083,7 +2223,7 @@ var QueryError2 = class extends Error {
|
|
|
2083
2223
|
};
|
|
2084
2224
|
|
|
2085
2225
|
// src/relations/relation-enforcer.ts
|
|
2086
|
-
var
|
|
2226
|
+
var import_core12 = require("@korajs/core");
|
|
2087
2227
|
init_serializer();
|
|
2088
2228
|
|
|
2089
2229
|
// src/relations/relation-lookup.ts
|
|
@@ -2108,7 +2248,7 @@ function getIncomingRelations(lookup, collection) {
|
|
|
2108
2248
|
}
|
|
2109
2249
|
|
|
2110
2250
|
// src/relations/relation-enforcer.ts
|
|
2111
|
-
var ReferentialIntegrityError = class extends
|
|
2251
|
+
var ReferentialIntegrityError = class extends import_core12.KoraError {
|
|
2112
2252
|
constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
|
|
2113
2253
|
super(
|
|
2114
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.`,
|
|
@@ -2197,7 +2337,7 @@ var RelationEnforcer = class {
|
|
|
2197
2337
|
for (const row of referencingRows) {
|
|
2198
2338
|
const now = Date.now();
|
|
2199
2339
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2200
|
-
const operation = await (0,
|
|
2340
|
+
const operation = await (0, import_core12.createOperation)(
|
|
2201
2341
|
{
|
|
2202
2342
|
nodeId: this.nodeId,
|
|
2203
2343
|
type: "delete",
|
|
@@ -2246,7 +2386,7 @@ var RelationEnforcer = class {
|
|
|
2246
2386
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2247
2387
|
const updateData = { [foreignKeyField]: null };
|
|
2248
2388
|
const previousData = { [foreignKeyField]: deletedRecordId };
|
|
2249
|
-
const operation = await (0,
|
|
2389
|
+
const operation = await (0, import_core12.createOperation)(
|
|
2250
2390
|
{
|
|
2251
2391
|
nodeId: this.nodeId,
|
|
2252
2392
|
type: "update",
|
|
@@ -2307,8 +2447,9 @@ var RelationEnforcer = class {
|
|
|
2307
2447
|
};
|
|
2308
2448
|
|
|
2309
2449
|
// src/replay/replay-to.ts
|
|
2310
|
-
var
|
|
2450
|
+
var import_core13 = require("@korajs/core");
|
|
2311
2451
|
var import_internal = require("@korajs/core/internal");
|
|
2452
|
+
init_op_data_encoding();
|
|
2312
2453
|
function collectCausalClosure(allOps, targetOperationId) {
|
|
2313
2454
|
const opMap = /* @__PURE__ */ new Map();
|
|
2314
2455
|
for (const op of allOps) {
|
|
@@ -2316,7 +2457,7 @@ function collectCausalClosure(allOps, targetOperationId) {
|
|
|
2316
2457
|
}
|
|
2317
2458
|
const target = opMap.get(targetOperationId);
|
|
2318
2459
|
if (!target) {
|
|
2319
|
-
throw new
|
|
2460
|
+
throw new import_core13.OperationError(
|
|
2320
2461
|
`Operation "${targetOperationId}" not found in the local operation log`,
|
|
2321
2462
|
{
|
|
2322
2463
|
operationId: targetOperationId
|
|
@@ -2348,7 +2489,7 @@ function buildReplaySnapshot(schema, allOps, targetOperationId) {
|
|
|
2348
2489
|
const operationsApplied = collectCausalClosure(allOps, targetOperationId);
|
|
2349
2490
|
const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
|
|
2350
2491
|
if (!targetOperation) {
|
|
2351
|
-
throw new
|
|
2492
|
+
throw new import_core13.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
|
|
2352
2493
|
operationId: targetOperationId
|
|
2353
2494
|
});
|
|
2354
2495
|
}
|
|
@@ -2393,7 +2534,9 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2393
2534
|
}
|
|
2394
2535
|
colMap.set(op.recordId, {
|
|
2395
2536
|
id: op.recordId,
|
|
2396
|
-
|
|
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),
|
|
2397
2540
|
deleted: false,
|
|
2398
2541
|
createdAt: wallTime,
|
|
2399
2542
|
updatedAt: wallTime
|
|
@@ -2404,9 +2547,10 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2404
2547
|
if (!op.data) {
|
|
2405
2548
|
return;
|
|
2406
2549
|
}
|
|
2550
|
+
const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
|
|
2407
2551
|
const existing = colMap.get(op.recordId);
|
|
2408
2552
|
if (existing && !existing.deleted) {
|
|
2409
|
-
existing.fields = { ...existing.fields, ...
|
|
2553
|
+
existing.fields = { ...existing.fields, ...decodedData };
|
|
2410
2554
|
existing.updatedAt = wallTime;
|
|
2411
2555
|
break;
|
|
2412
2556
|
}
|
|
@@ -2415,7 +2559,7 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2415
2559
|
}
|
|
2416
2560
|
colMap.set(op.recordId, {
|
|
2417
2561
|
id: op.recordId,
|
|
2418
|
-
fields:
|
|
2562
|
+
fields: decodedData,
|
|
2419
2563
|
deleted: false,
|
|
2420
2564
|
createdAt: wallTime,
|
|
2421
2565
|
updatedAt: wallTime
|
|
@@ -2471,7 +2615,7 @@ function toCollectionRecord(record, definition) {
|
|
|
2471
2615
|
}
|
|
2472
2616
|
|
|
2473
2617
|
// src/sequences/sequence-manager.ts
|
|
2474
|
-
var
|
|
2618
|
+
var import_core14 = require("@korajs/core");
|
|
2475
2619
|
var SequenceManager = class {
|
|
2476
2620
|
adapter;
|
|
2477
2621
|
nodeId;
|
|
@@ -2489,7 +2633,7 @@ var SequenceManager = class {
|
|
|
2489
2633
|
async next(name, config) {
|
|
2490
2634
|
const scope = config?.scope ?? "";
|
|
2491
2635
|
const startAt = config?.startAt ?? 1;
|
|
2492
|
-
const format = config?.format ?? (0,
|
|
2636
|
+
const format = config?.format ?? (0, import_core14.defaultSequenceFormat)(name);
|
|
2493
2637
|
let counter = 0;
|
|
2494
2638
|
await this.adapter.transaction(async (tx) => {
|
|
2495
2639
|
const rows = await tx.query(
|
|
@@ -2511,7 +2655,7 @@ var SequenceManager = class {
|
|
|
2511
2655
|
);
|
|
2512
2656
|
}
|
|
2513
2657
|
});
|
|
2514
|
-
return (0,
|
|
2658
|
+
return (0, import_core14.formatSequenceValue)(format, counter, this.nodeId);
|
|
2515
2659
|
}
|
|
2516
2660
|
/**
|
|
2517
2661
|
* Get the current counter value without incrementing.
|
|
@@ -2969,8 +3113,141 @@ var SubscriptionManager = class {
|
|
|
2969
3113
|
}
|
|
2970
3114
|
};
|
|
2971
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
|
+
|
|
2972
3248
|
// src/transaction/transaction-context.ts
|
|
2973
|
-
var
|
|
3249
|
+
var import_core16 = require("@korajs/core");
|
|
3250
|
+
init_op_data_encoding();
|
|
2974
3251
|
init_serializer();
|
|
2975
3252
|
var TransactionContext = class {
|
|
2976
3253
|
transactionId;
|
|
@@ -2981,7 +3258,7 @@ var TransactionContext = class {
|
|
|
2981
3258
|
config;
|
|
2982
3259
|
constructor(config) {
|
|
2983
3260
|
this.config = config;
|
|
2984
|
-
this.transactionId = (0,
|
|
3261
|
+
this.transactionId = (0, import_core16.generateUUIDv7)();
|
|
2985
3262
|
}
|
|
2986
3263
|
/**
|
|
2987
3264
|
* Set a human-readable mutation name for this transaction.
|
|
@@ -3096,8 +3373,8 @@ var TransactionContext = class {
|
|
|
3096
3373
|
}
|
|
3097
3374
|
async insert(collectionName, definition, data) {
|
|
3098
3375
|
this.ensureActive();
|
|
3099
|
-
const validated = (0,
|
|
3100
|
-
const recordId = (0,
|
|
3376
|
+
const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "insert");
|
|
3377
|
+
const recordId = (0, import_core16.generateUUIDv7)();
|
|
3101
3378
|
const now = Date.now();
|
|
3102
3379
|
for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
|
|
3103
3380
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
@@ -3106,13 +3383,15 @@ var TransactionContext = class {
|
|
|
3106
3383
|
}
|
|
3107
3384
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3108
3385
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3109
|
-
const operation = await (0,
|
|
3386
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3110
3387
|
{
|
|
3111
3388
|
nodeId: this.config.nodeId,
|
|
3112
3389
|
type: "insert",
|
|
3113
3390
|
collection: collectionName,
|
|
3114
3391
|
recordId,
|
|
3115
|
-
|
|
3392
|
+
// Binary richtext values are tagged as canonical JSON before the
|
|
3393
|
+
// operation is content-hashed (see encodeRichtextFieldsForOpData).
|
|
3394
|
+
data: encodeRichtextFieldsForOpData(validated, definition.fields),
|
|
3116
3395
|
previousData: null,
|
|
3117
3396
|
sequenceNumber,
|
|
3118
3397
|
causalDeps,
|
|
@@ -3159,7 +3438,7 @@ var TransactionContext = class {
|
|
|
3159
3438
|
if (!currentRecord) {
|
|
3160
3439
|
throw new RecordNotFoundError(collectionName, id);
|
|
3161
3440
|
}
|
|
3162
|
-
const validated = (0,
|
|
3441
|
+
const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "update");
|
|
3163
3442
|
const now = Date.now();
|
|
3164
3443
|
const previousData = {};
|
|
3165
3444
|
const resolvedData = {};
|
|
@@ -3167,9 +3446,9 @@ var TransactionContext = class {
|
|
|
3167
3446
|
for (const key of Object.keys(validated)) {
|
|
3168
3447
|
const value = validated[key];
|
|
3169
3448
|
previousData[key] = currentRecord[key];
|
|
3170
|
-
if ((0,
|
|
3171
|
-
resolvedData[key] = (0,
|
|
3172
|
-
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);
|
|
3173
3452
|
} else {
|
|
3174
3453
|
resolvedData[key] = value;
|
|
3175
3454
|
}
|
|
@@ -3177,14 +3456,16 @@ var TransactionContext = class {
|
|
|
3177
3456
|
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
3178
3457
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3179
3458
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3180
|
-
const operation = await (0,
|
|
3459
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3181
3460
|
{
|
|
3182
3461
|
nodeId: this.config.nodeId,
|
|
3183
3462
|
type: "update",
|
|
3184
3463
|
collection: collectionName,
|
|
3185
3464
|
recordId: id,
|
|
3186
|
-
|
|
3187
|
-
|
|
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),
|
|
3188
3469
|
sequenceNumber,
|
|
3189
3470
|
causalDeps,
|
|
3190
3471
|
schemaVersion: this.config.schema.version,
|
|
@@ -3230,7 +3511,7 @@ var TransactionContext = class {
|
|
|
3230
3511
|
const now = Date.now();
|
|
3231
3512
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3232
3513
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3233
|
-
const operation = await (0,
|
|
3514
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3234
3515
|
{
|
|
3235
3516
|
nodeId: this.config.nodeId,
|
|
3236
3517
|
type: "delete",
|
|
@@ -3291,14 +3572,16 @@ var TransactionContext = class {
|
|
|
3291
3572
|
if (entry.operation.type === "insert" && entry.operation.data) {
|
|
3292
3573
|
record = {
|
|
3293
3574
|
id,
|
|
3294
|
-
|
|
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),
|
|
3295
3578
|
createdAt: entry.operation.timestamp.wallTime,
|
|
3296
3579
|
updatedAt: entry.operation.timestamp.wallTime
|
|
3297
3580
|
};
|
|
3298
3581
|
} else if (entry.operation.type === "update" && entry.operation.data && record) {
|
|
3299
3582
|
record = {
|
|
3300
3583
|
...record,
|
|
3301
|
-
...entry.operation.data,
|
|
3584
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
3302
3585
|
updatedAt: entry.operation.timestamp.wallTime
|
|
3303
3586
|
};
|
|
3304
3587
|
} else if (entry.operation.type === "delete") {
|
|
@@ -3333,7 +3616,7 @@ var TransactionSequenceAllocator = class {
|
|
|
3333
3616
|
};
|
|
3334
3617
|
|
|
3335
3618
|
// src/store/tab-node-id.ts
|
|
3336
|
-
var
|
|
3619
|
+
var import_core17 = require("@korajs/core");
|
|
3337
3620
|
function perTabStorageKey(dbName) {
|
|
3338
3621
|
return `kora-tab-node-${dbName}`;
|
|
3339
3622
|
}
|
|
@@ -3354,7 +3637,7 @@ function resolvePerTabNodeId(dbName) {
|
|
|
3354
3637
|
if (existing) {
|
|
3355
3638
|
return existing;
|
|
3356
3639
|
}
|
|
3357
|
-
const nodeId = (0,
|
|
3640
|
+
const nodeId = (0, import_core17.generateUUIDv7)();
|
|
3358
3641
|
savePerTabNodeId(dbName, nodeId);
|
|
3359
3642
|
return nodeId;
|
|
3360
3643
|
}
|
|
@@ -3364,7 +3647,7 @@ var Store = class {
|
|
|
3364
3647
|
opened = false;
|
|
3365
3648
|
nodeId = "";
|
|
3366
3649
|
sequenceNumber = 0;
|
|
3367
|
-
versionVector = (0,
|
|
3650
|
+
versionVector = (0, import_core19.createVersionVector)();
|
|
3368
3651
|
clock = null;
|
|
3369
3652
|
collections = /* @__PURE__ */ new Map();
|
|
3370
3653
|
subscriptionManager;
|
|
@@ -3398,8 +3681,8 @@ var Store = class {
|
|
|
3398
3681
|
await this.adapter.open(this.schema);
|
|
3399
3682
|
await this.runMigrationsIfNeeded();
|
|
3400
3683
|
this.nodeId = await this.loadOrGenerateNodeId();
|
|
3401
|
-
this.clock = new
|
|
3402
|
-
this.causalTracker = new
|
|
3684
|
+
this.clock = new import_core19.HybridLogicalClock(this.nodeId);
|
|
3685
|
+
this.causalTracker = new import_core19.CausalTracker();
|
|
3403
3686
|
this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
|
|
3404
3687
|
this.sequenceNumber = await this.loadSequenceNumber();
|
|
3405
3688
|
this.versionVector = await this.loadVersionVector();
|
|
@@ -3485,6 +3768,14 @@ var Store = class {
|
|
|
3485
3768
|
/**
|
|
3486
3769
|
* Get the node ID for this store instance.
|
|
3487
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
|
+
}
|
|
3488
3779
|
getNodeId() {
|
|
3489
3780
|
this.ensureOpen();
|
|
3490
3781
|
return this.nodeId;
|
|
@@ -3511,45 +3802,158 @@ var Store = class {
|
|
|
3511
3802
|
if (this.clock) {
|
|
3512
3803
|
this.clock.receive(op.timestamp);
|
|
3513
3804
|
}
|
|
3514
|
-
const
|
|
3515
|
-
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;
|
|
3516
3809
|
await this.adapter.transaction(async (tx) => {
|
|
3517
|
-
|
|
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);
|
|
3518
3823
|
const existing2 = await tx.query(
|
|
3519
|
-
`SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
|
|
3824
|
+
`SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
|
|
3520
3825
|
[op.recordId]
|
|
3521
3826
|
);
|
|
3522
3827
|
const row = existing2[0];
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
const serializedData = serializeRecord(op.data, definition.fields);
|
|
3828
|
+
checkGuard(row);
|
|
3829
|
+
if (!row) {
|
|
3526
3830
|
const record = {
|
|
3527
3831
|
id: op.recordId,
|
|
3528
3832
|
...serializedData,
|
|
3529
3833
|
_created_at: wallTime,
|
|
3530
3834
|
_updated_at: wallTime,
|
|
3531
|
-
_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
|
+
)
|
|
3532
3840
|
};
|
|
3533
3841
|
const insertQuery = buildInsertQuery(collection, record);
|
|
3534
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);
|
|
3535
3896
|
}
|
|
3536
|
-
} else if (op.type === "update" &&
|
|
3537
|
-
const serializedChanges = serializeRecord(
|
|
3538
|
-
const
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
};
|
|
3543
|
-
if (options?.reactivateIfDeleted) {
|
|
3544
|
-
updatePayload._deleted = 0;
|
|
3545
|
-
}
|
|
3546
|
-
const updateQuery = buildLwwUpdateQuery(
|
|
3547
|
-
collection,
|
|
3548
|
-
op.recordId,
|
|
3549
|
-
updatePayload,
|
|
3550
|
-
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]
|
|
3551
3903
|
);
|
|
3552
|
-
|
|
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
|
+
}
|
|
3553
3957
|
} else if (op.type === "delete") {
|
|
3554
3958
|
const deleteQuery = buildLwwSoftDeleteQuery(
|
|
3555
3959
|
collection,
|
|
@@ -3577,6 +3981,67 @@ var Store = class {
|
|
|
3577
3981
|
this.subscriptionManager.notify(collection, op);
|
|
3578
3982
|
return "applied";
|
|
3579
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
|
+
}
|
|
3580
4045
|
/**
|
|
3581
4046
|
* Get operations from a node within a sequence number range.
|
|
3582
4047
|
* Implements the OperationLog interface for computeDelta.
|
|
@@ -3732,6 +4197,31 @@ var Store = class {
|
|
|
3732
4197
|
deleted: row._deleted === 1
|
|
3733
4198
|
};
|
|
3734
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
|
+
}
|
|
3735
4225
|
/**
|
|
3736
4226
|
* Latest operation from this device for a record (used for delete-vs-update merge on sync).
|
|
3737
4227
|
*/
|
|
@@ -3774,6 +4264,30 @@ var Store = class {
|
|
|
3774
4264
|
(nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
|
|
3775
4265
|
);
|
|
3776
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
|
+
}
|
|
3777
4291
|
/**
|
|
3778
4292
|
* Count of local operations ahead of the server version vector.
|
|
3779
4293
|
*/
|
|
@@ -3788,7 +4302,7 @@ var Store = class {
|
|
|
3788
4302
|
async compact(strategy) {
|
|
3789
4303
|
this.ensureOpen();
|
|
3790
4304
|
if (strategy.mode === "never") {
|
|
3791
|
-
return compactOperationLog(this.adapter, this.schema, strategy, (0,
|
|
4305
|
+
return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core19.createVersionVector)());
|
|
3792
4306
|
}
|
|
3793
4307
|
const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
|
|
3794
4308
|
return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
|
|
@@ -3823,7 +4337,7 @@ var Store = class {
|
|
|
3823
4337
|
let latest = null;
|
|
3824
4338
|
for (const row of rows) {
|
|
3825
4339
|
const op = deserializeOperationWithCollection(row, collection);
|
|
3826
|
-
if (!latest ||
|
|
4340
|
+
if (!latest || import_core19.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
|
|
3827
4341
|
latest = op;
|
|
3828
4342
|
}
|
|
3829
4343
|
}
|
|
@@ -3966,7 +4480,7 @@ var Store = class {
|
|
|
3966
4480
|
for (let v = storedVersion + 1; v <= targetVersion; v++) {
|
|
3967
4481
|
const migration = migrations[v];
|
|
3968
4482
|
if (!migration) continue;
|
|
3969
|
-
const sqlStatements = (0,
|
|
4483
|
+
const sqlStatements = (0, import_core19.migrationStepsToSQL)(migration.steps);
|
|
3970
4484
|
for (const sql of sqlStatements) {
|
|
3971
4485
|
try {
|
|
3972
4486
|
await this.adapter.execute(sql);
|
|
@@ -4044,7 +4558,7 @@ var Store = class {
|
|
|
4044
4558
|
if (rows[0]) {
|
|
4045
4559
|
return rows[0].value;
|
|
4046
4560
|
}
|
|
4047
|
-
const newNodeId = (0,
|
|
4561
|
+
const newNodeId = (0, import_core19.generateUUIDv7)();
|
|
4048
4562
|
await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
|
|
4049
4563
|
newNodeId
|
|
4050
4564
|
]);
|
|
@@ -4061,7 +4575,7 @@ var Store = class {
|
|
|
4061
4575
|
const rows = await this.adapter.query(
|
|
4062
4576
|
"SELECT node_id, sequence_number FROM _kora_version_vector"
|
|
4063
4577
|
);
|
|
4064
|
-
const vector = (0,
|
|
4578
|
+
const vector = (0, import_core19.createVersionVector)();
|
|
4065
4579
|
for (const row of rows) {
|
|
4066
4580
|
vector.set(row.node_id, row.sequence_number);
|
|
4067
4581
|
}
|
|
@@ -4074,6 +4588,483 @@ var Store = class {
|
|
|
4074
4588
|
}
|
|
4075
4589
|
};
|
|
4076
4590
|
|
|
4591
|
+
// src/reactivity/query-store.ts
|
|
4592
|
+
var EMPTY_ARRAY = Object.freeze([]);
|
|
4593
|
+
var QueryStore = class {
|
|
4594
|
+
snapshot = EMPTY_ARRAY;
|
|
4595
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4596
|
+
unsubscribeQuery = null;
|
|
4597
|
+
active = false;
|
|
4598
|
+
queryBuilder;
|
|
4599
|
+
constructor(queryBuilder) {
|
|
4600
|
+
this.queryBuilder = queryBuilder;
|
|
4601
|
+
}
|
|
4602
|
+
/**
|
|
4603
|
+
* Subscribe to snapshot changes.
|
|
4604
|
+
*
|
|
4605
|
+
* @returns Unsubscribe function
|
|
4606
|
+
*/
|
|
4607
|
+
subscribe = (onStoreChange) => {
|
|
4608
|
+
this.listeners.add(onStoreChange);
|
|
4609
|
+
if (!this.active) {
|
|
4610
|
+
this.startSubscription();
|
|
4611
|
+
}
|
|
4612
|
+
return () => {
|
|
4613
|
+
this.listeners.delete(onStoreChange);
|
|
4614
|
+
if (this.listeners.size === 0) {
|
|
4615
|
+
this.stopSubscription();
|
|
4616
|
+
}
|
|
4617
|
+
};
|
|
4618
|
+
};
|
|
4619
|
+
/** Synchronous read of the latest query results. */
|
|
4620
|
+
getSnapshot = () => {
|
|
4621
|
+
return this.snapshot;
|
|
4622
|
+
};
|
|
4623
|
+
/** Tear down listeners and the underlying query subscription. */
|
|
4624
|
+
destroy() {
|
|
4625
|
+
this.stopSubscription();
|
|
4626
|
+
this.listeners.clear();
|
|
4627
|
+
this.snapshot = EMPTY_ARRAY;
|
|
4628
|
+
}
|
|
4629
|
+
startSubscription() {
|
|
4630
|
+
this.active = true;
|
|
4631
|
+
this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
|
|
4632
|
+
if (!this.active) return;
|
|
4633
|
+
this.snapshot = Object.freeze([...results]);
|
|
4634
|
+
this.notifyListeners();
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
stopSubscription() {
|
|
4638
|
+
this.active = false;
|
|
4639
|
+
if (this.unsubscribeQuery) {
|
|
4640
|
+
this.unsubscribeQuery();
|
|
4641
|
+
this.unsubscribeQuery = null;
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
notifyListeners() {
|
|
4645
|
+
for (const listener of this.listeners) {
|
|
4646
|
+
listener();
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
};
|
|
4650
|
+
|
|
4651
|
+
// src/reactivity/assert-query-ready.ts
|
|
4652
|
+
var import_core20 = require("@korajs/core");
|
|
4653
|
+
var LEGACY_PENDING_COLLECTION = "__pending__";
|
|
4654
|
+
function assertQueryReady(query) {
|
|
4655
|
+
const descriptor = query.getDescriptor();
|
|
4656
|
+
if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
|
|
4657
|
+
throw new import_core20.AppNotReadyError(
|
|
4658
|
+
"Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
// src/reactivity/query-store-cache.ts
|
|
4664
|
+
var QueryStoreCache = class {
|
|
4665
|
+
constructor(scopeKey = "default") {
|
|
4666
|
+
this.scopeKey = scopeKey;
|
|
4667
|
+
}
|
|
4668
|
+
scopeKey;
|
|
4669
|
+
entries = /* @__PURE__ */ new Map();
|
|
4670
|
+
getOrCreate(queryBuilder) {
|
|
4671
|
+
const key = this.getKey(queryBuilder);
|
|
4672
|
+
const existing = this.entries.get(key);
|
|
4673
|
+
if (existing) {
|
|
4674
|
+
existing.refCount++;
|
|
4675
|
+
return existing.queryStore;
|
|
4676
|
+
}
|
|
4677
|
+
const queryStore = new QueryStore(queryBuilder);
|
|
4678
|
+
this.entries.set(key, {
|
|
4679
|
+
queryStore,
|
|
4680
|
+
refCount: 1
|
|
4681
|
+
});
|
|
4682
|
+
return queryStore;
|
|
4683
|
+
}
|
|
4684
|
+
release(queryBuilder) {
|
|
4685
|
+
const key = this.getKey(queryBuilder);
|
|
4686
|
+
const entry = this.entries.get(key);
|
|
4687
|
+
if (!entry) {
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
entry.refCount--;
|
|
4691
|
+
if (entry.refCount <= 0) {
|
|
4692
|
+
entry.queryStore.destroy();
|
|
4693
|
+
this.entries.delete(key);
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
clear() {
|
|
4697
|
+
for (const entry of this.entries.values()) {
|
|
4698
|
+
entry.queryStore.destroy();
|
|
4699
|
+
}
|
|
4700
|
+
this.entries.clear();
|
|
4701
|
+
}
|
|
4702
|
+
get size() {
|
|
4703
|
+
return this.entries.size;
|
|
4704
|
+
}
|
|
4705
|
+
getKey(queryBuilder) {
|
|
4706
|
+
return `${this.scopeKey}:${JSON.stringify(queryBuilder.getDescriptor())}`;
|
|
4707
|
+
}
|
|
4708
|
+
};
|
|
4709
|
+
var sharedCache = null;
|
|
4710
|
+
function getSharedQueryStoreCache() {
|
|
4711
|
+
if (!sharedCache) {
|
|
4712
|
+
sharedCache = new QueryStoreCache();
|
|
4713
|
+
}
|
|
4714
|
+
return sharedCache;
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
// src/richtext/create-richtext-controller.ts
|
|
4718
|
+
var Y2 = __toESM(require("yjs"), 1);
|
|
4719
|
+
init_richtext_serializer();
|
|
4720
|
+
var LOAD_ORIGIN = "kora-load";
|
|
4721
|
+
var REMOTE_ORIGIN = "kora-remote";
|
|
4722
|
+
var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
|
|
4723
|
+
var TEXT_KEY2 = "content";
|
|
4724
|
+
var COMPACT_AFTER_DELTAS = 20;
|
|
4725
|
+
var PERSIST_DEBOUNCE_MS = 400;
|
|
4726
|
+
function createRichTextController(options) {
|
|
4727
|
+
const {
|
|
4728
|
+
collection,
|
|
4729
|
+
collectionName,
|
|
4730
|
+
recordId,
|
|
4731
|
+
fieldName,
|
|
4732
|
+
store,
|
|
4733
|
+
syncEngine = null,
|
|
4734
|
+
useDocChannel
|
|
4735
|
+
} = options;
|
|
4736
|
+
const doc = new Y2.Doc();
|
|
4737
|
+
const text = doc.getText(TEXT_KEY2);
|
|
4738
|
+
const undoManager = new Y2.UndoManager(text);
|
|
4739
|
+
let user = options.user;
|
|
4740
|
+
let disposed = false;
|
|
4741
|
+
let ready = false;
|
|
4742
|
+
let error = null;
|
|
4743
|
+
let canUndo = false;
|
|
4744
|
+
let canRedo = false;
|
|
4745
|
+
let cursors = [];
|
|
4746
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4747
|
+
let snapshot = {
|
|
4748
|
+
ready: false,
|
|
4749
|
+
error: null,
|
|
4750
|
+
canUndo: false,
|
|
4751
|
+
canRedo: false,
|
|
4752
|
+
cursors: []
|
|
4753
|
+
};
|
|
4754
|
+
const baseUpdateRef = { current: null };
|
|
4755
|
+
const pendingDeltasRef = { current: [] };
|
|
4756
|
+
const docChannelActiveRef = { current: false };
|
|
4757
|
+
let persistTimer = null;
|
|
4758
|
+
let recordUnsubscribe = null;
|
|
4759
|
+
let docChannelUnsubscribe = null;
|
|
4760
|
+
let awarenessUnsubscribe = null;
|
|
4761
|
+
const refreshSnapshot = () => {
|
|
4762
|
+
snapshot = {
|
|
4763
|
+
ready,
|
|
4764
|
+
error,
|
|
4765
|
+
canUndo,
|
|
4766
|
+
canRedo,
|
|
4767
|
+
cursors: [...cursors]
|
|
4768
|
+
};
|
|
4769
|
+
};
|
|
4770
|
+
const notify = () => {
|
|
4771
|
+
refreshSnapshot();
|
|
4772
|
+
for (const listener of listeners) {
|
|
4773
|
+
listener();
|
|
4774
|
+
}
|
|
4775
|
+
};
|
|
4776
|
+
const syncHistoryState = () => {
|
|
4777
|
+
canUndo = undoManager.undoStack.length > 0;
|
|
4778
|
+
canRedo = undoManager.redoStack.length > 0;
|
|
4779
|
+
notify();
|
|
4780
|
+
};
|
|
4781
|
+
const getSnapshot = () => snapshot;
|
|
4782
|
+
const resolveAwarenessUser = () => {
|
|
4783
|
+
if (user) {
|
|
4784
|
+
return user;
|
|
4785
|
+
}
|
|
4786
|
+
if (syncEngine) {
|
|
4787
|
+
const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
|
|
4788
|
+
if (existing) {
|
|
4789
|
+
return existing;
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
return {
|
|
4793
|
+
name: "Anonymous",
|
|
4794
|
+
color: "#6366f1"
|
|
4795
|
+
};
|
|
4796
|
+
};
|
|
4797
|
+
const setCursor = (anchor, head) => {
|
|
4798
|
+
if (!syncEngine) {
|
|
4799
|
+
return;
|
|
4800
|
+
}
|
|
4801
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4802
|
+
const state = {
|
|
4803
|
+
user: resolveAwarenessUser(),
|
|
4804
|
+
cursor: {
|
|
4805
|
+
collection: collectionName,
|
|
4806
|
+
recordId,
|
|
4807
|
+
field: fieldName,
|
|
4808
|
+
anchor,
|
|
4809
|
+
head
|
|
4810
|
+
}
|
|
4811
|
+
};
|
|
4812
|
+
awareness.setLocalState(state);
|
|
4813
|
+
};
|
|
4814
|
+
const clearCursor = () => {
|
|
4815
|
+
if (!syncEngine) {
|
|
4816
|
+
return;
|
|
4817
|
+
}
|
|
4818
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4819
|
+
const current = awareness.getLocalState();
|
|
4820
|
+
if (!current?.cursor) {
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
|
|
4824
|
+
return;
|
|
4825
|
+
}
|
|
4826
|
+
awareness.setLocalState({ user: current.user });
|
|
4827
|
+
};
|
|
4828
|
+
const applyRemoteSnapshot = (value) => {
|
|
4829
|
+
const encoded = encodeRichtextInput(value);
|
|
4830
|
+
if (!encoded) {
|
|
4831
|
+
return;
|
|
4832
|
+
}
|
|
4833
|
+
const currentSnapshot = Y2.encodeStateAsUpdate(doc);
|
|
4834
|
+
if (updatesEqual(currentSnapshot, encoded)) {
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
Y2.applyUpdate(doc, encoded, REMOTE_ORIGIN);
|
|
4838
|
+
baseUpdateRef.current = encoded;
|
|
4839
|
+
pendingDeltasRef.current = [];
|
|
4840
|
+
syncHistoryState();
|
|
4841
|
+
};
|
|
4842
|
+
const flushPersist = async () => {
|
|
4843
|
+
const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
|
|
4844
|
+
if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
|
|
4845
|
+
baseUpdateRef.current = snapshot2;
|
|
4846
|
+
pendingDeltasRef.current = [];
|
|
4847
|
+
}
|
|
4848
|
+
try {
|
|
4849
|
+
await collection.update(recordId, {
|
|
4850
|
+
[fieldName]: snapshot2
|
|
4851
|
+
});
|
|
4852
|
+
} catch (cause) {
|
|
4853
|
+
if (!disposed) {
|
|
4854
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
4855
|
+
notify();
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
};
|
|
4859
|
+
const schedulePersist = () => {
|
|
4860
|
+
if (persistTimer) {
|
|
4861
|
+
clearTimeout(persistTimer);
|
|
4862
|
+
}
|
|
4863
|
+
persistTimer = setTimeout(() => {
|
|
4864
|
+
persistTimer = null;
|
|
4865
|
+
void flushPersist();
|
|
4866
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
4867
|
+
};
|
|
4868
|
+
const onDocUpdate = (update, origin) => {
|
|
4869
|
+
syncHistoryState();
|
|
4870
|
+
if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4873
|
+
pendingDeltasRef.current.push(update);
|
|
4874
|
+
if (docChannelActiveRef.current && syncEngine) {
|
|
4875
|
+
syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
|
|
4876
|
+
schedulePersist();
|
|
4877
|
+
return;
|
|
4878
|
+
}
|
|
4879
|
+
void flushPersist();
|
|
4880
|
+
};
|
|
4881
|
+
const subscribeRecordChanges = () => {
|
|
4882
|
+
recordUnsubscribe?.();
|
|
4883
|
+
recordUnsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
|
|
4884
|
+
const record = results[0];
|
|
4885
|
+
if (!record) {
|
|
4886
|
+
return;
|
|
4887
|
+
}
|
|
4888
|
+
applyRemoteSnapshot(record[fieldName]);
|
|
4889
|
+
});
|
|
4890
|
+
};
|
|
4891
|
+
const subscribeDocChannel = () => {
|
|
4892
|
+
docChannelUnsubscribe?.();
|
|
4893
|
+
if (!ready || !syncEngine || !docChannelActiveRef.current) {
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
const channel = syncEngine.getRichtextDocChannel?.();
|
|
4897
|
+
if (!channel) {
|
|
4898
|
+
return;
|
|
4899
|
+
}
|
|
4900
|
+
docChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {
|
|
4901
|
+
Y2.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
|
|
4902
|
+
syncHistoryState();
|
|
4903
|
+
});
|
|
4904
|
+
};
|
|
4905
|
+
const updateCursors = () => {
|
|
4906
|
+
if (!syncEngine) {
|
|
4907
|
+
cursors = [];
|
|
4908
|
+
notify();
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4912
|
+
const localClientId = awareness.clientId;
|
|
4913
|
+
const states = awareness.getStates();
|
|
4914
|
+
const fieldCursors = [];
|
|
4915
|
+
for (const [clientId, state] of states) {
|
|
4916
|
+
if (clientId === localClientId) continue;
|
|
4917
|
+
if (!state.cursor) continue;
|
|
4918
|
+
if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
|
|
4919
|
+
continue;
|
|
4920
|
+
}
|
|
4921
|
+
fieldCursors.push({
|
|
4922
|
+
clientId,
|
|
4923
|
+
userName: state.user.name,
|
|
4924
|
+
color: state.user.color,
|
|
4925
|
+
anchor: state.cursor.anchor,
|
|
4926
|
+
head: state.cursor.head
|
|
4927
|
+
});
|
|
4928
|
+
}
|
|
4929
|
+
cursors = fieldCursors;
|
|
4930
|
+
notify();
|
|
4931
|
+
};
|
|
4932
|
+
const subscribeAwareness = () => {
|
|
4933
|
+
awarenessUnsubscribe?.();
|
|
4934
|
+
if (!syncEngine) {
|
|
4935
|
+
cursors = [];
|
|
4936
|
+
notify();
|
|
4937
|
+
return;
|
|
4938
|
+
}
|
|
4939
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4940
|
+
awarenessUnsubscribe = awareness.on("change", () => {
|
|
4941
|
+
updateCursors();
|
|
4942
|
+
});
|
|
4943
|
+
updateCursors();
|
|
4944
|
+
};
|
|
4945
|
+
const initialize = async () => {
|
|
4946
|
+
ready = false;
|
|
4947
|
+
error = null;
|
|
4948
|
+
notify();
|
|
4949
|
+
try {
|
|
4950
|
+
const record = await collection.findById(recordId);
|
|
4951
|
+
if (disposed) return;
|
|
4952
|
+
doc.transact(() => {
|
|
4953
|
+
const target = doc.getText(TEXT_KEY2);
|
|
4954
|
+
target.delete(0, target.length);
|
|
4955
|
+
}, LOAD_ORIGIN);
|
|
4956
|
+
const encoded = encodeRichtextInput(record?.[fieldName]);
|
|
4957
|
+
baseUpdateRef.current = encoded;
|
|
4958
|
+
pendingDeltasRef.current = [];
|
|
4959
|
+
if (encoded) {
|
|
4960
|
+
Y2.applyUpdate(doc, encoded, LOAD_ORIGIN);
|
|
4961
|
+
}
|
|
4962
|
+
const channel = syncEngine?.getRichtextDocChannel?.();
|
|
4963
|
+
docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false;
|
|
4964
|
+
ready = true;
|
|
4965
|
+
syncHistoryState();
|
|
4966
|
+
subscribeRecordChanges();
|
|
4967
|
+
subscribeDocChannel();
|
|
4968
|
+
subscribeAwareness();
|
|
4969
|
+
notify();
|
|
4970
|
+
} catch (cause) {
|
|
4971
|
+
if (disposed) return;
|
|
4972
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
4973
|
+
notify();
|
|
4974
|
+
}
|
|
4975
|
+
};
|
|
4976
|
+
doc.on("update", onDocUpdate);
|
|
4977
|
+
void initialize();
|
|
4978
|
+
return {
|
|
4979
|
+
doc,
|
|
4980
|
+
text,
|
|
4981
|
+
getSnapshot,
|
|
4982
|
+
subscribe(listener) {
|
|
4983
|
+
listeners.add(listener);
|
|
4984
|
+
return () => {
|
|
4985
|
+
listeners.delete(listener);
|
|
4986
|
+
};
|
|
4987
|
+
},
|
|
4988
|
+
undo() {
|
|
4989
|
+
undoManager.undo();
|
|
4990
|
+
syncHistoryState();
|
|
4991
|
+
},
|
|
4992
|
+
redo() {
|
|
4993
|
+
undoManager.redo();
|
|
4994
|
+
syncHistoryState();
|
|
4995
|
+
},
|
|
4996
|
+
setCursor,
|
|
4997
|
+
clearCursor,
|
|
4998
|
+
setUser(nextUser) {
|
|
4999
|
+
user = nextUser;
|
|
5000
|
+
},
|
|
5001
|
+
destroy() {
|
|
5002
|
+
if (disposed) {
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
disposed = true;
|
|
5006
|
+
if (persistTimer) {
|
|
5007
|
+
clearTimeout(persistTimer);
|
|
5008
|
+
persistTimer = null;
|
|
5009
|
+
}
|
|
5010
|
+
doc.off("update", onDocUpdate);
|
|
5011
|
+
recordUnsubscribe?.();
|
|
5012
|
+
docChannelUnsubscribe?.();
|
|
5013
|
+
awarenessUnsubscribe?.();
|
|
5014
|
+
clearCursor();
|
|
5015
|
+
undoManager.destroy();
|
|
5016
|
+
baseUpdateRef.current = null;
|
|
5017
|
+
pendingDeltasRef.current = [];
|
|
5018
|
+
docChannelActiveRef.current = false;
|
|
5019
|
+
listeners.clear();
|
|
5020
|
+
}
|
|
5021
|
+
};
|
|
5022
|
+
}
|
|
5023
|
+
function encodeRichtextInput(value) {
|
|
5024
|
+
if (value === null || value === void 0) {
|
|
5025
|
+
return null;
|
|
5026
|
+
}
|
|
5027
|
+
try {
|
|
5028
|
+
return encodeRichtext(value);
|
|
5029
|
+
} catch {
|
|
5030
|
+
if (typeof value === "string") {
|
|
5031
|
+
const fallbackDoc = new Y2.Doc();
|
|
5032
|
+
fallbackDoc.getText(TEXT_KEY2).insert(0, value);
|
|
5033
|
+
return Y2.encodeStateAsUpdate(fallbackDoc);
|
|
5034
|
+
}
|
|
5035
|
+
throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
function composeRichtextSnapshot(base, deltas) {
|
|
5039
|
+
const mergedDoc = new Y2.Doc();
|
|
5040
|
+
if (base) {
|
|
5041
|
+
Y2.applyUpdate(mergedDoc, base);
|
|
5042
|
+
}
|
|
5043
|
+
for (const delta of deltas) {
|
|
5044
|
+
Y2.applyUpdate(mergedDoc, delta);
|
|
5045
|
+
}
|
|
5046
|
+
return Y2.encodeStateAsUpdate(mergedDoc);
|
|
5047
|
+
}
|
|
5048
|
+
function updatesEqual(left, right) {
|
|
5049
|
+
if (left.length !== right.length) {
|
|
5050
|
+
return false;
|
|
5051
|
+
}
|
|
5052
|
+
for (let index = 0; index < left.length; index++) {
|
|
5053
|
+
if (left[index] !== right[index]) {
|
|
5054
|
+
return false;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
return true;
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
// src/richtext/as-rich-text-sync-engine.ts
|
|
5061
|
+
function asRichTextSyncEngine(engine) {
|
|
5062
|
+
if (engine == null) {
|
|
5063
|
+
return null;
|
|
5064
|
+
}
|
|
5065
|
+
return engine;
|
|
5066
|
+
}
|
|
5067
|
+
|
|
4077
5068
|
// src/index.ts
|
|
4078
5069
|
init_richtext_serializer();
|
|
4079
5070
|
|
|
@@ -4091,9 +5082,12 @@ init_export_audit();
|
|
|
4091
5082
|
Collection,
|
|
4092
5083
|
InvalidStateTransitionError,
|
|
4093
5084
|
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
5085
|
+
OptimisticLockError,
|
|
4094
5086
|
PersistenceError,
|
|
4095
5087
|
QueryBuilder,
|
|
4096
5088
|
QueryError,
|
|
5089
|
+
QueryStore,
|
|
5090
|
+
QueryStoreCache,
|
|
4097
5091
|
RecordNotFoundError,
|
|
4098
5092
|
SequenceManager,
|
|
4099
5093
|
Store,
|
|
@@ -4103,14 +5097,18 @@ init_export_audit();
|
|
|
4103
5097
|
TransactionContext,
|
|
4104
5098
|
WorkerInitError,
|
|
4105
5099
|
WorkerTimeoutError,
|
|
5100
|
+
asRichTextSyncEngine,
|
|
5101
|
+
assertQueryReady,
|
|
4106
5102
|
collectOperationsAheadOfServer,
|
|
4107
5103
|
compactOperationLog,
|
|
4108
5104
|
computeAckCompactionWatermark,
|
|
5105
|
+
createRichTextController,
|
|
4109
5106
|
decodeAuditExport,
|
|
4110
5107
|
decodeRichtext,
|
|
4111
5108
|
deserializeVersionVectorFromMeta,
|
|
4112
5109
|
encodeRichtext,
|
|
4113
5110
|
exportBackup,
|
|
5111
|
+
getSharedQueryStoreCache,
|
|
4114
5112
|
mergeVersionVectors,
|
|
4115
5113
|
persistedAuditTraceFromEvent,
|
|
4116
5114
|
pluralize,
|