@korajs/store 0.4.0 → 0.6.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/dist/adapters/better-sqlite3.d.cts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/indexeddb.cjs +410 -17
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +23 -5
- package/dist/adapters/indexeddb.d.ts +23 -5
- package/dist/adapters/indexeddb.js +163 -18
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
- package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
- package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
- package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +34 -5
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +249 -2
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +15 -3
- package/dist/adapters/sqlite-wasm.d.ts +15 -3
- package/dist/adapters/sqlite-wasm.js +2 -2
- package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
- package/dist/backup-JN3LB4YT.js +14 -0
- package/dist/backup-JN3LB4YT.js.map +1 -0
- package/dist/chunk-56LL4XP6.js +205 -0
- package/dist/chunk-56LL4XP6.js.map +1 -0
- package/dist/chunk-6AHYGQQT.js +425 -0
- package/dist/chunk-6AHYGQQT.js.map +1 -0
- package/dist/chunk-7BPETY5Y.js +570 -0
- package/dist/chunk-7BPETY5Y.js.map +1 -0
- package/dist/chunk-AGN3CIPB.js +188 -0
- package/dist/chunk-AGN3CIPB.js.map +1 -0
- package/dist/chunk-EZSTD2XC.js +380 -0
- package/dist/chunk-EZSTD2XC.js.map +1 -0
- package/dist/chunk-MYSJN3DF.js +117 -0
- package/dist/chunk-MYSJN3DF.js.map +1 -0
- package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
- package/dist/chunk-XO4LMPAH.js.map +1 -0
- package/dist/export-audit-5GV6VRDZ.js +15 -0
- package/dist/export-audit-5GV6VRDZ.js.map +1 -0
- package/dist/index.cjs +2662 -577
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +565 -187
- package/dist/index.d.ts +565 -187
- package/dist/index.js +1381 -739
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +789 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +154 -0
- package/dist/internal.d.ts +154 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
- package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
- package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
- package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
- package/dist/types-BMyHmwvn.d.ts +163 -0
- package/dist/types-CTr00g_d.d.cts +163 -0
- package/dist/types-DepnRDk_.d.cts +212 -0
- package/dist/types-DepnRDk_.d.ts +212 -0
- package/package.json +23 -2
- package/dist/chunk-DXKLAQ6P.js.map +0 -1
- package/dist/chunk-ZP5AXQ3Z.js +0 -179
- package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
- package/dist/types-DF-KDSK1.d.cts +0 -106
- package/dist/types-DF-KDSK1.d.ts +0 -106
- /package/dist/{sqlite-wasm-channel-46AOWNPM.js.map → audit-trace-store-AM4EWQNZ.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,15 +30,921 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/serialization/richtext-serializer.ts
|
|
34
|
+
function encodeRichtext(value) {
|
|
35
|
+
if (value === null || value === void 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
const doc = new Y.Doc();
|
|
40
|
+
doc.getText(TEXT_KEY).insert(0, value);
|
|
41
|
+
return Y.encodeStateAsUpdate(doc);
|
|
42
|
+
}
|
|
43
|
+
if (value instanceof Uint8Array) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
if (value instanceof ArrayBuffer) {
|
|
47
|
+
return new Uint8Array(value);
|
|
48
|
+
}
|
|
49
|
+
throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
|
|
50
|
+
}
|
|
51
|
+
function decodeRichtext(value) {
|
|
52
|
+
if (value === null || value === void 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
|
|
56
|
+
return new Uint8Array(value);
|
|
57
|
+
}
|
|
58
|
+
if (value instanceof Uint8Array) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
if (value instanceof ArrayBuffer) {
|
|
62
|
+
return new Uint8Array(value);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function richtextToPlainText(value) {
|
|
69
|
+
const encoded = encodeRichtext(value);
|
|
70
|
+
if (!encoded) return "";
|
|
71
|
+
const doc = new Y.Doc();
|
|
72
|
+
Y.applyUpdate(doc, encoded);
|
|
73
|
+
return doc.getText(TEXT_KEY).toString();
|
|
74
|
+
}
|
|
75
|
+
function richtextStatesEqual(a, b) {
|
|
76
|
+
try {
|
|
77
|
+
return richtextToPlainText(a) === richtextToPlainText(b);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
var Y, TEXT_KEY;
|
|
83
|
+
var init_richtext_serializer = __esm({
|
|
84
|
+
"src/serialization/richtext-serializer.ts"() {
|
|
85
|
+
"use strict";
|
|
86
|
+
Y = __toESM(require("yjs"), 1);
|
|
87
|
+
TEXT_KEY = "content";
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// src/serialization/serializer.ts
|
|
92
|
+
function serializeRecord(data, fields) {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const [key, value] of Object.entries(data)) {
|
|
95
|
+
const descriptor = fields[key];
|
|
96
|
+
if (!descriptor) {
|
|
97
|
+
result[key] = value;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
result[key] = serializeValue(value, descriptor);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function deserializeRecord(row, fields) {
|
|
105
|
+
const result = {
|
|
106
|
+
id: row.id,
|
|
107
|
+
createdAt: row._created_at,
|
|
108
|
+
updatedAt: row._updated_at
|
|
109
|
+
};
|
|
110
|
+
for (const [key, descriptor] of Object.entries(fields)) {
|
|
111
|
+
const rawValue = row[key];
|
|
112
|
+
if (rawValue === void 0 || rawValue === null) {
|
|
113
|
+
result[key] = rawValue ?? null;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
result[key] = deserializeValue(rawValue, descriptor);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function serializeOperation(op) {
|
|
121
|
+
const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
|
|
122
|
+
let dataPayload = null;
|
|
123
|
+
if (op.data) {
|
|
124
|
+
dataPayload = { ...op.data };
|
|
125
|
+
if (op.atomicOps !== void 0 && Object.keys(op.atomicOps).length > 0) {
|
|
126
|
+
dataPayload[ATOMIC_OPS_KEY] = op.atomicOps;
|
|
127
|
+
}
|
|
128
|
+
if (op.transactionId !== void 0) {
|
|
129
|
+
dataPayload[TX_ID_KEY] = op.transactionId;
|
|
130
|
+
}
|
|
131
|
+
if (op.mutationName !== void 0) {
|
|
132
|
+
dataPayload[MUTATION_NAME_KEY] = op.mutationName;
|
|
133
|
+
}
|
|
134
|
+
} else if (hasMetadata) {
|
|
135
|
+
dataPayload = {};
|
|
136
|
+
if (op.transactionId !== void 0) {
|
|
137
|
+
dataPayload[TX_ID_KEY] = op.transactionId;
|
|
138
|
+
}
|
|
139
|
+
if (op.mutationName !== void 0) {
|
|
140
|
+
dataPayload[MUTATION_NAME_KEY] = op.mutationName;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
id: op.id,
|
|
145
|
+
node_id: op.nodeId,
|
|
146
|
+
type: op.type,
|
|
147
|
+
record_id: op.recordId,
|
|
148
|
+
data: dataPayload ? JSON.stringify(dataPayload) : null,
|
|
149
|
+
previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
|
|
150
|
+
timestamp: import_core4.HybridLogicalClock.serialize(op.timestamp),
|
|
151
|
+
sequence_number: op.sequenceNumber,
|
|
152
|
+
causal_deps: JSON.stringify(op.causalDeps),
|
|
153
|
+
schema_version: op.schemaVersion
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function deserializeOperation(row) {
|
|
157
|
+
let data = null;
|
|
158
|
+
let atomicOps;
|
|
159
|
+
let transactionId;
|
|
160
|
+
let mutationName;
|
|
161
|
+
if (row.data) {
|
|
162
|
+
const parsed = JSON.parse(row.data);
|
|
163
|
+
if (ATOMIC_OPS_KEY in parsed) {
|
|
164
|
+
atomicOps = parsed[ATOMIC_OPS_KEY];
|
|
165
|
+
}
|
|
166
|
+
if (TX_ID_KEY in parsed) {
|
|
167
|
+
transactionId = parsed[TX_ID_KEY];
|
|
168
|
+
}
|
|
169
|
+
if (MUTATION_NAME_KEY in parsed) {
|
|
170
|
+
mutationName = parsed[MUTATION_NAME_KEY];
|
|
171
|
+
}
|
|
172
|
+
const { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed;
|
|
173
|
+
data = Object.keys(rest).length > 0 ? rest : null;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
id: row.id,
|
|
177
|
+
nodeId: row.node_id,
|
|
178
|
+
type: row.type,
|
|
179
|
+
collection: "",
|
|
180
|
+
// Collection name is derived from the table name by the caller
|
|
181
|
+
recordId: row.record_id,
|
|
182
|
+
data,
|
|
183
|
+
previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
|
|
184
|
+
timestamp: import_core4.HybridLogicalClock.deserialize(row.timestamp),
|
|
185
|
+
sequenceNumber: row.sequence_number,
|
|
186
|
+
causalDeps: JSON.parse(row.causal_deps),
|
|
187
|
+
schemaVersion: row.schema_version,
|
|
188
|
+
...atomicOps !== void 0 ? { atomicOps } : {},
|
|
189
|
+
...transactionId !== void 0 ? { transactionId } : {},
|
|
190
|
+
...mutationName !== void 0 ? { mutationName } : {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function deserializeOperationWithCollection(row, collection) {
|
|
194
|
+
const op = deserializeOperation(row);
|
|
195
|
+
return { ...op, collection };
|
|
196
|
+
}
|
|
197
|
+
function serializeValue(value, descriptor) {
|
|
198
|
+
if (value === null || value === void 0) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
switch (descriptor.kind) {
|
|
202
|
+
case "boolean":
|
|
203
|
+
return value ? 1 : 0;
|
|
204
|
+
case "array":
|
|
205
|
+
return JSON.stringify(value);
|
|
206
|
+
case "richtext":
|
|
207
|
+
return encodeRichtext(value);
|
|
208
|
+
default:
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function deserializeValue(value, descriptor) {
|
|
213
|
+
switch (descriptor.kind) {
|
|
214
|
+
case "boolean":
|
|
215
|
+
return value === 1 || value === true;
|
|
216
|
+
case "array":
|
|
217
|
+
if (typeof value === "string") {
|
|
218
|
+
return JSON.parse(value);
|
|
219
|
+
}
|
|
220
|
+
return value;
|
|
221
|
+
case "richtext":
|
|
222
|
+
return decodeRichtext(value);
|
|
223
|
+
default:
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
var import_core4, ATOMIC_OPS_KEY, TX_ID_KEY, MUTATION_NAME_KEY;
|
|
228
|
+
var init_serializer = __esm({
|
|
229
|
+
"src/serialization/serializer.ts"() {
|
|
230
|
+
"use strict";
|
|
231
|
+
import_core4 = require("@korajs/core");
|
|
232
|
+
init_richtext_serializer();
|
|
233
|
+
ATOMIC_OPS_KEY = "__kora_atomic_ops__";
|
|
234
|
+
TX_ID_KEY = "__kora_tx_id__";
|
|
235
|
+
MUTATION_NAME_KEY = "__kora_mutation__";
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// src/backup/backup.ts
|
|
240
|
+
var backup_exports = {};
|
|
241
|
+
__export(backup_exports, {
|
|
242
|
+
exportBackup: () => exportBackup,
|
|
243
|
+
readBackupManifest: () => readBackupManifest,
|
|
244
|
+
restoreBackup: () => restoreBackup,
|
|
245
|
+
verifyBackupChecksum: () => verifyBackupChecksum
|
|
246
|
+
});
|
|
247
|
+
function encodeSection(name, content) {
|
|
248
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
249
|
+
const header = new Uint8Array(8);
|
|
250
|
+
const dv = new DataView(header.buffer);
|
|
251
|
+
dv.setUint32(0, nameBytes.length, true);
|
|
252
|
+
dv.setUint32(4, content.length, true);
|
|
253
|
+
const result = new Uint8Array(header.length + nameBytes.length + content.length);
|
|
254
|
+
result.set(header, 0);
|
|
255
|
+
result.set(nameBytes, 8);
|
|
256
|
+
result.set(content, 8 + nameBytes.length);
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
function encodeJsonSection(name, data) {
|
|
260
|
+
const json = JSON.stringify(data);
|
|
261
|
+
return encodeSection(name, new TextEncoder().encode(json));
|
|
262
|
+
}
|
|
263
|
+
function readUint32(view, offset) {
|
|
264
|
+
return view.getUint32(offset, true);
|
|
265
|
+
}
|
|
266
|
+
function parseSections(data) {
|
|
267
|
+
const sections = [];
|
|
268
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
269
|
+
let offset = 0;
|
|
270
|
+
while (offset + 8 <= data.byteLength) {
|
|
271
|
+
const nameLen = readUint32(dv, offset);
|
|
272
|
+
const contentLen = readUint32(dv, offset + 4);
|
|
273
|
+
offset += 8;
|
|
274
|
+
if (offset + nameLen + contentLen > data.byteLength) break;
|
|
275
|
+
const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
|
|
276
|
+
offset += nameLen;
|
|
277
|
+
const content = data.slice(offset, offset + contentLen);
|
|
278
|
+
offset += contentLen;
|
|
279
|
+
sections.push({ name, content });
|
|
280
|
+
}
|
|
281
|
+
return sections;
|
|
282
|
+
}
|
|
283
|
+
function findSection(sections, name) {
|
|
284
|
+
for (const s of sections) {
|
|
285
|
+
if (s.name === name) return s.content;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function parseJsonSection(sections, name) {
|
|
290
|
+
const content = findSection(sections, name);
|
|
291
|
+
if (!content) return null;
|
|
292
|
+
return JSON.parse(new TextDecoder().decode(content));
|
|
293
|
+
}
|
|
294
|
+
async function exportBackup(adapter, schema, nodeId, schemaVersion, options) {
|
|
295
|
+
const onProgress = options?.onProgress ?? (() => {
|
|
296
|
+
});
|
|
297
|
+
const includeRecords = options?.includeRecords ?? true;
|
|
298
|
+
const collectionFilter = options?.collections;
|
|
299
|
+
const collections = collectionFilter ? Object.keys(schema.collections).filter((c) => collectionFilter.includes(c)) : Object.keys(schema.collections);
|
|
300
|
+
onProgress({ phase: "reading", progress: 0, message: "Reading version vector" });
|
|
301
|
+
const versionVector = await readVersionVector(adapter);
|
|
302
|
+
onProgress({ phase: "reading", progress: 0.1, message: "Reading metadata" });
|
|
303
|
+
const meta = await readMeta(adapter);
|
|
304
|
+
onProgress({ phase: "reading", progress: 0.2, message: "Reading operations" });
|
|
305
|
+
const operations = await readAllOperations(adapter, schema);
|
|
306
|
+
onProgress({ phase: "reading", progress: 0.5, message: "Reading records" });
|
|
307
|
+
const sections = [];
|
|
308
|
+
let allContentForChecksum = new Uint8Array(0);
|
|
309
|
+
const addSection = (name, data) => {
|
|
310
|
+
sections.push(data);
|
|
311
|
+
const newLen = allContentForChecksum.length + data.length;
|
|
312
|
+
const combined = new Uint8Array(newLen);
|
|
313
|
+
combined.set(allContentForChecksum, 0);
|
|
314
|
+
combined.set(data, allContentForChecksum.length);
|
|
315
|
+
allContentForChecksum = combined;
|
|
316
|
+
};
|
|
317
|
+
const vvObj = {};
|
|
318
|
+
for (const [nid, seq] of versionVector) {
|
|
319
|
+
vvObj[nid] = seq;
|
|
320
|
+
}
|
|
321
|
+
addSection("version_vector", encodeJsonSection("version_vector", vvObj));
|
|
322
|
+
const metaObj = {};
|
|
323
|
+
for (const row of meta) {
|
|
324
|
+
metaObj[row.key] = row.value;
|
|
325
|
+
}
|
|
326
|
+
addSection("meta", encodeJsonSection("meta", metaObj));
|
|
327
|
+
const opLines = `${operations.map((op) => JSON.stringify(op)).join("\n")}
|
|
328
|
+
`;
|
|
329
|
+
addSection("operations", encodeSection("operations", new TextEncoder().encode(opLines)));
|
|
330
|
+
let totalRecords = 0;
|
|
331
|
+
if (includeRecords) {
|
|
332
|
+
for (const col of collections) {
|
|
333
|
+
const records = await readCollectionRecords(adapter, col);
|
|
334
|
+
if (records.length > 0) {
|
|
335
|
+
const lines = `${records.map((r) => JSON.stringify(r)).join("\n")}
|
|
336
|
+
`;
|
|
337
|
+
const sectionName = `records:${col}`;
|
|
338
|
+
addSection(sectionName, encodeSection(sectionName, new TextEncoder().encode(lines)));
|
|
339
|
+
totalRecords += records.length;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
onProgress({ phase: "reading", progress: 0.9, message: "Computing checksum" });
|
|
344
|
+
const checksumHex = await computeSha256(allContentForChecksum);
|
|
345
|
+
onProgress({ phase: "writing", progress: 1, message: "Finalizing" });
|
|
346
|
+
const manifest = {
|
|
347
|
+
version: BACKUP_VERSION,
|
|
348
|
+
createdAt: Date.now(),
|
|
349
|
+
nodeId,
|
|
350
|
+
schemaVersion,
|
|
351
|
+
operationCount: operations.length,
|
|
352
|
+
collections,
|
|
353
|
+
includesRecords: includeRecords,
|
|
354
|
+
checksum: checksumHex
|
|
355
|
+
};
|
|
356
|
+
const header = new Uint8Array(0);
|
|
357
|
+
const manifestSection = encodeJsonSection("manifest", manifest);
|
|
358
|
+
const checksumSection = encodeSection("checksum", new TextEncoder().encode(checksumHex));
|
|
359
|
+
const totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length;
|
|
360
|
+
const result = new Uint8Array(totalLen);
|
|
361
|
+
let pos = 0;
|
|
362
|
+
result.set(manifestSection, pos);
|
|
363
|
+
pos += manifestSection.length;
|
|
364
|
+
result.set(allContentForChecksum, pos);
|
|
365
|
+
pos += allContentForChecksum.length;
|
|
366
|
+
result.set(checksumSection, pos);
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
function readBackupManifest(data) {
|
|
370
|
+
const sections = parseSections(data);
|
|
371
|
+
const manifest = parseJsonSection(sections, "manifest");
|
|
372
|
+
if (!manifest) {
|
|
373
|
+
throw new Error("Invalid backup: manifest section not found");
|
|
374
|
+
}
|
|
375
|
+
return manifest;
|
|
376
|
+
}
|
|
377
|
+
async function verifyBackupChecksum(data) {
|
|
378
|
+
const sections = parseSections(data);
|
|
379
|
+
const manifest = parseJsonSection(sections, "manifest");
|
|
380
|
+
if (!manifest) return false;
|
|
381
|
+
const storedChecksum = manifest.checksum;
|
|
382
|
+
if (!storedChecksum) return false;
|
|
383
|
+
const manifestSection = findSection(sections, "manifest");
|
|
384
|
+
if (!manifestSection) return false;
|
|
385
|
+
const checksumSection = findSection(sections, "checksum");
|
|
386
|
+
const checksumBytes = checksumSection ? new TextEncoder().encode(manifest.checksum) : new Uint8Array(0);
|
|
387
|
+
const contentSections = sections.filter((s) => s.name !== "manifest" && s.name !== "checksum");
|
|
388
|
+
let contentForHash = new Uint8Array(0);
|
|
389
|
+
for (const section of contentSections) {
|
|
390
|
+
const sectionBytes = encodeSection(section.name, section.content);
|
|
391
|
+
const newLen = contentForHash.length + sectionBytes.length;
|
|
392
|
+
const combined = new Uint8Array(newLen);
|
|
393
|
+
combined.set(contentForHash, 0);
|
|
394
|
+
combined.set(sectionBytes, contentForHash.length);
|
|
395
|
+
contentForHash = combined;
|
|
396
|
+
}
|
|
397
|
+
const computed = await computeSha256(contentForHash);
|
|
398
|
+
return computed === storedChecksum;
|
|
399
|
+
}
|
|
400
|
+
async function restoreBackup(adapter, schema, data, options) {
|
|
401
|
+
const startTime = Date.now();
|
|
402
|
+
const onProgress = options?.onProgress ?? (() => {
|
|
403
|
+
});
|
|
404
|
+
const collectionFilter = options?.collections;
|
|
405
|
+
onProgress({ phase: "verifying", progress: 0, message: "Parsing backup" });
|
|
406
|
+
const sections = parseSections(data);
|
|
407
|
+
const manifest = parseJsonSection(sections, "manifest");
|
|
408
|
+
if (!manifest) {
|
|
409
|
+
return {
|
|
410
|
+
operationsRestored: 0,
|
|
411
|
+
recordsRestored: 0,
|
|
412
|
+
success: false,
|
|
413
|
+
error: "Invalid backup: manifest section not found",
|
|
414
|
+
duration: Date.now() - startTime
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (manifest.version !== BACKUP_VERSION) {
|
|
418
|
+
return {
|
|
419
|
+
operationsRestored: 0,
|
|
420
|
+
recordsRestored: 0,
|
|
421
|
+
success: false,
|
|
422
|
+
error: `Unsupported backup version: ${manifest.version}`,
|
|
423
|
+
duration: Date.now() - startTime
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
onProgress({ phase: "verifying", progress: 0.2, message: "Verifying checksum" });
|
|
427
|
+
const valid = await verifyBackupChecksum(data);
|
|
428
|
+
if (!valid) {
|
|
429
|
+
return {
|
|
430
|
+
operationsRestored: 0,
|
|
431
|
+
recordsRestored: 0,
|
|
432
|
+
success: false,
|
|
433
|
+
error: "Backup checksum mismatch: data may be corrupted",
|
|
434
|
+
duration: Date.now() - startTime
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
onProgress({ phase: "restoring", progress: 0.3, message: "Loading operations" });
|
|
438
|
+
const opsContent = findSection(sections, "operations");
|
|
439
|
+
let operations = [];
|
|
440
|
+
if (opsContent) {
|
|
441
|
+
const text = new TextDecoder().decode(opsContent);
|
|
442
|
+
const lines = text.trim().split("\n").filter((l) => l.length > 0);
|
|
443
|
+
operations = lines.map((line) => JSON.parse(line));
|
|
444
|
+
}
|
|
445
|
+
if (collectionFilter) {
|
|
446
|
+
operations = operations.filter((op) => collectionFilter.includes(op.collection));
|
|
447
|
+
}
|
|
448
|
+
const vvData = parseJsonSection(sections, "version_vector");
|
|
449
|
+
const metaData = parseJsonSection(sections, "meta");
|
|
450
|
+
onProgress({ phase: "restoring", progress: 0.4, message: "Applying operations" });
|
|
451
|
+
if (options?.merge) {
|
|
452
|
+
await adapter.transaction(async (tx) => {
|
|
453
|
+
if (vvData) {
|
|
454
|
+
for (const [nid, seq] of Object.entries(vvData)) {
|
|
455
|
+
await tx.execute(
|
|
456
|
+
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
457
|
+
[nid, seq]
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (metaData) {
|
|
462
|
+
for (const [key, value] of Object.entries(metaData)) {
|
|
463
|
+
await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
464
|
+
key,
|
|
465
|
+
value
|
|
466
|
+
]);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
for (const op of operations) {
|
|
470
|
+
const collection = op.collection;
|
|
471
|
+
const opRow = {
|
|
472
|
+
id: op.id,
|
|
473
|
+
node_id: op.nodeId,
|
|
474
|
+
type: op.type,
|
|
475
|
+
record_id: op.recordId,
|
|
476
|
+
data: op.data !== null ? JSON.stringify(op.data) : null,
|
|
477
|
+
previous_data: op.previousData !== null ? JSON.stringify(op.previousData) : null,
|
|
478
|
+
timestamp: JSON.stringify(op.timestamp),
|
|
479
|
+
sequence_number: op.sequenceNumber,
|
|
480
|
+
causal_deps: JSON.stringify(op.causalDeps),
|
|
481
|
+
schema_version: op.schemaVersion
|
|
482
|
+
};
|
|
483
|
+
await tx.execute(
|
|
484
|
+
`INSERT OR IGNORE INTO _kora_ops_${collection} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
485
|
+
[
|
|
486
|
+
opRow.id,
|
|
487
|
+
opRow.node_id,
|
|
488
|
+
opRow.type,
|
|
489
|
+
opRow.record_id,
|
|
490
|
+
opRow.data,
|
|
491
|
+
opRow.previous_data,
|
|
492
|
+
opRow.timestamp,
|
|
493
|
+
opRow.sequence_number,
|
|
494
|
+
opRow.causal_deps,
|
|
495
|
+
opRow.schema_version
|
|
496
|
+
]
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
} else {
|
|
501
|
+
await adapter.transaction(async (tx) => {
|
|
502
|
+
for (const collection of Object.keys(schema.collections)) {
|
|
503
|
+
await tx.execute(`DELETE FROM _kora_ops_${collection}`);
|
|
504
|
+
await tx.execute(`DELETE FROM ${collection}`);
|
|
505
|
+
}
|
|
506
|
+
await tx.execute("DELETE FROM _kora_version_vector");
|
|
507
|
+
await tx.execute("DELETE FROM _kora_meta");
|
|
508
|
+
if (vvData) {
|
|
509
|
+
for (const [nid, seq] of Object.entries(vvData)) {
|
|
510
|
+
await tx.execute(
|
|
511
|
+
"INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
512
|
+
[nid, seq]
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (metaData) {
|
|
517
|
+
for (const [key, value] of Object.entries(metaData)) {
|
|
518
|
+
await tx.execute("INSERT INTO _kora_meta (key, value) VALUES (?, ?)", [key, value]);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
for (const op of operations) {
|
|
522
|
+
const collection = op.collection;
|
|
523
|
+
await tx.execute(
|
|
524
|
+
`INSERT INTO _kora_ops_${collection} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
525
|
+
[
|
|
526
|
+
op.id,
|
|
527
|
+
op.nodeId,
|
|
528
|
+
op.type,
|
|
529
|
+
op.recordId,
|
|
530
|
+
op.data !== null ? JSON.stringify(op.data) : null,
|
|
531
|
+
op.previousData !== null ? JSON.stringify(op.previousData) : null,
|
|
532
|
+
JSON.stringify(op.timestamp),
|
|
533
|
+
op.sequenceNumber,
|
|
534
|
+
JSON.stringify(op.causalDeps),
|
|
535
|
+
op.schemaVersion
|
|
536
|
+
]
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
onProgress({ phase: "restoring", progress: 0.8, message: "Restoring records" });
|
|
542
|
+
let recordsRestored = 0;
|
|
543
|
+
if (manifest.includesRecords) {
|
|
544
|
+
for (const col of manifest.collections) {
|
|
545
|
+
if (collectionFilter && !collectionFilter.includes(col)) continue;
|
|
546
|
+
const sectionContent = findSection(sections, `records:${col}`);
|
|
547
|
+
if (!sectionContent) continue;
|
|
548
|
+
const text = new TextDecoder().decode(sectionContent);
|
|
549
|
+
const lines = text.trim().split("\n").filter((l) => l.length > 0);
|
|
550
|
+
const records = lines.map((line) => JSON.parse(line));
|
|
551
|
+
if (records.length === 0) continue;
|
|
552
|
+
await adapter.transaction(async (tx) => {
|
|
553
|
+
for (const record of records) {
|
|
554
|
+
const keys = Object.keys(record);
|
|
555
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
556
|
+
const values = keys.map((k) => {
|
|
557
|
+
const v = record[k];
|
|
558
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
559
|
+
if (Array.isArray(v) || typeof v === "object" && v !== null) return JSON.stringify(v);
|
|
560
|
+
return v;
|
|
561
|
+
});
|
|
562
|
+
await tx.execute(
|
|
563
|
+
`INSERT OR REPLACE INTO ${col} (${keys.join(", ")}) VALUES (${placeholders})`,
|
|
564
|
+
values
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
recordsRestored += records.length;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
onProgress({ phase: "restoring", progress: 1, message: "Done" });
|
|
572
|
+
return {
|
|
573
|
+
operationsRestored: operations.length,
|
|
574
|
+
recordsRestored,
|
|
575
|
+
success: true,
|
|
576
|
+
duration: Date.now() - startTime
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async function readVersionVector(adapter) {
|
|
580
|
+
const rows = await adapter.query(
|
|
581
|
+
"SELECT node_id, sequence_number FROM _kora_version_vector"
|
|
582
|
+
);
|
|
583
|
+
const vv = /* @__PURE__ */ new Map();
|
|
584
|
+
for (const row of rows) {
|
|
585
|
+
vv.set(row.node_id, row.sequence_number);
|
|
586
|
+
}
|
|
587
|
+
return vv;
|
|
588
|
+
}
|
|
589
|
+
async function readMeta(adapter) {
|
|
590
|
+
return adapter.query("SELECT key, value FROM _kora_meta");
|
|
591
|
+
}
|
|
592
|
+
async function readAllOperations(adapter, schema) {
|
|
593
|
+
const allOps = [];
|
|
594
|
+
for (const collectionName of Object.keys(schema.collections)) {
|
|
595
|
+
const rows = await adapter.query(
|
|
596
|
+
`SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
|
|
597
|
+
);
|
|
598
|
+
for (const row of rows) {
|
|
599
|
+
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return allOps;
|
|
603
|
+
}
|
|
604
|
+
async function readCollectionRecords(adapter, collection) {
|
|
605
|
+
return adapter.query(`SELECT * FROM ${collection} WHERE _deleted = 0`);
|
|
606
|
+
}
|
|
607
|
+
async function computeSha256(data) {
|
|
608
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
609
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
610
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
611
|
+
}
|
|
612
|
+
var import_core5, BACKUP_VERSION;
|
|
613
|
+
var init_backup = __esm({
|
|
614
|
+
"src/backup/backup.ts"() {
|
|
615
|
+
"use strict";
|
|
616
|
+
import_core5 = require("@korajs/core");
|
|
617
|
+
init_serializer();
|
|
618
|
+
BACKUP_VERSION = 1;
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// src/audit/audit-json.ts
|
|
623
|
+
function auditJsonReplacer(_key, value) {
|
|
624
|
+
if (value instanceof Uint8Array) {
|
|
625
|
+
let binary = "";
|
|
626
|
+
for (let i = 0; i < value.length; i++) {
|
|
627
|
+
binary += String.fromCharCode(value[i]);
|
|
628
|
+
}
|
|
629
|
+
return { [WIRE_BYTES_KEY]: btoa(binary) };
|
|
630
|
+
}
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
function auditJsonReviver(_key, value) {
|
|
634
|
+
if (value !== null && typeof value === "object" && WIRE_BYTES_KEY in value && typeof value[WIRE_BYTES_KEY] === "string") {
|
|
635
|
+
const encoded = value[WIRE_BYTES_KEY];
|
|
636
|
+
if (typeof encoded !== "string") {
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
const binary = atob(encoded);
|
|
640
|
+
const bytes = new Uint8Array(binary.length);
|
|
641
|
+
for (let i = 0; i < binary.length; i++) {
|
|
642
|
+
bytes[i] = binary.charCodeAt(i);
|
|
643
|
+
}
|
|
644
|
+
return bytes;
|
|
645
|
+
}
|
|
646
|
+
return value;
|
|
647
|
+
}
|
|
648
|
+
function serializeAuditJson(value) {
|
|
649
|
+
return JSON.stringify(value, auditJsonReplacer);
|
|
650
|
+
}
|
|
651
|
+
function deserializeAuditJson(json) {
|
|
652
|
+
return JSON.parse(json, auditJsonReviver);
|
|
653
|
+
}
|
|
654
|
+
var WIRE_BYTES_KEY;
|
|
655
|
+
var init_audit_json = __esm({
|
|
656
|
+
"src/audit/audit-json.ts"() {
|
|
657
|
+
"use strict";
|
|
658
|
+
WIRE_BYTES_KEY = "__kora_bytes__";
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// src/audit/audit-trace-store.ts
|
|
663
|
+
var audit_trace_store_exports = {};
|
|
664
|
+
__export(audit_trace_store_exports, {
|
|
665
|
+
appendAuditTrace: () => appendAuditTrace,
|
|
666
|
+
auditTraceIndexFields: () => auditTraceIndexFields,
|
|
667
|
+
countAuditTraces: () => countAuditTraces,
|
|
668
|
+
persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
|
|
669
|
+
readAuditTraces: () => readAuditTraces
|
|
670
|
+
});
|
|
671
|
+
function persistedAuditTraceFromEvent(event) {
|
|
672
|
+
return {
|
|
673
|
+
id: (0, import_core16.generateUUIDv7)(),
|
|
674
|
+
recordedAt: Date.now(),
|
|
675
|
+
eventType: event.type,
|
|
676
|
+
constraint: event.type === "constraint:violated" ? event.constraint : void 0,
|
|
677
|
+
trace: event.trace
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async function appendAuditTrace(adapter, trace) {
|
|
681
|
+
const collection = trace.trace.operationA.collection;
|
|
682
|
+
const recordId = trace.trace.operationA.recordId;
|
|
683
|
+
await adapter.execute(
|
|
684
|
+
`INSERT INTO _kora_audit_traces (
|
|
685
|
+
id, recorded_at, event_type, collection, record_id, field, strategy, tier, constraint_name, trace_json
|
|
686
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
687
|
+
[
|
|
688
|
+
trace.id,
|
|
689
|
+
trace.recordedAt,
|
|
690
|
+
trace.eventType,
|
|
691
|
+
collection,
|
|
692
|
+
recordId,
|
|
693
|
+
trace.trace.field,
|
|
694
|
+
trace.trace.strategy,
|
|
695
|
+
trace.trace.tier,
|
|
696
|
+
trace.constraint ?? null,
|
|
697
|
+
serializeAuditJson(trace)
|
|
698
|
+
]
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
async function readAuditTraces(adapter, query) {
|
|
702
|
+
const conditions = [];
|
|
703
|
+
const params = [];
|
|
704
|
+
if (query?.collections && query.collections.length > 0) {
|
|
705
|
+
const placeholders = query.collections.map(() => "?").join(", ");
|
|
706
|
+
conditions.push(`collection IN (${placeholders})`);
|
|
707
|
+
params.push(...query.collections);
|
|
708
|
+
}
|
|
709
|
+
if (query?.since !== void 0) {
|
|
710
|
+
conditions.push("recorded_at >= ?");
|
|
711
|
+
params.push(query.since);
|
|
712
|
+
}
|
|
713
|
+
if (query?.until !== void 0) {
|
|
714
|
+
conditions.push("recorded_at <= ?");
|
|
715
|
+
params.push(query.until);
|
|
716
|
+
}
|
|
717
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
718
|
+
const rows = await adapter.query(
|
|
719
|
+
`SELECT * FROM _kora_audit_traces ${whereClause} ORDER BY recorded_at ASC, id ASC`,
|
|
720
|
+
params
|
|
721
|
+
);
|
|
722
|
+
return rows.map(rowToPersistedTrace);
|
|
723
|
+
}
|
|
724
|
+
function rowToPersistedTrace(row) {
|
|
725
|
+
return deserializeAuditJson(row.trace_json);
|
|
726
|
+
}
|
|
727
|
+
async function countAuditTraces(adapter) {
|
|
728
|
+
const rows = await adapter.query(
|
|
729
|
+
"SELECT COUNT(*) as count FROM _kora_audit_traces"
|
|
730
|
+
);
|
|
731
|
+
return rows[0]?.count ?? 0;
|
|
732
|
+
}
|
|
733
|
+
function auditTraceIndexFields(trace) {
|
|
734
|
+
return {
|
|
735
|
+
collection: trace.operationA.collection,
|
|
736
|
+
recordId: trace.operationA.recordId
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
var import_core16;
|
|
740
|
+
var init_audit_trace_store = __esm({
|
|
741
|
+
"src/audit/audit-trace-store.ts"() {
|
|
742
|
+
"use strict";
|
|
743
|
+
import_core16 = require("@korajs/core");
|
|
744
|
+
init_audit_json();
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// src/audit/export-audit.ts
|
|
749
|
+
var export_audit_exports = {};
|
|
750
|
+
__export(export_audit_exports, {
|
|
751
|
+
decodeAuditExport: () => decodeAuditExport,
|
|
752
|
+
exportAudit: () => exportAudit,
|
|
753
|
+
readAuditExportManifest: () => readAuditExportManifest,
|
|
754
|
+
verifyAuditExportChecksum: () => verifyAuditExportChecksum
|
|
755
|
+
});
|
|
756
|
+
function encodeSection2(name, content) {
|
|
757
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
758
|
+
const header = new Uint8Array(8);
|
|
759
|
+
const view = new DataView(header.buffer);
|
|
760
|
+
view.setUint32(0, nameBytes.length, true);
|
|
761
|
+
view.setUint32(4, content.length, true);
|
|
762
|
+
const result = new Uint8Array(header.length + nameBytes.length + content.length);
|
|
763
|
+
result.set(header, 0);
|
|
764
|
+
result.set(nameBytes, 8);
|
|
765
|
+
result.set(content, 8 + nameBytes.length);
|
|
766
|
+
return result;
|
|
767
|
+
}
|
|
768
|
+
function encodeJsonSection2(name, data) {
|
|
769
|
+
return encodeSection2(name, new TextEncoder().encode(serializeAuditJson(data)));
|
|
770
|
+
}
|
|
771
|
+
function encodeNdjsonSection(name, records) {
|
|
772
|
+
const lines = `${records.map((record) => serializeAuditJson(record)).join("\n")}
|
|
773
|
+
`;
|
|
774
|
+
return encodeSection2(name, new TextEncoder().encode(lines));
|
|
775
|
+
}
|
|
776
|
+
function readUint322(view, offset) {
|
|
777
|
+
return view.getUint32(offset, true);
|
|
778
|
+
}
|
|
779
|
+
function parseSections2(data) {
|
|
780
|
+
const sections = [];
|
|
781
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
782
|
+
let offset = 0;
|
|
783
|
+
while (offset + 8 <= data.byteLength) {
|
|
784
|
+
const nameLen = readUint322(view, offset);
|
|
785
|
+
const contentLen = readUint322(view, offset + 4);
|
|
786
|
+
offset += 8;
|
|
787
|
+
if (offset + nameLen + contentLen > data.byteLength) {
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
|
|
791
|
+
offset += nameLen;
|
|
792
|
+
const content = data.slice(offset, offset + contentLen);
|
|
793
|
+
offset += contentLen;
|
|
794
|
+
sections.push({ name, content });
|
|
795
|
+
}
|
|
796
|
+
return sections;
|
|
797
|
+
}
|
|
798
|
+
function findSection2(sections, name) {
|
|
799
|
+
for (const section of sections) {
|
|
800
|
+
if (section.name === name) {
|
|
801
|
+
return section.content;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
async function computeSha2562(data) {
|
|
807
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
808
|
+
return Array.from(new Uint8Array(hashBuffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
809
|
+
}
|
|
810
|
+
async function readAllOperations2(adapter, schema, collectionFilter) {
|
|
811
|
+
const collections = collectionFilter ? Object.keys(schema.collections).filter((name) => collectionFilter.includes(name)) : Object.keys(schema.collections);
|
|
812
|
+
const allOps = [];
|
|
813
|
+
for (const collectionName of collections) {
|
|
814
|
+
const rows = await adapter.query(
|
|
815
|
+
`SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
|
|
816
|
+
);
|
|
817
|
+
for (const row of rows) {
|
|
818
|
+
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return allOps;
|
|
822
|
+
}
|
|
823
|
+
function filterOperationsByCollections(operations, collectionFilter) {
|
|
824
|
+
if (!collectionFilter || collectionFilter.length === 0) {
|
|
825
|
+
return operations;
|
|
826
|
+
}
|
|
827
|
+
const allowed = new Set(collectionFilter);
|
|
828
|
+
return operations.filter((op) => allowed.has(op.collection));
|
|
829
|
+
}
|
|
830
|
+
async function exportAudit(adapter, schema, nodeId, schemaVersion, options) {
|
|
831
|
+
const onProgress = options?.onProgress ?? (() => {
|
|
832
|
+
});
|
|
833
|
+
const traceQuery = {
|
|
834
|
+
collections: options?.collections,
|
|
835
|
+
since: options?.since,
|
|
836
|
+
until: options?.until
|
|
837
|
+
};
|
|
838
|
+
onProgress({ phase: "reading", progress: 0, message: "Reading operations" });
|
|
839
|
+
const operations = filterOperationsByCollections(
|
|
840
|
+
await readAllOperations2(adapter, schema, options?.collections),
|
|
841
|
+
options?.collections
|
|
842
|
+
);
|
|
843
|
+
onProgress({ phase: "reading", progress: 0.4, message: "Reading merge traces" });
|
|
844
|
+
const mergeTraces = await readAuditTraces(adapter, traceQuery);
|
|
845
|
+
onProgress({ phase: "writing", progress: 0.7, message: "Encoding audit export" });
|
|
846
|
+
const sections = [];
|
|
847
|
+
let contentForChecksum = new Uint8Array(0);
|
|
848
|
+
const addSection = (section) => {
|
|
849
|
+
sections.push(section);
|
|
850
|
+
const combined = new Uint8Array(contentForChecksum.length + section.length);
|
|
851
|
+
combined.set(contentForChecksum, 0);
|
|
852
|
+
combined.set(section, contentForChecksum.length);
|
|
853
|
+
contentForChecksum = combined;
|
|
854
|
+
};
|
|
855
|
+
addSection(encodeNdjsonSection("operations", operations));
|
|
856
|
+
addSection(encodeNdjsonSection("merge_traces", mergeTraces));
|
|
857
|
+
onProgress({ phase: "verifying", progress: 0.9, message: "Computing checksum" });
|
|
858
|
+
const checksum = await computeSha2562(contentForChecksum);
|
|
859
|
+
const manifest = {
|
|
860
|
+
version: 1,
|
|
861
|
+
exportedAt: Date.now(),
|
|
862
|
+
nodeId,
|
|
863
|
+
schemaVersion,
|
|
864
|
+
operationCount: operations.length,
|
|
865
|
+
mergeTraceCount: mergeTraces.length,
|
|
866
|
+
checksum
|
|
867
|
+
};
|
|
868
|
+
const manifestSection = encodeJsonSection2("manifest", manifest);
|
|
869
|
+
const checksumSection = encodeSection2("checksum", new TextEncoder().encode(checksum));
|
|
870
|
+
onProgress({ phase: "writing", progress: 1, message: "Finalizing" });
|
|
871
|
+
const parts = [manifestSection, ...sections, checksumSection];
|
|
872
|
+
const finalOutput = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
873
|
+
let offset = 0;
|
|
874
|
+
for (const part of parts) {
|
|
875
|
+
finalOutput.set(part, offset);
|
|
876
|
+
offset += part.length;
|
|
877
|
+
}
|
|
878
|
+
return finalOutput;
|
|
879
|
+
}
|
|
880
|
+
function decodeAuditExport(data) {
|
|
881
|
+
const sections = parseSections2(data);
|
|
882
|
+
const manifest = JSON.parse(
|
|
883
|
+
new TextDecoder().decode(findSection2(sections, "manifest") ?? new Uint8Array())
|
|
884
|
+
);
|
|
885
|
+
const operationsContent = findSection2(sections, "operations");
|
|
886
|
+
const mergeTracesContent = findSection2(sections, "merge_traces");
|
|
887
|
+
const operations = parseNdjsonSection(operationsContent);
|
|
888
|
+
const mergeTraces = parseNdjsonSection(mergeTracesContent);
|
|
889
|
+
return { manifest, operations, mergeTraces };
|
|
890
|
+
}
|
|
891
|
+
function readAuditExportManifest(data) {
|
|
892
|
+
const sections = parseSections2(data);
|
|
893
|
+
const manifestContent = findSection2(sections, "manifest");
|
|
894
|
+
if (!manifestContent) {
|
|
895
|
+
throw new Error("Invalid audit export: missing manifest section");
|
|
896
|
+
}
|
|
897
|
+
return JSON.parse(new TextDecoder().decode(manifestContent));
|
|
898
|
+
}
|
|
899
|
+
async function verifyAuditExportChecksum(data) {
|
|
900
|
+
const sections = parseSections2(data);
|
|
901
|
+
const manifest = readAuditExportManifest(data);
|
|
902
|
+
const contentSections = sections.filter(
|
|
903
|
+
(section) => section.name !== "manifest" && section.name !== "checksum"
|
|
904
|
+
);
|
|
905
|
+
let content = new Uint8Array(0);
|
|
906
|
+
for (const section of contentSections) {
|
|
907
|
+
const encoded = encodeSection2(section.name, section.content);
|
|
908
|
+
const combined = new Uint8Array(content.length + encoded.length);
|
|
909
|
+
combined.set(content, 0);
|
|
910
|
+
combined.set(encoded, content.length);
|
|
911
|
+
content = combined;
|
|
912
|
+
}
|
|
913
|
+
const checksum = await computeSha2562(content);
|
|
914
|
+
return checksum === manifest.checksum;
|
|
915
|
+
}
|
|
916
|
+
function parseNdjsonSection(content) {
|
|
917
|
+
if (!content || content.length === 0) {
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
const text = new TextDecoder().decode(content).trim();
|
|
921
|
+
if (text.length === 0) {
|
|
922
|
+
return [];
|
|
923
|
+
}
|
|
924
|
+
return text.split("\n").map((line) => deserializeAuditJson(line));
|
|
925
|
+
}
|
|
926
|
+
var init_export_audit = __esm({
|
|
927
|
+
"src/audit/export-audit.ts"() {
|
|
928
|
+
"use strict";
|
|
929
|
+
init_serializer();
|
|
930
|
+
init_audit_json();
|
|
931
|
+
init_audit_trace_store();
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
|
|
30
935
|
// src/index.ts
|
|
31
936
|
var index_exports = {};
|
|
32
937
|
__export(index_exports, {
|
|
33
938
|
AdapterError: () => AdapterError,
|
|
939
|
+
COMPACTION_BASELINE_META_KEY: () => COMPACTION_BASELINE_META_KEY,
|
|
34
940
|
Collection: () => Collection,
|
|
35
941
|
InvalidStateTransitionError: () => InvalidStateTransitionError,
|
|
942
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY: () => LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
36
943
|
PersistenceError: () => PersistenceError,
|
|
37
944
|
QueryBuilder: () => QueryBuilder,
|
|
38
945
|
QueryError: () => QueryError,
|
|
946
|
+
QueryStore: () => QueryStore,
|
|
947
|
+
QueryStoreCache: () => QueryStoreCache,
|
|
39
948
|
RecordNotFoundError: () => RecordNotFoundError,
|
|
40
949
|
SequenceManager: () => SequenceManager,
|
|
41
950
|
Store: () => Store,
|
|
@@ -45,13 +954,32 @@ __export(index_exports, {
|
|
|
45
954
|
TransactionContext: () => TransactionContext,
|
|
46
955
|
WorkerInitError: () => WorkerInitError,
|
|
47
956
|
WorkerTimeoutError: () => WorkerTimeoutError,
|
|
957
|
+
asRichTextSyncEngine: () => asRichTextSyncEngine,
|
|
958
|
+
assertQueryReady: () => assertQueryReady,
|
|
959
|
+
collectOperationsAheadOfServer: () => collectOperationsAheadOfServer,
|
|
960
|
+
compactOperationLog: () => compactOperationLog,
|
|
961
|
+
computeAckCompactionWatermark: () => computeAckCompactionWatermark,
|
|
962
|
+
createRichTextController: () => createRichTextController,
|
|
963
|
+
decodeAuditExport: () => decodeAuditExport,
|
|
48
964
|
decodeRichtext: () => decodeRichtext,
|
|
965
|
+
deserializeVersionVectorFromMeta: () => deserializeVersionVectorFromMeta,
|
|
49
966
|
encodeRichtext: () => encodeRichtext,
|
|
967
|
+
exportBackup: () => exportBackup,
|
|
968
|
+
getSharedQueryStoreCache: () => getSharedQueryStoreCache,
|
|
969
|
+
mergeVersionVectors: () => mergeVersionVectors,
|
|
970
|
+
persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
|
|
50
971
|
pluralize: () => pluralize,
|
|
972
|
+
readAuditExportManifest: () => readAuditExportManifest,
|
|
973
|
+
readBackupManifest: () => readBackupManifest,
|
|
974
|
+
restoreBackup: () => restoreBackup,
|
|
975
|
+
richtextStatesEqual: () => richtextStatesEqual,
|
|
51
976
|
richtextToPlainText: () => richtextToPlainText,
|
|
977
|
+
serializeVersionVectorToMeta: () => serializeVersionVectorToMeta,
|
|
52
978
|
singularize: () => singularize,
|
|
53
979
|
validateStateTransition: () => validateStateTransition,
|
|
54
|
-
validateUpdateStateMachine: () => validateUpdateStateMachine
|
|
980
|
+
validateUpdateStateMachine: () => validateUpdateStateMachine,
|
|
981
|
+
verifyAuditExportChecksum: () => verifyAuditExportChecksum,
|
|
982
|
+
verifyBackupChecksum: () => verifyBackupChecksum
|
|
55
983
|
});
|
|
56
984
|
module.exports = __toCommonJS(index_exports);
|
|
57
985
|
|
|
@@ -107,11 +1035,175 @@ var PersistenceError = class extends import_core.KoraError {
|
|
|
107
1035
|
}
|
|
108
1036
|
};
|
|
109
1037
|
|
|
1038
|
+
// src/compaction/types.ts
|
|
1039
|
+
var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
|
|
1040
|
+
|
|
1041
|
+
// src/compaction/compact-operation-log.ts
|
|
1042
|
+
var import_core2 = require("@korajs/core");
|
|
1043
|
+
function computeAckCompactionWatermark(serverVector) {
|
|
1044
|
+
const watermark = (0, import_core2.createVersionVector)();
|
|
1045
|
+
for (const [nodeId, seq] of serverVector) {
|
|
1046
|
+
if (seq > 0) {
|
|
1047
|
+
watermark.set(nodeId, seq);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return watermark;
|
|
1051
|
+
}
|
|
1052
|
+
async function compactOperationLog(adapter, schema, strategy, serverVector) {
|
|
1053
|
+
if (strategy.mode === "never") {
|
|
1054
|
+
return { deletedCount: 0, watermark: (0, import_core2.createVersionVector)() };
|
|
1055
|
+
}
|
|
1056
|
+
const watermark = computeAckCompactionWatermark(serverVector);
|
|
1057
|
+
if (watermark.size === 0) {
|
|
1058
|
+
return { deletedCount: 0, watermark };
|
|
1059
|
+
}
|
|
1060
|
+
const ageCutoffWall = strategy.mode === "after-days" ? Date.now() - strategy.days * 24 * 60 * 60 * 1e3 : null;
|
|
1061
|
+
const ageCutoffSerialized = ageCutoffWall !== null ? import_core2.HybridLogicalClock.serialize({
|
|
1062
|
+
wallTime: ageCutoffWall,
|
|
1063
|
+
logical: 0,
|
|
1064
|
+
nodeId: ""
|
|
1065
|
+
}).slice(0, 15) : null;
|
|
1066
|
+
let deletedCount = 0;
|
|
1067
|
+
const collectionNames = Object.keys(schema.collections);
|
|
1068
|
+
await adapter.transaction(async (tx) => {
|
|
1069
|
+
for (const collectionName of collectionNames) {
|
|
1070
|
+
const table = `_kora_ops_${collectionName}`;
|
|
1071
|
+
for (const [nodeId, maxSeq] of watermark) {
|
|
1072
|
+
if (maxSeq <= 0) continue;
|
|
1073
|
+
const countSql = buildCountSql(table, ageCutoffSerialized !== null);
|
|
1074
|
+
const countParams = ageCutoffSerialized !== null ? [nodeId, maxSeq, ageCutoffSerialized] : [nodeId, maxSeq];
|
|
1075
|
+
const countRows = await tx.query(countSql, countParams);
|
|
1076
|
+
deletedCount += countRows[0]?.count ?? 0;
|
|
1077
|
+
const deleteSql = buildDeleteSql(table, ageCutoffSerialized !== null);
|
|
1078
|
+
await tx.execute(deleteSql, countParams);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
1082
|
+
COMPACTION_BASELINE_META_KEY,
|
|
1083
|
+
String(Date.now())
|
|
1084
|
+
]);
|
|
1085
|
+
});
|
|
1086
|
+
return { deletedCount, watermark };
|
|
1087
|
+
}
|
|
1088
|
+
function buildCountSql(table, withAge) {
|
|
1089
|
+
if (withAge) {
|
|
1090
|
+
return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
|
|
1091
|
+
}
|
|
1092
|
+
return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
|
|
1093
|
+
}
|
|
1094
|
+
function buildDeleteSql(table, withAge) {
|
|
1095
|
+
if (withAge) {
|
|
1096
|
+
return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
|
|
1097
|
+
}
|
|
1098
|
+
return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/sync/sync-state.ts
|
|
1102
|
+
var import_core3 = require("@korajs/core");
|
|
1103
|
+
var LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
|
|
1104
|
+
var DELTA_CURSOR_META_KEY = "delta_cursor";
|
|
1105
|
+
function serializeVersionVectorToMeta(vector) {
|
|
1106
|
+
const record = {};
|
|
1107
|
+
for (const [nodeId, seq] of vector) {
|
|
1108
|
+
record[nodeId] = seq;
|
|
1109
|
+
}
|
|
1110
|
+
return JSON.stringify(record);
|
|
1111
|
+
}
|
|
1112
|
+
function deserializeVersionVectorFromMeta(value) {
|
|
1113
|
+
const parsed = JSON.parse(value);
|
|
1114
|
+
const vector = (0, import_core3.createVersionVector)();
|
|
1115
|
+
for (const [nodeId, seq] of Object.entries(parsed)) {
|
|
1116
|
+
if (typeof seq === "number" && seq >= 0) {
|
|
1117
|
+
vector.set(nodeId, seq);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return vector;
|
|
1121
|
+
}
|
|
1122
|
+
function mergeVersionVectors(a, b) {
|
|
1123
|
+
const merged = new Map(a);
|
|
1124
|
+
for (const [nodeId, seq] of b) {
|
|
1125
|
+
merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, seq));
|
|
1126
|
+
}
|
|
1127
|
+
return merged;
|
|
1128
|
+
}
|
|
1129
|
+
async function collectOperationsAheadOfServer(localVector, serverVector, fetchRange) {
|
|
1130
|
+
const missing = [];
|
|
1131
|
+
for (const [nodeId, localSeq] of localVector) {
|
|
1132
|
+
const serverSeq = serverVector.get(nodeId) ?? 0;
|
|
1133
|
+
if (localSeq > serverSeq) {
|
|
1134
|
+
const ops = await fetchRange(nodeId, serverSeq + 1, localSeq);
|
|
1135
|
+
missing.push(...ops);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
missing.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
1139
|
+
return missing;
|
|
1140
|
+
}
|
|
1141
|
+
async function loadLastAckedServerVector(adapter) {
|
|
1142
|
+
const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
|
|
1143
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY
|
|
1144
|
+
]);
|
|
1145
|
+
const row = rows[0];
|
|
1146
|
+
if (!row?.value) {
|
|
1147
|
+
return (0, import_core3.createVersionVector)();
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
return deserializeVersionVectorFromMeta(row.value);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return (0, import_core3.createVersionVector)();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async function saveLastAckedServerVector(adapter, vector) {
|
|
1156
|
+
await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
1157
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
1158
|
+
serializeVersionVectorToMeta(vector)
|
|
1159
|
+
]);
|
|
1160
|
+
}
|
|
1161
|
+
async function loadDeltaCursor(adapter) {
|
|
1162
|
+
const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
|
|
1163
|
+
DELTA_CURSOR_META_KEY
|
|
1164
|
+
]);
|
|
1165
|
+
return rows[0]?.value ?? null;
|
|
1166
|
+
}
|
|
1167
|
+
async function saveDeltaCursor(adapter, cursor) {
|
|
1168
|
+
if (cursor === null) {
|
|
1169
|
+
await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [DELTA_CURSOR_META_KEY]);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
1173
|
+
DELTA_CURSOR_META_KEY,
|
|
1174
|
+
cursor
|
|
1175
|
+
]);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
110
1178
|
// src/store/store.ts
|
|
111
|
-
var
|
|
1179
|
+
var import_core17 = require("@korajs/core");
|
|
1180
|
+
init_backup();
|
|
112
1181
|
|
|
113
|
-
// src/
|
|
114
|
-
var
|
|
1182
|
+
// src/mutations/execute-delete.ts
|
|
1183
|
+
var import_core7 = require("@korajs/core");
|
|
1184
|
+
|
|
1185
|
+
// src/lww/row-version.ts
|
|
1186
|
+
var import_core6 = require("@korajs/core");
|
|
1187
|
+
var EMPTY_ROW_VERSION = "";
|
|
1188
|
+
function serializeRowVersion(timestamp) {
|
|
1189
|
+
return import_core6.HybridLogicalClock.serialize(timestamp);
|
|
1190
|
+
}
|
|
1191
|
+
function rowVersionFromRecord(row) {
|
|
1192
|
+
const version = row._version;
|
|
1193
|
+
if (typeof version === "string" && version.length > 0) {
|
|
1194
|
+
return import_core6.HybridLogicalClock.deserialize(version);
|
|
1195
|
+
}
|
|
1196
|
+
return { wallTime: row._updated_at, logical: 0, nodeId: "" };
|
|
1197
|
+
}
|
|
1198
|
+
function isIncomingNewerThanRow(incoming, row) {
|
|
1199
|
+
return import_core6.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
|
|
1200
|
+
}
|
|
1201
|
+
function lwwVersionWhereClause(remoteVersion) {
|
|
1202
|
+
return {
|
|
1203
|
+
sql: "(_version = ? OR _version < ?)",
|
|
1204
|
+
params: [EMPTY_ROW_VERSION, remoteVersion]
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
115
1207
|
|
|
116
1208
|
// src/query/sql-builder.ts
|
|
117
1209
|
function buildSelectQuery(descriptor, fields) {
|
|
@@ -164,12 +1256,32 @@ function buildUpdateQuery(collection, id, changes) {
|
|
|
164
1256
|
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
165
1257
|
return { sql, params };
|
|
166
1258
|
}
|
|
167
|
-
function buildSoftDeleteQuery(collection, id, updatedAt) {
|
|
1259
|
+
function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1260
|
+
if (version !== void 0) {
|
|
1261
|
+
return {
|
|
1262
|
+
sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ?`,
|
|
1263
|
+
params: [updatedAt, version, id]
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
168
1266
|
return {
|
|
169
1267
|
sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
|
|
170
1268
|
params: [updatedAt, id]
|
|
171
1269
|
};
|
|
172
1270
|
}
|
|
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
|
+
function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1279
|
+
const lww = lwwVersionWhereClause(version);
|
|
1280
|
+
return {
|
|
1281
|
+
sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ? AND ${lww.sql}`,
|
|
1282
|
+
params: [updatedAt, version, id, ...lww.params]
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
173
1285
|
var VALID_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"]);
|
|
174
1286
|
function buildWhereClauseParts(where, fields, params) {
|
|
175
1287
|
const conditions = [];
|
|
@@ -255,200 +1367,188 @@ function validateFieldName(fieldName, fields) {
|
|
|
255
1367
|
`Unknown field "${fieldName}" in query. Available fields: ${[...allowedFields].join(", ")}`,
|
|
256
1368
|
{ field: fieldName }
|
|
257
1369
|
);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// src/serialization/serializer.ts
|
|
262
|
-
var import_core2 = require("@korajs/core");
|
|
263
|
-
|
|
264
|
-
// src/serialization/richtext-serializer.ts
|
|
265
|
-
var Y = __toESM(require("yjs"), 1);
|
|
266
|
-
var TEXT_KEY = "content";
|
|
267
|
-
function encodeRichtext(value) {
|
|
268
|
-
if (value === null || value === void 0) {
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
if (typeof value === "string") {
|
|
272
|
-
const doc = new Y.Doc();
|
|
273
|
-
doc.getText(TEXT_KEY).insert(0, value);
|
|
274
|
-
return Y.encodeStateAsUpdate(doc);
|
|
275
|
-
}
|
|
276
|
-
if (value instanceof Uint8Array) {
|
|
277
|
-
return value;
|
|
278
|
-
}
|
|
279
|
-
if (value instanceof ArrayBuffer) {
|
|
280
|
-
return new Uint8Array(value);
|
|
281
|
-
}
|
|
282
|
-
throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
|
|
283
|
-
}
|
|
284
|
-
function decodeRichtext(value) {
|
|
285
|
-
if (value === null || value === void 0) {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
|
|
289
|
-
return new Uint8Array(value);
|
|
290
|
-
}
|
|
291
|
-
if (value instanceof Uint8Array) {
|
|
292
|
-
return value;
|
|
293
|
-
}
|
|
294
|
-
if (value instanceof ArrayBuffer) {
|
|
295
|
-
return new Uint8Array(value);
|
|
296
|
-
}
|
|
297
|
-
throw new Error(
|
|
298
|
-
"Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined."
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
function richtextToPlainText(value) {
|
|
302
|
-
const encoded = encodeRichtext(value);
|
|
303
|
-
if (!encoded) return "";
|
|
304
|
-
const doc = new Y.Doc();
|
|
305
|
-
Y.applyUpdate(doc, encoded);
|
|
306
|
-
return doc.getText(TEXT_KEY).toString();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// src/serialization/serializer.ts
|
|
310
|
-
function serializeRecord(data, fields) {
|
|
311
|
-
const result = {};
|
|
312
|
-
for (const [key, value] of Object.entries(data)) {
|
|
313
|
-
const descriptor = fields[key];
|
|
314
|
-
if (!descriptor) {
|
|
315
|
-
result[key] = value;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
result[key] = serializeValue(value, descriptor);
|
|
319
|
-
}
|
|
320
|
-
return result;
|
|
321
|
-
}
|
|
322
|
-
function deserializeRecord(row, fields) {
|
|
323
|
-
const result = {
|
|
324
|
-
id: row.id,
|
|
325
|
-
createdAt: row._created_at,
|
|
326
|
-
updatedAt: row._updated_at
|
|
327
|
-
};
|
|
328
|
-
for (const [key, descriptor] of Object.entries(fields)) {
|
|
329
|
-
const rawValue = row[key];
|
|
330
|
-
if (rawValue === void 0 || rawValue === null) {
|
|
331
|
-
result[key] = rawValue ?? null;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
result[key] = deserializeValue(rawValue, descriptor);
|
|
335
|
-
}
|
|
336
|
-
return result;
|
|
337
|
-
}
|
|
338
|
-
var ATOMIC_OPS_KEY = "__kora_atomic_ops__";
|
|
339
|
-
var TX_ID_KEY = "__kora_tx_id__";
|
|
340
|
-
var MUTATION_NAME_KEY = "__kora_mutation__";
|
|
341
|
-
function serializeOperation(op) {
|
|
342
|
-
const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
|
|
343
|
-
let dataPayload = null;
|
|
344
|
-
if (op.data) {
|
|
345
|
-
dataPayload = { ...op.data };
|
|
346
|
-
if (op.atomicOps !== void 0 && Object.keys(op.atomicOps).length > 0) {
|
|
347
|
-
dataPayload[ATOMIC_OPS_KEY] = op.atomicOps;
|
|
348
|
-
}
|
|
349
|
-
if (op.transactionId !== void 0) {
|
|
350
|
-
dataPayload[TX_ID_KEY] = op.transactionId;
|
|
351
|
-
}
|
|
352
|
-
if (op.mutationName !== void 0) {
|
|
353
|
-
dataPayload[MUTATION_NAME_KEY] = op.mutationName;
|
|
354
|
-
}
|
|
355
|
-
} else if (hasMetadata) {
|
|
356
|
-
dataPayload = {};
|
|
357
|
-
if (op.transactionId !== void 0) {
|
|
358
|
-
dataPayload[TX_ID_KEY] = op.transactionId;
|
|
359
|
-
}
|
|
360
|
-
if (op.mutationName !== void 0) {
|
|
361
|
-
dataPayload[MUTATION_NAME_KEY] = op.mutationName;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
id: op.id,
|
|
366
|
-
node_id: op.nodeId,
|
|
367
|
-
type: op.type,
|
|
368
|
-
record_id: op.recordId,
|
|
369
|
-
data: dataPayload ? JSON.stringify(dataPayload) : null,
|
|
370
|
-
previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
|
|
371
|
-
timestamp: import_core2.HybridLogicalClock.serialize(op.timestamp),
|
|
372
|
-
sequence_number: op.sequenceNumber,
|
|
373
|
-
causal_deps: JSON.stringify(op.causalDeps),
|
|
374
|
-
schema_version: op.schemaVersion
|
|
375
|
-
};
|
|
1370
|
+
}
|
|
376
1371
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1372
|
+
|
|
1373
|
+
// src/mutations/execute-delete.ts
|
|
1374
|
+
init_serializer();
|
|
1375
|
+
|
|
1376
|
+
// src/store/sequence-allocator.ts
|
|
1377
|
+
async function allocateNextSequenceNumber(adapter, nodeId) {
|
|
1378
|
+
const rows = await adapter.query(
|
|
1379
|
+
`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
|
|
1380
|
+
ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
|
|
1381
|
+
RETURNING sequence_number`,
|
|
1382
|
+
[nodeId]
|
|
1383
|
+
);
|
|
1384
|
+
const seq = rows[0]?.sequence_number;
|
|
1385
|
+
if (seq === void 0) {
|
|
1386
|
+
throw new Error(`Failed to allocate sequence number for node "${nodeId}"`);
|
|
1387
|
+
}
|
|
1388
|
+
return seq;
|
|
1389
|
+
}
|
|
1390
|
+
async function allocateNextSequenceInTransaction(tx, nodeId) {
|
|
1391
|
+
const rows = await tx.query(
|
|
1392
|
+
`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
|
|
1393
|
+
ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
|
|
1394
|
+
RETURNING sequence_number`,
|
|
1395
|
+
[nodeId]
|
|
1396
|
+
);
|
|
1397
|
+
const seq = rows[0]?.sequence_number;
|
|
1398
|
+
if (seq === void 0) {
|
|
1399
|
+
throw new Error(`Failed to allocate sequence number for node "${nodeId}" in transaction`);
|
|
1400
|
+
}
|
|
1401
|
+
return seq;
|
|
1402
|
+
}
|
|
1403
|
+
async function readSequenceNumber(adapter, nodeId) {
|
|
1404
|
+
const rows = await adapter.query(
|
|
1405
|
+
"SELECT sequence_number FROM _kora_version_vector WHERE node_id = ?",
|
|
1406
|
+
[nodeId]
|
|
1407
|
+
);
|
|
1408
|
+
return rows[0]?.sequence_number ?? 0;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// src/mutations/resolve-causal-deps.ts
|
|
1412
|
+
function resolveCausalDeps(ctx) {
|
|
1413
|
+
const merged = [];
|
|
1414
|
+
for (const id of ctx.extraCausalDeps ?? []) {
|
|
1415
|
+
if (!merged.includes(id)) {
|
|
1416
|
+
merged.push(id);
|
|
389
1417
|
}
|
|
390
|
-
|
|
391
|
-
|
|
1418
|
+
}
|
|
1419
|
+
for (const id of ctx.causalTracker?.nextCausalDeps(ctx.collection, ctx.inTransaction) ?? []) {
|
|
1420
|
+
if (!merged.includes(id)) {
|
|
1421
|
+
merged.push(id);
|
|
392
1422
|
}
|
|
393
|
-
const { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed;
|
|
394
|
-
data = Object.keys(rest).length > 0 ? rest : null;
|
|
395
1423
|
}
|
|
396
|
-
return
|
|
397
|
-
id: row.id,
|
|
398
|
-
nodeId: row.node_id,
|
|
399
|
-
type: row.type,
|
|
400
|
-
collection: "",
|
|
401
|
-
// Collection name is derived from the table name by the caller
|
|
402
|
-
recordId: row.record_id,
|
|
403
|
-
data,
|
|
404
|
-
previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
|
|
405
|
-
timestamp: import_core2.HybridLogicalClock.deserialize(row.timestamp),
|
|
406
|
-
sequenceNumber: row.sequence_number,
|
|
407
|
-
causalDeps: JSON.parse(row.causal_deps),
|
|
408
|
-
schemaVersion: row.schema_version,
|
|
409
|
-
...atomicOps !== void 0 ? { atomicOps } : {},
|
|
410
|
-
...transactionId !== void 0 ? { transactionId } : {},
|
|
411
|
-
...mutationName !== void 0 ? { mutationName } : {}
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
function deserializeOperationWithCollection(row, collection) {
|
|
415
|
-
const op = deserializeOperation(row);
|
|
416
|
-
return { ...op, collection };
|
|
1424
|
+
return merged;
|
|
417
1425
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
1426
|
+
|
|
1427
|
+
// src/mutations/execute-delete.ts
|
|
1428
|
+
async function executeDelete(ctx, id, options) {
|
|
1429
|
+
const currentRows = await ctx.adapter.query(
|
|
1430
|
+
`SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
|
|
1431
|
+
[id]
|
|
1432
|
+
);
|
|
1433
|
+
if (!currentRows[0]) {
|
|
1434
|
+
throw new RecordNotFoundError(ctx.collection, id);
|
|
1435
|
+
}
|
|
1436
|
+
const causalDeps = resolveCausalDeps(ctx);
|
|
1437
|
+
let operation = options?.operation;
|
|
1438
|
+
const cascadedOps = [];
|
|
1439
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
1440
|
+
if (!operation) {
|
|
1441
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1442
|
+
operation = await (0, import_core7.createOperation)(
|
|
1443
|
+
{
|
|
1444
|
+
nodeId: ctx.nodeId,
|
|
1445
|
+
type: "delete",
|
|
1446
|
+
collection: ctx.collection,
|
|
1447
|
+
recordId: id,
|
|
1448
|
+
data: null,
|
|
1449
|
+
previousData: null,
|
|
1450
|
+
sequenceNumber,
|
|
1451
|
+
causalDeps,
|
|
1452
|
+
schemaVersion: ctx.schema.version
|
|
1453
|
+
},
|
|
1454
|
+
ctx.clock
|
|
1455
|
+
);
|
|
1456
|
+
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1457
|
+
}
|
|
1458
|
+
if (ctx.relationEnforcer && !options?.skipReferentialEnforcement) {
|
|
1459
|
+
const enforcementResult = await ctx.relationEnforcer.enforceDelete(ctx.collection, id, tx, [
|
|
1460
|
+
operation.id
|
|
1461
|
+
]);
|
|
1462
|
+
cascadedOps.push(...enforcementResult.operations);
|
|
1463
|
+
}
|
|
1464
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1465
|
+
const deleteQuery = buildSoftDeleteQuery(
|
|
1466
|
+
ctx.collection,
|
|
1467
|
+
id,
|
|
1468
|
+
operation.timestamp.wallTime,
|
|
1469
|
+
version
|
|
1470
|
+
);
|
|
1471
|
+
const opInsert = buildInsertQuery(
|
|
1472
|
+
`_kora_ops_${ctx.collection}`,
|
|
1473
|
+
serializeOperation(operation)
|
|
1474
|
+
);
|
|
1475
|
+
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
1476
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1477
|
+
});
|
|
1478
|
+
if (!operation) {
|
|
1479
|
+
throw new Error("Delete operation was not created");
|
|
421
1480
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
case "array":
|
|
426
|
-
return JSON.stringify(value);
|
|
427
|
-
case "richtext":
|
|
428
|
-
return encodeRichtext(value);
|
|
429
|
-
default:
|
|
430
|
-
return value;
|
|
1481
|
+
ctx.onMutation(ctx.collection, operation);
|
|
1482
|
+
for (const cascadedOp of cascadedOps) {
|
|
1483
|
+
ctx.onMutation(cascadedOp.collection, cascadedOp);
|
|
431
1484
|
}
|
|
1485
|
+
return [operation, ...cascadedOps];
|
|
432
1486
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
default:
|
|
445
|
-
return value;
|
|
1487
|
+
|
|
1488
|
+
// src/mutations/execute-insert.ts
|
|
1489
|
+
var import_core8 = require("@korajs/core");
|
|
1490
|
+
init_serializer();
|
|
1491
|
+
async function executeInsert(ctx, data) {
|
|
1492
|
+
const validated = (0, import_core8.validateRecord)(ctx.collection, ctx.definition, data, "insert");
|
|
1493
|
+
const recordId = (0, import_core8.generateUUIDv7)();
|
|
1494
|
+
for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
|
|
1495
|
+
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
1496
|
+
validated[fieldName] = Date.now();
|
|
1497
|
+
}
|
|
446
1498
|
}
|
|
1499
|
+
const causalDeps = resolveCausalDeps(ctx);
|
|
1500
|
+
let operation;
|
|
1501
|
+
let record;
|
|
1502
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
1503
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1504
|
+
operation = await (0, import_core8.createOperation)(
|
|
1505
|
+
{
|
|
1506
|
+
nodeId: ctx.nodeId,
|
|
1507
|
+
type: "insert",
|
|
1508
|
+
collection: ctx.collection,
|
|
1509
|
+
recordId,
|
|
1510
|
+
data: { ...validated },
|
|
1511
|
+
previousData: null,
|
|
1512
|
+
sequenceNumber,
|
|
1513
|
+
causalDeps,
|
|
1514
|
+
schemaVersion: ctx.schema.version
|
|
1515
|
+
},
|
|
1516
|
+
ctx.clock
|
|
1517
|
+
);
|
|
1518
|
+
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1519
|
+
const serializedData = serializeRecord(validated, ctx.definition.fields);
|
|
1520
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1521
|
+
record = {
|
|
1522
|
+
id: recordId,
|
|
1523
|
+
...serializedData,
|
|
1524
|
+
_created_at: operation.timestamp.wallTime,
|
|
1525
|
+
_updated_at: operation.timestamp.wallTime,
|
|
1526
|
+
_version: version
|
|
1527
|
+
};
|
|
1528
|
+
const builtInsert = buildInsertQuery(ctx.collection, record);
|
|
1529
|
+
const opInsert = buildInsertQuery(
|
|
1530
|
+
`_kora_ops_${ctx.collection}`,
|
|
1531
|
+
serializeOperation(operation)
|
|
1532
|
+
);
|
|
1533
|
+
await tx.execute(builtInsert.sql, builtInsert.params);
|
|
1534
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1535
|
+
});
|
|
1536
|
+
ctx.onMutation(ctx.collection, operation);
|
|
1537
|
+
return {
|
|
1538
|
+
id: recordId,
|
|
1539
|
+
...validated,
|
|
1540
|
+
createdAt: operation.timestamp.wallTime,
|
|
1541
|
+
updatedAt: operation.timestamp.wallTime
|
|
1542
|
+
};
|
|
447
1543
|
}
|
|
448
1544
|
|
|
1545
|
+
// src/mutations/execute-update.ts
|
|
1546
|
+
var import_core10 = require("@korajs/core");
|
|
1547
|
+
init_serializer();
|
|
1548
|
+
|
|
449
1549
|
// src/state-machine/state-validator.ts
|
|
450
|
-
var
|
|
451
|
-
var InvalidStateTransitionError = class extends
|
|
1550
|
+
var import_core9 = require("@korajs/core");
|
|
1551
|
+
var InvalidStateTransitionError = class extends import_core9.KoraError {
|
|
452
1552
|
constructor(collection, recordId, field, fromState, toState, allowedStates) {
|
|
453
1553
|
super(
|
|
454
1554
|
`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)"}`,
|
|
@@ -482,7 +1582,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
|
|
|
482
1582
|
collection: collectionName,
|
|
483
1583
|
transitions: stateMachine.transitions
|
|
484
1584
|
};
|
|
485
|
-
const transitionResult = (0,
|
|
1585
|
+
const transitionResult = (0, import_core9.validateTransition)(constraint, currentState, newState);
|
|
486
1586
|
if (transitionResult.valid) {
|
|
487
1587
|
return { valid: true, allowedStates: transitionResult.allowedTargets };
|
|
488
1588
|
}
|
|
@@ -528,18 +1628,103 @@ function validateUpdateStateMachine(collectionName, recordId, collectionDef, cur
|
|
|
528
1628
|
return filtered;
|
|
529
1629
|
}
|
|
530
1630
|
|
|
1631
|
+
// src/mutations/execute-update.ts
|
|
1632
|
+
async function executeUpdate(ctx, id, data) {
|
|
1633
|
+
const currentRows = await ctx.adapter.query(
|
|
1634
|
+
`SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
|
|
1635
|
+
[id]
|
|
1636
|
+
);
|
|
1637
|
+
const currentRow = currentRows[0];
|
|
1638
|
+
if (!currentRow) {
|
|
1639
|
+
throw new RecordNotFoundError(ctx.collection, id);
|
|
1640
|
+
}
|
|
1641
|
+
let validated = (0, import_core10.validateRecord)(ctx.collection, ctx.definition, data, "update");
|
|
1642
|
+
const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
|
|
1643
|
+
validated = validateUpdateStateMachine(
|
|
1644
|
+
ctx.collection,
|
|
1645
|
+
id,
|
|
1646
|
+
ctx.definition,
|
|
1647
|
+
currentRecord,
|
|
1648
|
+
validated
|
|
1649
|
+
);
|
|
1650
|
+
if (Object.keys(validated).length === 0) {
|
|
1651
|
+
return { ...currentRecord };
|
|
1652
|
+
}
|
|
1653
|
+
const previousData = {};
|
|
1654
|
+
const resolvedData = {};
|
|
1655
|
+
const atomicOps = {};
|
|
1656
|
+
for (const key of Object.keys(validated)) {
|
|
1657
|
+
const value = validated[key];
|
|
1658
|
+
previousData[key] = currentRecord[key];
|
|
1659
|
+
if ((0, import_core10.isAtomicOp)(value)) {
|
|
1660
|
+
resolvedData[key] = (0, import_core10.resolveAtomicOp)(currentRecord[key], value);
|
|
1661
|
+
atomicOps[key] = (0, import_core10.toAtomicOp)(value);
|
|
1662
|
+
} else {
|
|
1663
|
+
resolvedData[key] = value;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
1667
|
+
const causalDeps = resolveCausalDeps(ctx);
|
|
1668
|
+
let operation;
|
|
1669
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
1670
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1671
|
+
operation = await (0, import_core10.createOperation)(
|
|
1672
|
+
{
|
|
1673
|
+
nodeId: ctx.nodeId,
|
|
1674
|
+
type: "update",
|
|
1675
|
+
collection: ctx.collection,
|
|
1676
|
+
recordId: id,
|
|
1677
|
+
data: { ...resolvedData },
|
|
1678
|
+
previousData,
|
|
1679
|
+
sequenceNumber,
|
|
1680
|
+
causalDeps,
|
|
1681
|
+
schemaVersion: ctx.schema.version,
|
|
1682
|
+
...hasAtomicOps ? { atomicOps } : {}
|
|
1683
|
+
},
|
|
1684
|
+
ctx.clock
|
|
1685
|
+
);
|
|
1686
|
+
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1687
|
+
const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
|
|
1688
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1689
|
+
const updateQuery = buildUpdateQuery(ctx.collection, id, {
|
|
1690
|
+
...serializedChanges,
|
|
1691
|
+
_updated_at: operation.timestamp.wallTime,
|
|
1692
|
+
_version: version
|
|
1693
|
+
});
|
|
1694
|
+
const opInsert = buildInsertQuery(
|
|
1695
|
+
`_kora_ops_${ctx.collection}`,
|
|
1696
|
+
serializeOperation(operation)
|
|
1697
|
+
);
|
|
1698
|
+
await tx.execute(updateQuery.sql, updateQuery.params);
|
|
1699
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1700
|
+
});
|
|
1701
|
+
ctx.onMutation(ctx.collection, operation);
|
|
1702
|
+
const rows = await ctx.adapter.query(
|
|
1703
|
+
`SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
|
|
1704
|
+
[id]
|
|
1705
|
+
);
|
|
1706
|
+
const row = rows[0];
|
|
1707
|
+
if (!row) {
|
|
1708
|
+
throw new RecordNotFoundError(ctx.collection, id);
|
|
1709
|
+
}
|
|
1710
|
+
return deserializeRecord(row, ctx.definition.fields);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
531
1713
|
// src/collection/collection.ts
|
|
1714
|
+
init_serializer();
|
|
532
1715
|
var Collection = class {
|
|
533
|
-
constructor(name, definition, schema, adapter, clock, nodeId,
|
|
1716
|
+
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
|
|
534
1717
|
this.name = name;
|
|
535
1718
|
this.definition = definition;
|
|
536
1719
|
this.schema = schema;
|
|
537
1720
|
this.adapter = adapter;
|
|
538
1721
|
this.clock = clock;
|
|
539
1722
|
this.nodeId = nodeId;
|
|
540
|
-
this.
|
|
1723
|
+
this.allocateSequenceNumber = allocateSequenceNumber;
|
|
541
1724
|
this.onMutation = onMutation;
|
|
542
|
-
this.relationEnforcer = relationEnforcer
|
|
1725
|
+
this.relationEnforcer = relationEnforcer;
|
|
1726
|
+
this.mutationHandler = mutationHandler;
|
|
1727
|
+
this.causalTracker = causalTracker;
|
|
543
1728
|
}
|
|
544
1729
|
name;
|
|
545
1730
|
definition;
|
|
@@ -547,72 +1732,32 @@ var Collection = class {
|
|
|
547
1732
|
adapter;
|
|
548
1733
|
clock;
|
|
549
1734
|
nodeId;
|
|
550
|
-
|
|
1735
|
+
allocateSequenceNumber;
|
|
551
1736
|
onMutation;
|
|
552
1737
|
relationEnforcer;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
*
|
|
557
|
-
* @param data - The record data (auto fields and defaults are applied automatically)
|
|
558
|
-
* @returns The inserted record with id, createdAt, updatedAt
|
|
559
|
-
*/
|
|
560
|
-
async insert(data) {
|
|
561
|
-
const validated = (0, import_core4.validateRecord)(this.name, this.definition, data, "insert");
|
|
562
|
-
const recordId = (0, import_core4.generateUUIDv7)();
|
|
563
|
-
const now = Date.now();
|
|
564
|
-
for (const [fieldName, descriptor] of Object.entries(this.definition.fields)) {
|
|
565
|
-
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
566
|
-
validated[fieldName] = now;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
const sequenceNumber = this.getSequenceNumber();
|
|
570
|
-
const operation = await (0, import_core4.createOperation)(
|
|
571
|
-
{
|
|
572
|
-
nodeId: this.nodeId,
|
|
573
|
-
type: "insert",
|
|
574
|
-
collection: this.name,
|
|
575
|
-
recordId,
|
|
576
|
-
data: { ...validated },
|
|
577
|
-
previousData: null,
|
|
578
|
-
sequenceNumber,
|
|
579
|
-
causalDeps: [],
|
|
580
|
-
schemaVersion: this.schema.version
|
|
581
|
-
},
|
|
582
|
-
this.clock
|
|
583
|
-
);
|
|
584
|
-
const serializedData = serializeRecord(validated, this.definition.fields);
|
|
585
|
-
const record = {
|
|
586
|
-
id: recordId,
|
|
587
|
-
...serializedData,
|
|
588
|
-
_created_at: now,
|
|
589
|
-
_updated_at: now
|
|
590
|
-
};
|
|
591
|
-
const insertQuery = buildInsertQuery(this.name, record);
|
|
592
|
-
const opRow = serializeOperation(operation);
|
|
593
|
-
const opInsert = buildInsertQuery(
|
|
594
|
-
`_kora_ops_${this.name}`,
|
|
595
|
-
opRow
|
|
596
|
-
);
|
|
597
|
-
await this.adapter.transaction(async (tx) => {
|
|
598
|
-
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
599
|
-
await tx.execute(opInsert.sql, opInsert.params);
|
|
600
|
-
await tx.execute(
|
|
601
|
-
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
602
|
-
[this.nodeId, sequenceNumber]
|
|
603
|
-
);
|
|
604
|
-
});
|
|
605
|
-
this.onMutation(this.name, operation);
|
|
1738
|
+
mutationHandler;
|
|
1739
|
+
causalTracker;
|
|
1740
|
+
mutationContext() {
|
|
606
1741
|
return {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1742
|
+
collection: this.name,
|
|
1743
|
+
definition: this.definition,
|
|
1744
|
+
schema: this.schema,
|
|
1745
|
+
adapter: this.adapter,
|
|
1746
|
+
clock: this.clock,
|
|
1747
|
+
nodeId: this.nodeId,
|
|
1748
|
+
allocateSequenceNumber: this.allocateSequenceNumber,
|
|
1749
|
+
onMutation: this.onMutation,
|
|
1750
|
+
relationEnforcer: this.relationEnforcer,
|
|
1751
|
+
causalTracker: this.causalTracker,
|
|
1752
|
+
inTransaction: false
|
|
611
1753
|
};
|
|
612
1754
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1755
|
+
async insert(data) {
|
|
1756
|
+
if (this.mutationHandler) {
|
|
1757
|
+
return this.mutationHandler.insert(this.name, data);
|
|
1758
|
+
}
|
|
1759
|
+
return executeInsert(this.mutationContext(), data);
|
|
1760
|
+
}
|
|
616
1761
|
async findById(id) {
|
|
617
1762
|
const rows = await this.adapter.query(
|
|
618
1763
|
`SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
|
|
@@ -622,175 +1767,34 @@ var Collection = class {
|
|
|
622
1767
|
if (!row) return null;
|
|
623
1768
|
return deserializeRecord(row, this.definition.fields);
|
|
624
1769
|
}
|
|
625
|
-
/**
|
|
626
|
-
* Update an existing record. Only the provided fields are changed.
|
|
627
|
-
*
|
|
628
|
-
* @param id - The record ID to update
|
|
629
|
-
* @param data - Partial data with only the fields to change
|
|
630
|
-
* @returns The updated record
|
|
631
|
-
* @throws {RecordNotFoundError} If the record doesn't exist or is deleted
|
|
632
|
-
*/
|
|
633
1770
|
async update(id, data) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
[id]
|
|
637
|
-
);
|
|
638
|
-
const currentRow = currentRows[0];
|
|
639
|
-
if (!currentRow) {
|
|
640
|
-
throw new RecordNotFoundError(this.name, id);
|
|
641
|
-
}
|
|
642
|
-
let validated = (0, import_core4.validateRecord)(this.name, this.definition, data, "update");
|
|
643
|
-
const now = Date.now();
|
|
644
|
-
const currentRecord = deserializeRecord(currentRow, this.definition.fields);
|
|
645
|
-
validated = validateUpdateStateMachine(
|
|
646
|
-
this.name,
|
|
647
|
-
id,
|
|
648
|
-
this.definition,
|
|
649
|
-
currentRecord,
|
|
650
|
-
validated
|
|
651
|
-
);
|
|
652
|
-
if (Object.keys(validated).length === 0) {
|
|
653
|
-
const fullRecord = await this.findById(id);
|
|
654
|
-
if (!fullRecord) {
|
|
655
|
-
throw new RecordNotFoundError(this.name, id);
|
|
656
|
-
}
|
|
657
|
-
return fullRecord;
|
|
658
|
-
}
|
|
659
|
-
const previousData = {};
|
|
660
|
-
const resolvedData = {};
|
|
661
|
-
const atomicOps = {};
|
|
662
|
-
for (const key of Object.keys(validated)) {
|
|
663
|
-
const value = validated[key];
|
|
664
|
-
previousData[key] = currentRecord[key];
|
|
665
|
-
if ((0, import_core4.isAtomicOp)(value)) {
|
|
666
|
-
resolvedData[key] = (0, import_core4.resolveAtomicOp)(currentRecord[key], value);
|
|
667
|
-
atomicOps[key] = (0, import_core4.toAtomicOp)(value);
|
|
668
|
-
} else {
|
|
669
|
-
resolvedData[key] = value;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
673
|
-
const sequenceNumber = this.getSequenceNumber();
|
|
674
|
-
const operation = await (0, import_core4.createOperation)(
|
|
675
|
-
{
|
|
676
|
-
nodeId: this.nodeId,
|
|
677
|
-
type: "update",
|
|
678
|
-
collection: this.name,
|
|
679
|
-
recordId: id,
|
|
680
|
-
data: { ...resolvedData },
|
|
681
|
-
previousData,
|
|
682
|
-
sequenceNumber,
|
|
683
|
-
causalDeps: [],
|
|
684
|
-
schemaVersion: this.schema.version,
|
|
685
|
-
...hasAtomicOps ? { atomicOps } : {}
|
|
686
|
-
},
|
|
687
|
-
this.clock
|
|
688
|
-
);
|
|
689
|
-
const serializedChanges = serializeRecord(resolvedData, this.definition.fields);
|
|
690
|
-
const updateQuery = buildUpdateQuery(this.name, id, {
|
|
691
|
-
...serializedChanges,
|
|
692
|
-
_updated_at: now
|
|
693
|
-
});
|
|
694
|
-
const opRow = serializeOperation(operation);
|
|
695
|
-
const opInsert = buildInsertQuery(
|
|
696
|
-
`_kora_ops_${this.name}`,
|
|
697
|
-
opRow
|
|
698
|
-
);
|
|
699
|
-
await this.adapter.transaction(async (tx) => {
|
|
700
|
-
await tx.execute(updateQuery.sql, updateQuery.params);
|
|
701
|
-
await tx.execute(opInsert.sql, opInsert.params);
|
|
702
|
-
await tx.execute(
|
|
703
|
-
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
704
|
-
[this.nodeId, sequenceNumber]
|
|
705
|
-
);
|
|
706
|
-
});
|
|
707
|
-
this.onMutation(this.name, operation);
|
|
708
|
-
const updatedRow = await this.findById(id);
|
|
709
|
-
if (!updatedRow) {
|
|
710
|
-
throw new RecordNotFoundError(this.name, id);
|
|
1771
|
+
if (this.mutationHandler) {
|
|
1772
|
+
return this.mutationHandler.update(this.name, id, data);
|
|
711
1773
|
}
|
|
712
|
-
return
|
|
1774
|
+
return executeUpdate(this.mutationContext(), id, data);
|
|
713
1775
|
}
|
|
714
|
-
/**
|
|
715
|
-
* Soft-delete a record by its ID.
|
|
716
|
-
*
|
|
717
|
-
* When a RelationEnforcer is configured, referential integrity policies
|
|
718
|
-
* are enforced within the same transaction:
|
|
719
|
-
* - **cascade**: Recursively deletes all referencing records
|
|
720
|
-
* - **set-null**: Sets foreign keys to null on referencing records
|
|
721
|
-
* - **restrict**: Throws ReferentialIntegrityError if references exist
|
|
722
|
-
* - **no-action**: Does nothing (dangling references are left)
|
|
723
|
-
*
|
|
724
|
-
* All cascaded operations are created atomically within the same transaction
|
|
725
|
-
* and share causal dependencies with the original delete.
|
|
726
|
-
*
|
|
727
|
-
* @param id - The record ID to delete
|
|
728
|
-
* @throws {RecordNotFoundError} If the record doesn't exist or is already deleted
|
|
729
|
-
* @throws {ReferentialIntegrityError} If a 'restrict' policy is violated
|
|
730
|
-
*/
|
|
731
1776
|
async delete(id) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
);
|
|
736
|
-
if (!currentRows[0]) {
|
|
737
|
-
throw new RecordNotFoundError(this.name, id);
|
|
738
|
-
}
|
|
739
|
-
const now = Date.now();
|
|
740
|
-
const sequenceNumber = this.getSequenceNumber();
|
|
741
|
-
const operation = await (0, import_core4.createOperation)(
|
|
742
|
-
{
|
|
743
|
-
nodeId: this.nodeId,
|
|
744
|
-
type: "delete",
|
|
745
|
-
collection: this.name,
|
|
746
|
-
recordId: id,
|
|
747
|
-
data: null,
|
|
748
|
-
previousData: null,
|
|
749
|
-
sequenceNumber,
|
|
750
|
-
causalDeps: [],
|
|
751
|
-
schemaVersion: this.schema.version
|
|
752
|
-
},
|
|
753
|
-
this.clock
|
|
754
|
-
);
|
|
755
|
-
const deleteQuery = buildSoftDeleteQuery(this.name, id, now);
|
|
756
|
-
const opRow = serializeOperation(operation);
|
|
757
|
-
const opInsert = buildInsertQuery(
|
|
758
|
-
`_kora_ops_${this.name}`,
|
|
759
|
-
opRow
|
|
760
|
-
);
|
|
761
|
-
const cascadedOps = [];
|
|
762
|
-
await this.adapter.transaction(async (tx) => {
|
|
763
|
-
if (this.relationEnforcer) {
|
|
764
|
-
const enforcementResult = await this.relationEnforcer.enforceDelete(
|
|
765
|
-
this.name,
|
|
766
|
-
id,
|
|
767
|
-
tx,
|
|
768
|
-
[operation.id]
|
|
769
|
-
);
|
|
770
|
-
cascadedOps.push(...enforcementResult.operations);
|
|
771
|
-
}
|
|
772
|
-
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
773
|
-
await tx.execute(opInsert.sql, opInsert.params);
|
|
774
|
-
await tx.execute(
|
|
775
|
-
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
776
|
-
[this.nodeId, sequenceNumber]
|
|
777
|
-
);
|
|
778
|
-
});
|
|
779
|
-
this.onMutation(this.name, operation);
|
|
780
|
-
for (const cascadedOp of cascadedOps) {
|
|
781
|
-
this.onMutation(cascadedOp.collection, cascadedOp);
|
|
1777
|
+
if (this.mutationHandler) {
|
|
1778
|
+
await this.mutationHandler.delete(this.name, id);
|
|
1779
|
+
return;
|
|
782
1780
|
}
|
|
1781
|
+
await executeDelete(this.mutationContext(), id);
|
|
783
1782
|
}
|
|
784
|
-
/** Get the collection name */
|
|
785
1783
|
getName() {
|
|
786
1784
|
return this.name;
|
|
787
1785
|
}
|
|
788
|
-
/** Get the collection definition */
|
|
789
1786
|
getDefinition() {
|
|
790
1787
|
return this.definition;
|
|
791
1788
|
}
|
|
1789
|
+
/** Replace the mutation handler (e.g. after ApplyPipeline is wired in createApp). */
|
|
1790
|
+
setMutationHandler(handler) {
|
|
1791
|
+
this.mutationHandler = handler;
|
|
1792
|
+
}
|
|
792
1793
|
};
|
|
793
1794
|
|
|
1795
|
+
// src/query/query-builder.ts
|
|
1796
|
+
init_serializer();
|
|
1797
|
+
|
|
794
1798
|
// src/query/pluralize.ts
|
|
795
1799
|
function isVowel(char) {
|
|
796
1800
|
if (!char) return false;
|
|
@@ -1085,7 +2089,8 @@ var QueryError2 = class extends Error {
|
|
|
1085
2089
|
};
|
|
1086
2090
|
|
|
1087
2091
|
// src/relations/relation-enforcer.ts
|
|
1088
|
-
var
|
|
2092
|
+
var import_core11 = require("@korajs/core");
|
|
2093
|
+
init_serializer();
|
|
1089
2094
|
|
|
1090
2095
|
// src/relations/relation-lookup.ts
|
|
1091
2096
|
function buildRelationLookup(schema) {
|
|
@@ -1109,7 +2114,7 @@ function getIncomingRelations(lookup, collection) {
|
|
|
1109
2114
|
}
|
|
1110
2115
|
|
|
1111
2116
|
// src/relations/relation-enforcer.ts
|
|
1112
|
-
var ReferentialIntegrityError = class extends
|
|
2117
|
+
var ReferentialIntegrityError = class extends import_core11.KoraError {
|
|
1113
2118
|
constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
|
|
1114
2119
|
super(
|
|
1115
2120
|
`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.`,
|
|
@@ -1131,13 +2136,11 @@ var RelationEnforcer = class {
|
|
|
1131
2136
|
adapter;
|
|
1132
2137
|
clock;
|
|
1133
2138
|
nodeId;
|
|
1134
|
-
getSequenceNumber;
|
|
1135
2139
|
constructor(config) {
|
|
1136
2140
|
this.schema = config.schema;
|
|
1137
2141
|
this.adapter = config.adapter;
|
|
1138
2142
|
this.clock = config.clock;
|
|
1139
2143
|
this.nodeId = config.nodeId;
|
|
1140
|
-
this.getSequenceNumber = config.getSequenceNumber;
|
|
1141
2144
|
this.lookup = buildRelationLookup(config.schema);
|
|
1142
2145
|
}
|
|
1143
2146
|
/**
|
|
@@ -1199,8 +2202,8 @@ var RelationEnforcer = class {
|
|
|
1199
2202
|
const operations = [];
|
|
1200
2203
|
for (const row of referencingRows) {
|
|
1201
2204
|
const now = Date.now();
|
|
1202
|
-
const sequenceNumber = this.
|
|
1203
|
-
const operation = await (0,
|
|
2205
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2206
|
+
const operation = await (0, import_core11.createOperation)(
|
|
1204
2207
|
{
|
|
1205
2208
|
nodeId: this.nodeId,
|
|
1206
2209
|
type: "delete",
|
|
@@ -1216,16 +2219,11 @@ var RelationEnforcer = class {
|
|
|
1216
2219
|
);
|
|
1217
2220
|
const deleteQuery = buildSoftDeleteQuery(sourceCollection, row.id, now);
|
|
1218
2221
|
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
1219
|
-
const opRow = serializeOperation(operation);
|
|
1220
2222
|
const opInsert = buildInsertQuery(
|
|
1221
2223
|
`_kora_ops_${sourceCollection}`,
|
|
1222
|
-
|
|
2224
|
+
serializeOperation(operation)
|
|
1223
2225
|
);
|
|
1224
2226
|
await tx.execute(opInsert.sql, opInsert.params);
|
|
1225
|
-
await tx.execute(
|
|
1226
|
-
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
1227
|
-
[this.nodeId, sequenceNumber]
|
|
1228
|
-
);
|
|
1229
2227
|
operations.push(operation);
|
|
1230
2228
|
const cascadeResult = await this.enforceDelete(sourceCollection, row.id, tx, [operation.id]);
|
|
1231
2229
|
operations.push(...cascadeResult.operations);
|
|
@@ -1251,10 +2249,10 @@ var RelationEnforcer = class {
|
|
|
1251
2249
|
const operations = [];
|
|
1252
2250
|
for (const row of referencingRows) {
|
|
1253
2251
|
const now = Date.now();
|
|
1254
|
-
const sequenceNumber = this.
|
|
2252
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
1255
2253
|
const updateData = { [foreignKeyField]: null };
|
|
1256
2254
|
const previousData = { [foreignKeyField]: deletedRecordId };
|
|
1257
|
-
const operation = await (0,
|
|
2255
|
+
const operation = await (0, import_core11.createOperation)(
|
|
1258
2256
|
{
|
|
1259
2257
|
nodeId: this.nodeId,
|
|
1260
2258
|
type: "update",
|
|
@@ -1280,10 +2278,6 @@ var RelationEnforcer = class {
|
|
|
1280
2278
|
opRow
|
|
1281
2279
|
);
|
|
1282
2280
|
await tx.execute(opInsert.sql, opInsert.params);
|
|
1283
|
-
await tx.execute(
|
|
1284
|
-
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
1285
|
-
[this.nodeId, sequenceNumber]
|
|
1286
|
-
);
|
|
1287
2281
|
operations.push(operation);
|
|
1288
2282
|
}
|
|
1289
2283
|
return operations;
|
|
@@ -1318,8 +2312,172 @@ var RelationEnforcer = class {
|
|
|
1318
2312
|
}
|
|
1319
2313
|
};
|
|
1320
2314
|
|
|
2315
|
+
// src/replay/replay-to.ts
|
|
2316
|
+
var import_core12 = require("@korajs/core");
|
|
2317
|
+
var import_internal = require("@korajs/core/internal");
|
|
2318
|
+
function collectCausalClosure(allOps, targetOperationId) {
|
|
2319
|
+
const opMap = /* @__PURE__ */ new Map();
|
|
2320
|
+
for (const op of allOps) {
|
|
2321
|
+
opMap.set(op.id, op);
|
|
2322
|
+
}
|
|
2323
|
+
const target = opMap.get(targetOperationId);
|
|
2324
|
+
if (!target) {
|
|
2325
|
+
throw new import_core12.OperationError(
|
|
2326
|
+
`Operation "${targetOperationId}" not found in the local operation log`,
|
|
2327
|
+
{
|
|
2328
|
+
operationId: targetOperationId
|
|
2329
|
+
}
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
const included = /* @__PURE__ */ new Set();
|
|
2333
|
+
const stack = [targetOperationId];
|
|
2334
|
+
while (stack.length > 0) {
|
|
2335
|
+
const id = stack.pop();
|
|
2336
|
+
if (id === void 0 || included.has(id)) {
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
included.add(id);
|
|
2340
|
+
const op = opMap.get(id);
|
|
2341
|
+
if (!op) {
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
for (const depId of op.causalDeps) {
|
|
2345
|
+
if (opMap.has(depId)) {
|
|
2346
|
+
stack.push(depId);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const subset = allOps.filter((op) => included.has(op.id));
|
|
2351
|
+
return (0, import_internal.topologicalSort)(subset);
|
|
2352
|
+
}
|
|
2353
|
+
function buildReplaySnapshot(schema, allOps, targetOperationId) {
|
|
2354
|
+
const operationsApplied = collectCausalClosure(allOps, targetOperationId);
|
|
2355
|
+
const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
|
|
2356
|
+
if (!targetOperation) {
|
|
2357
|
+
throw new import_core12.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
|
|
2358
|
+
operationId: targetOperationId
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
const memory = /* @__PURE__ */ new Map();
|
|
2362
|
+
for (const op of operationsApplied) {
|
|
2363
|
+
applyOperationToMemory(memory, op, schema);
|
|
2364
|
+
}
|
|
2365
|
+
const collections = materializeCollections(schema, memory);
|
|
2366
|
+
return {
|
|
2367
|
+
targetOperation,
|
|
2368
|
+
operationsApplied,
|
|
2369
|
+
collections,
|
|
2370
|
+
findRecord(collection, recordId) {
|
|
2371
|
+
const colMap = memory.get(collection);
|
|
2372
|
+
const record = colMap?.get(recordId);
|
|
2373
|
+
if (!record || record.deleted) {
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
const definition = schema.collections[collection];
|
|
2377
|
+
if (!definition) {
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
return toCollectionRecord(record, definition);
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
function applyOperationToMemory(state, op, schema) {
|
|
2385
|
+
const definition = schema.collections[op.collection];
|
|
2386
|
+
if (!definition) {
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
let colMap = state.get(op.collection);
|
|
2390
|
+
if (!colMap) {
|
|
2391
|
+
colMap = /* @__PURE__ */ new Map();
|
|
2392
|
+
state.set(op.collection, colMap);
|
|
2393
|
+
}
|
|
2394
|
+
const wallTime = op.timestamp.wallTime;
|
|
2395
|
+
switch (op.type) {
|
|
2396
|
+
case "insert": {
|
|
2397
|
+
if (!op.data) {
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
colMap.set(op.recordId, {
|
|
2401
|
+
id: op.recordId,
|
|
2402
|
+
fields: { ...op.data },
|
|
2403
|
+
deleted: false,
|
|
2404
|
+
createdAt: wallTime,
|
|
2405
|
+
updatedAt: wallTime
|
|
2406
|
+
});
|
|
2407
|
+
break;
|
|
2408
|
+
}
|
|
2409
|
+
case "update": {
|
|
2410
|
+
if (!op.data) {
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
const existing = colMap.get(op.recordId);
|
|
2414
|
+
if (existing && !existing.deleted) {
|
|
2415
|
+
existing.fields = { ...existing.fields, ...op.data };
|
|
2416
|
+
existing.updatedAt = wallTime;
|
|
2417
|
+
break;
|
|
2418
|
+
}
|
|
2419
|
+
if (existing?.deleted) {
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
colMap.set(op.recordId, {
|
|
2423
|
+
id: op.recordId,
|
|
2424
|
+
fields: { ...op.data },
|
|
2425
|
+
deleted: false,
|
|
2426
|
+
createdAt: wallTime,
|
|
2427
|
+
updatedAt: wallTime
|
|
2428
|
+
});
|
|
2429
|
+
break;
|
|
2430
|
+
}
|
|
2431
|
+
case "delete": {
|
|
2432
|
+
const existing = colMap.get(op.recordId);
|
|
2433
|
+
if (existing) {
|
|
2434
|
+
existing.deleted = true;
|
|
2435
|
+
existing.updatedAt = wallTime;
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
colMap.set(op.recordId, {
|
|
2439
|
+
id: op.recordId,
|
|
2440
|
+
fields: {},
|
|
2441
|
+
deleted: true,
|
|
2442
|
+
createdAt: wallTime,
|
|
2443
|
+
updatedAt: wallTime
|
|
2444
|
+
});
|
|
2445
|
+
break;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function materializeCollections(schema, memory) {
|
|
2450
|
+
const collections = {};
|
|
2451
|
+
for (const [collectionName, definition] of Object.entries(schema.collections)) {
|
|
2452
|
+
const colMap = memory.get(collectionName);
|
|
2453
|
+
const records = [];
|
|
2454
|
+
if (colMap) {
|
|
2455
|
+
for (const record of colMap.values()) {
|
|
2456
|
+
if (!record.deleted) {
|
|
2457
|
+
records.push(toCollectionRecord(record, definition));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
collections[collectionName] = records;
|
|
2462
|
+
}
|
|
2463
|
+
return collections;
|
|
2464
|
+
}
|
|
2465
|
+
function toCollectionRecord(record, definition) {
|
|
2466
|
+
const result = {
|
|
2467
|
+
id: record.id,
|
|
2468
|
+
createdAt: record.createdAt,
|
|
2469
|
+
updatedAt: record.updatedAt
|
|
2470
|
+
};
|
|
2471
|
+
for (const [fieldName] of Object.entries(definition.fields)) {
|
|
2472
|
+
if (fieldName in record.fields) {
|
|
2473
|
+
result[fieldName] = record.fields[fieldName];
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
return result;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
1321
2479
|
// src/sequences/sequence-manager.ts
|
|
1322
|
-
var
|
|
2480
|
+
var import_core13 = require("@korajs/core");
|
|
1323
2481
|
var SequenceManager = class {
|
|
1324
2482
|
adapter;
|
|
1325
2483
|
nodeId;
|
|
@@ -1337,7 +2495,7 @@ var SequenceManager = class {
|
|
|
1337
2495
|
async next(name, config) {
|
|
1338
2496
|
const scope = config?.scope ?? "";
|
|
1339
2497
|
const startAt = config?.startAt ?? 1;
|
|
1340
|
-
const format = config?.format ?? (0,
|
|
2498
|
+
const format = config?.format ?? (0, import_core13.defaultSequenceFormat)(name);
|
|
1341
2499
|
let counter = 0;
|
|
1342
2500
|
await this.adapter.transaction(async (tx) => {
|
|
1343
2501
|
const rows = await tx.query(
|
|
@@ -1359,7 +2517,7 @@ var SequenceManager = class {
|
|
|
1359
2517
|
);
|
|
1360
2518
|
}
|
|
1361
2519
|
});
|
|
1362
|
-
return (0,
|
|
2520
|
+
return (0, import_core13.formatSequenceValue)(format, counter, this.nodeId);
|
|
1363
2521
|
}
|
|
1364
2522
|
/**
|
|
1365
2523
|
* Get the current counter value without incrementing.
|
|
@@ -1401,6 +2559,9 @@ var SequenceManager = class {
|
|
|
1401
2559
|
}
|
|
1402
2560
|
};
|
|
1403
2561
|
|
|
2562
|
+
// src/store/store.ts
|
|
2563
|
+
init_serializer();
|
|
2564
|
+
|
|
1404
2565
|
// src/subscription/bloom-filter.ts
|
|
1405
2566
|
var FNV_OFFSET_BASIS = 2166136261;
|
|
1406
2567
|
var FNV_PRIME = 16777619;
|
|
@@ -1561,6 +2722,7 @@ var SubscriptionManager = class {
|
|
|
1561
2722
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1562
2723
|
pendingCollections = /* @__PURE__ */ new Set();
|
|
1563
2724
|
flushScheduled = false;
|
|
2725
|
+
onQuerySubscribed;
|
|
1564
2726
|
// Bloom filter state
|
|
1565
2727
|
bloomFilter = null;
|
|
1566
2728
|
bloomDirty = false;
|
|
@@ -1577,6 +2739,7 @@ var SubscriptionManager = class {
|
|
|
1577
2739
|
this.bloomThreshold = options?.bloomThreshold ?? DEFAULT_BLOOM_THRESHOLD;
|
|
1578
2740
|
this.bloomExpectedItems = options?.bloomExpectedItems ?? DEFAULT_BLOOM_EXPECTED_ITEMS;
|
|
1579
2741
|
this.bloomFalsePositiveRate = options?.bloomFalsePositiveRate ?? DEFAULT_BLOOM_FALSE_POSITIVE_RATE;
|
|
2742
|
+
this.onQuerySubscribed = options?.onQuerySubscribed;
|
|
1580
2743
|
}
|
|
1581
2744
|
/**
|
|
1582
2745
|
* Register a new subscription.
|
|
@@ -1597,9 +2760,11 @@ var SubscriptionManager = class {
|
|
|
1597
2760
|
};
|
|
1598
2761
|
this.subscriptions.set(id, subscription);
|
|
1599
2762
|
this.bloomDirty = true;
|
|
2763
|
+
const externalCleanup = this.onQuerySubscribed?.(descriptor);
|
|
1600
2764
|
return () => {
|
|
1601
2765
|
this.subscriptions.delete(id);
|
|
1602
2766
|
this.bloomDirty = true;
|
|
2767
|
+
externalCleanup?.();
|
|
1603
2768
|
};
|
|
1604
2769
|
}
|
|
1605
2770
|
/**
|
|
@@ -1620,6 +2785,7 @@ var SubscriptionManager = class {
|
|
|
1620
2785
|
};
|
|
1621
2786
|
this.subscriptions.set(id, subscription);
|
|
1622
2787
|
this.bloomDirty = true;
|
|
2788
|
+
const externalCleanup = this.onQuerySubscribed?.(descriptor);
|
|
1623
2789
|
executeFn().then((results) => {
|
|
1624
2790
|
if (this.subscriptions.has(id)) {
|
|
1625
2791
|
subscription.lastResults = results;
|
|
@@ -1629,6 +2795,7 @@ var SubscriptionManager = class {
|
|
|
1629
2795
|
return () => {
|
|
1630
2796
|
this.subscriptions.delete(id);
|
|
1631
2797
|
this.bloomDirty = true;
|
|
2798
|
+
externalCleanup?.();
|
|
1632
2799
|
};
|
|
1633
2800
|
}
|
|
1634
2801
|
/**
|
|
@@ -1764,10 +2931,7 @@ var SubscriptionManager = class {
|
|
|
1764
2931
|
* any included collection dependencies.
|
|
1765
2932
|
*/
|
|
1766
2933
|
rebuildBloomFilter() {
|
|
1767
|
-
const filter = new SubscriptionBloomFilter(
|
|
1768
|
-
this.bloomExpectedItems,
|
|
1769
|
-
this.bloomFalsePositiveRate
|
|
1770
|
-
);
|
|
2934
|
+
const filter = new SubscriptionBloomFilter(this.bloomExpectedItems, this.bloomFalsePositiveRate);
|
|
1771
2935
|
for (const sub of this.subscriptions.values()) {
|
|
1772
2936
|
filter.add(sub.descriptor.collection);
|
|
1773
2937
|
if (sub.descriptor.includeCollections) {
|
|
@@ -1787,18 +2951,33 @@ var SubscriptionManager = class {
|
|
|
1787
2951
|
});
|
|
1788
2952
|
}
|
|
1789
2953
|
/**
|
|
1790
|
-
* Compare two result sets. Uses
|
|
1791
|
-
*
|
|
2954
|
+
* Compare two result sets. Uses ID-based comparison first (same length, same IDs),
|
|
2955
|
+
* then deep comparison of remaining fields when IDs match.
|
|
2956
|
+
* Avoids JSON.stringify which is O(n) and fragile to field ordering differences.
|
|
1792
2957
|
*/
|
|
1793
2958
|
resultsEqual(prev, next) {
|
|
1794
2959
|
if (prev.length !== next.length) return false;
|
|
1795
2960
|
if (prev.length === 0) return true;
|
|
1796
|
-
|
|
2961
|
+
for (let i = 0; i < prev.length; i++) {
|
|
2962
|
+
if (prev[i]?.id !== next[i]?.id) return false;
|
|
2963
|
+
}
|
|
2964
|
+
for (let i = 0; i < prev.length; i++) {
|
|
2965
|
+
const a = prev[i];
|
|
2966
|
+
const b = next[i];
|
|
2967
|
+
const keysA = Object.keys(a);
|
|
2968
|
+
const keysB = Object.keys(b);
|
|
2969
|
+
if (keysA.length !== keysB.length) return false;
|
|
2970
|
+
for (const key of keysA) {
|
|
2971
|
+
if (a[key] !== b[key]) return false;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return true;
|
|
1797
2975
|
}
|
|
1798
2976
|
};
|
|
1799
2977
|
|
|
1800
2978
|
// src/transaction/transaction-context.ts
|
|
1801
|
-
var
|
|
2979
|
+
var import_core14 = require("@korajs/core");
|
|
2980
|
+
init_serializer();
|
|
1802
2981
|
var TransactionContext = class {
|
|
1803
2982
|
transactionId;
|
|
1804
2983
|
mutationName;
|
|
@@ -1808,7 +2987,7 @@ var TransactionContext = class {
|
|
|
1808
2987
|
config;
|
|
1809
2988
|
constructor(config) {
|
|
1810
2989
|
this.config = config;
|
|
1811
|
-
this.transactionId = (0,
|
|
2990
|
+
this.transactionId = (0, import_core14.generateUUIDv7)();
|
|
1812
2991
|
}
|
|
1813
2992
|
/**
|
|
1814
2993
|
* Set a human-readable mutation name for this transaction.
|
|
@@ -1855,16 +3034,48 @@ var TransactionContext = class {
|
|
|
1855
3034
|
if (this.buffer.length === 0) {
|
|
1856
3035
|
return { operations: [], affectedCollections: /* @__PURE__ */ new Set() };
|
|
1857
3036
|
}
|
|
3037
|
+
const handler = this.config.localMutationHandler;
|
|
3038
|
+
if (handler?.commitTransaction) {
|
|
3039
|
+
return handler.commitTransaction({
|
|
3040
|
+
entries: this.buffer.map((entry) => ({
|
|
3041
|
+
operation: entry.operation,
|
|
3042
|
+
commands: entry.commands,
|
|
3043
|
+
collection: entry.collection
|
|
3044
|
+
})),
|
|
3045
|
+
transactionId: this.transactionId,
|
|
3046
|
+
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
1858
3049
|
const operations = [];
|
|
1859
3050
|
const affectedCollections = /* @__PURE__ */ new Set();
|
|
1860
3051
|
await this.config.adapter.transaction(async (tx) => {
|
|
1861
3052
|
for (const entry of this.buffer) {
|
|
3053
|
+
if (entry.operation.type === "delete" && this.config.relationEnforcer) {
|
|
3054
|
+
const cascadeResult = await this.config.relationEnforcer.enforceDelete(
|
|
3055
|
+
entry.collection,
|
|
3056
|
+
entry.operation.recordId,
|
|
3057
|
+
tx,
|
|
3058
|
+
[entry.operation.id]
|
|
3059
|
+
);
|
|
3060
|
+
for (const cascadedOp of cascadeResult.operations) {
|
|
3061
|
+
operations.push(cascadedOp);
|
|
3062
|
+
affectedCollections.add(cascadedOp.collection);
|
|
3063
|
+
this.config.causalTracker?.afterOperation(cascadedOp.collection, cascadedOp.id, true);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
1862
3066
|
for (const cmd of entry.commands) {
|
|
1863
3067
|
await tx.execute(cmd.sql, cmd.params);
|
|
1864
3068
|
}
|
|
1865
3069
|
operations.push(entry.operation);
|
|
1866
3070
|
affectedCollections.add(entry.collection);
|
|
1867
3071
|
}
|
|
3072
|
+
const finalSeq = this.config.sequenceAllocator.getHighWaterMark();
|
|
3073
|
+
if (finalSeq > 0) {
|
|
3074
|
+
await tx.execute(
|
|
3075
|
+
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
3076
|
+
[this.config.nodeId, finalSeq]
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
1868
3079
|
});
|
|
1869
3080
|
return { operations, affectedCollections };
|
|
1870
3081
|
}
|
|
@@ -1891,16 +3102,17 @@ var TransactionContext = class {
|
|
|
1891
3102
|
}
|
|
1892
3103
|
async insert(collectionName, definition, data) {
|
|
1893
3104
|
this.ensureActive();
|
|
1894
|
-
const validated = (0,
|
|
1895
|
-
const recordId = (0,
|
|
3105
|
+
const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "insert");
|
|
3106
|
+
const recordId = (0, import_core14.generateUUIDv7)();
|
|
1896
3107
|
const now = Date.now();
|
|
1897
3108
|
for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
|
|
1898
3109
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
1899
3110
|
validated[fieldName] = now;
|
|
1900
3111
|
}
|
|
1901
3112
|
}
|
|
1902
|
-
const sequenceNumber = this.config.
|
|
1903
|
-
const
|
|
3113
|
+
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3114
|
+
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3115
|
+
const operation = await (0, import_core14.createOperation)(
|
|
1904
3116
|
{
|
|
1905
3117
|
nodeId: this.config.nodeId,
|
|
1906
3118
|
type: "insert",
|
|
@@ -1909,19 +3121,22 @@ var TransactionContext = class {
|
|
|
1909
3121
|
data: { ...validated },
|
|
1910
3122
|
previousData: null,
|
|
1911
3123
|
sequenceNumber,
|
|
1912
|
-
causalDeps
|
|
3124
|
+
causalDeps,
|
|
1913
3125
|
schemaVersion: this.config.schema.version,
|
|
1914
3126
|
transactionId: this.transactionId,
|
|
1915
3127
|
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
|
|
1916
3128
|
},
|
|
1917
3129
|
this.config.clock
|
|
1918
3130
|
);
|
|
3131
|
+
this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
|
|
1919
3132
|
const serializedData = serializeRecord(validated, definition.fields);
|
|
3133
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1920
3134
|
const record = {
|
|
1921
3135
|
id: recordId,
|
|
1922
3136
|
...serializedData,
|
|
1923
|
-
_created_at:
|
|
1924
|
-
_updated_at:
|
|
3137
|
+
_created_at: operation.timestamp.wallTime,
|
|
3138
|
+
_updated_at: operation.timestamp.wallTime,
|
|
3139
|
+
_version: version
|
|
1925
3140
|
};
|
|
1926
3141
|
const insertQuery = buildInsertQuery(collectionName, record);
|
|
1927
3142
|
const opRow = serializeOperation(operation);
|
|
@@ -1934,11 +3149,7 @@ var TransactionContext = class {
|
|
|
1934
3149
|
collection: collectionName,
|
|
1935
3150
|
commands: [
|
|
1936
3151
|
{ sql: insertQuery.sql, params: insertQuery.params },
|
|
1937
|
-
{ sql: opInsert.sql, params: opInsert.params }
|
|
1938
|
-
{
|
|
1939
|
-
sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
1940
|
-
params: [this.config.nodeId, sequenceNumber]
|
|
1941
|
-
}
|
|
3152
|
+
{ sql: opInsert.sql, params: opInsert.params }
|
|
1942
3153
|
]
|
|
1943
3154
|
});
|
|
1944
3155
|
return {
|
|
@@ -1954,7 +3165,7 @@ var TransactionContext = class {
|
|
|
1954
3165
|
if (!currentRecord) {
|
|
1955
3166
|
throw new RecordNotFoundError(collectionName, id);
|
|
1956
3167
|
}
|
|
1957
|
-
const validated = (0,
|
|
3168
|
+
const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "update");
|
|
1958
3169
|
const now = Date.now();
|
|
1959
3170
|
const previousData = {};
|
|
1960
3171
|
const resolvedData = {};
|
|
@@ -1962,16 +3173,17 @@ var TransactionContext = class {
|
|
|
1962
3173
|
for (const key of Object.keys(validated)) {
|
|
1963
3174
|
const value = validated[key];
|
|
1964
3175
|
previousData[key] = currentRecord[key];
|
|
1965
|
-
if ((0,
|
|
1966
|
-
resolvedData[key] = (0,
|
|
1967
|
-
atomicOps[key] = (0,
|
|
3176
|
+
if ((0, import_core14.isAtomicOp)(value)) {
|
|
3177
|
+
resolvedData[key] = (0, import_core14.resolveAtomicOp)(currentRecord[key], value);
|
|
3178
|
+
atomicOps[key] = (0, import_core14.toAtomicOp)(value);
|
|
1968
3179
|
} else {
|
|
1969
3180
|
resolvedData[key] = value;
|
|
1970
3181
|
}
|
|
1971
3182
|
}
|
|
1972
3183
|
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
1973
|
-
const sequenceNumber = this.config.
|
|
1974
|
-
const
|
|
3184
|
+
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3185
|
+
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3186
|
+
const operation = await (0, import_core14.createOperation)(
|
|
1975
3187
|
{
|
|
1976
3188
|
nodeId: this.config.nodeId,
|
|
1977
3189
|
type: "update",
|
|
@@ -1980,7 +3192,7 @@ var TransactionContext = class {
|
|
|
1980
3192
|
data: { ...resolvedData },
|
|
1981
3193
|
previousData,
|
|
1982
3194
|
sequenceNumber,
|
|
1983
|
-
causalDeps
|
|
3195
|
+
causalDeps,
|
|
1984
3196
|
schemaVersion: this.config.schema.version,
|
|
1985
3197
|
transactionId: this.transactionId,
|
|
1986
3198
|
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {},
|
|
@@ -1988,10 +3200,13 @@ var TransactionContext = class {
|
|
|
1988
3200
|
},
|
|
1989
3201
|
this.config.clock
|
|
1990
3202
|
);
|
|
3203
|
+
this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
|
|
1991
3204
|
const serializedChanges = serializeRecord(resolvedData, definition.fields);
|
|
3205
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1992
3206
|
const updateQuery = buildUpdateQuery(collectionName, id, {
|
|
1993
3207
|
...serializedChanges,
|
|
1994
|
-
_updated_at:
|
|
3208
|
+
_updated_at: operation.timestamp.wallTime,
|
|
3209
|
+
_version: version
|
|
1995
3210
|
});
|
|
1996
3211
|
const opRow = serializeOperation(operation);
|
|
1997
3212
|
const opInsert = buildInsertQuery(
|
|
@@ -2003,11 +3218,7 @@ var TransactionContext = class {
|
|
|
2003
3218
|
collection: collectionName,
|
|
2004
3219
|
commands: [
|
|
2005
3220
|
{ sql: updateQuery.sql, params: updateQuery.params },
|
|
2006
|
-
{ sql: opInsert.sql, params: opInsert.params }
|
|
2007
|
-
{
|
|
2008
|
-
sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
2009
|
-
params: [this.config.nodeId, sequenceNumber]
|
|
2010
|
-
}
|
|
3221
|
+
{ sql: opInsert.sql, params: opInsert.params }
|
|
2011
3222
|
]
|
|
2012
3223
|
});
|
|
2013
3224
|
return {
|
|
@@ -2023,8 +3234,9 @@ var TransactionContext = class {
|
|
|
2023
3234
|
throw new RecordNotFoundError(collectionName, id);
|
|
2024
3235
|
}
|
|
2025
3236
|
const now = Date.now();
|
|
2026
|
-
const sequenceNumber = this.config.
|
|
2027
|
-
const
|
|
3237
|
+
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3238
|
+
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3239
|
+
const operation = await (0, import_core14.createOperation)(
|
|
2028
3240
|
{
|
|
2029
3241
|
nodeId: this.config.nodeId,
|
|
2030
3242
|
type: "delete",
|
|
@@ -2033,13 +3245,14 @@ var TransactionContext = class {
|
|
|
2033
3245
|
data: null,
|
|
2034
3246
|
previousData: null,
|
|
2035
3247
|
sequenceNumber,
|
|
2036
|
-
causalDeps
|
|
3248
|
+
causalDeps,
|
|
2037
3249
|
schemaVersion: this.config.schema.version,
|
|
2038
3250
|
transactionId: this.transactionId,
|
|
2039
3251
|
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
|
|
2040
3252
|
},
|
|
2041
3253
|
this.config.clock
|
|
2042
3254
|
);
|
|
3255
|
+
this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
|
|
2043
3256
|
const deleteQuery = buildSoftDeleteQuery(collectionName, id, now);
|
|
2044
3257
|
const opRow = serializeOperation(operation);
|
|
2045
3258
|
const opInsert = buildInsertQuery(
|
|
@@ -2051,11 +3264,7 @@ var TransactionContext = class {
|
|
|
2051
3264
|
collection: collectionName,
|
|
2052
3265
|
commands: [
|
|
2053
3266
|
{ sql: deleteQuery.sql, params: deleteQuery.params },
|
|
2054
|
-
{ sql: opInsert.sql, params: opInsert.params }
|
|
2055
|
-
{
|
|
2056
|
-
sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
2057
|
-
params: [this.config.nodeId, sequenceNumber]
|
|
2058
|
-
}
|
|
3267
|
+
{ sql: opInsert.sql, params: opInsert.params }
|
|
2059
3268
|
]
|
|
2060
3269
|
});
|
|
2061
3270
|
}
|
|
@@ -2106,25 +3315,86 @@ var TransactionContext = class {
|
|
|
2106
3315
|
}
|
|
2107
3316
|
};
|
|
2108
3317
|
|
|
3318
|
+
// src/transaction/transaction-sequence.ts
|
|
3319
|
+
var TransactionSequenceAllocator = class {
|
|
3320
|
+
constructor(adapter, nodeId) {
|
|
3321
|
+
this.adapter = adapter;
|
|
3322
|
+
this.nodeId = nodeId;
|
|
3323
|
+
}
|
|
3324
|
+
adapter;
|
|
3325
|
+
nodeId;
|
|
3326
|
+
loaded = false;
|
|
3327
|
+
watermark = 0;
|
|
3328
|
+
async allocate() {
|
|
3329
|
+
if (!this.loaded) {
|
|
3330
|
+
this.watermark = await readSequenceNumber(this.adapter, this.nodeId);
|
|
3331
|
+
this.loaded = true;
|
|
3332
|
+
}
|
|
3333
|
+
this.watermark++;
|
|
3334
|
+
return this.watermark;
|
|
3335
|
+
}
|
|
3336
|
+
getHighWaterMark() {
|
|
3337
|
+
return this.watermark;
|
|
3338
|
+
}
|
|
3339
|
+
};
|
|
3340
|
+
|
|
3341
|
+
// src/store/tab-node-id.ts
|
|
3342
|
+
var import_core15 = require("@korajs/core");
|
|
3343
|
+
function perTabStorageKey(dbName) {
|
|
3344
|
+
return `kora-tab-node-${dbName}`;
|
|
3345
|
+
}
|
|
3346
|
+
function loadPerTabNodeId(dbName) {
|
|
3347
|
+
if (typeof sessionStorage === "undefined") {
|
|
3348
|
+
return null;
|
|
3349
|
+
}
|
|
3350
|
+
return sessionStorage.getItem(perTabStorageKey(dbName));
|
|
3351
|
+
}
|
|
3352
|
+
function savePerTabNodeId(dbName, nodeId) {
|
|
3353
|
+
if (typeof sessionStorage === "undefined") {
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
sessionStorage.setItem(perTabStorageKey(dbName), nodeId);
|
|
3357
|
+
}
|
|
3358
|
+
function resolvePerTabNodeId(dbName) {
|
|
3359
|
+
const existing = loadPerTabNodeId(dbName);
|
|
3360
|
+
if (existing) {
|
|
3361
|
+
return existing;
|
|
3362
|
+
}
|
|
3363
|
+
const nodeId = (0, import_core15.generateUUIDv7)();
|
|
3364
|
+
savePerTabNodeId(dbName, nodeId);
|
|
3365
|
+
return nodeId;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
2109
3368
|
// src/store/store.ts
|
|
2110
3369
|
var Store = class {
|
|
2111
3370
|
opened = false;
|
|
2112
3371
|
nodeId = "";
|
|
2113
3372
|
sequenceNumber = 0;
|
|
2114
|
-
versionVector = (0,
|
|
3373
|
+
versionVector = (0, import_core17.createVersionVector)();
|
|
2115
3374
|
clock = null;
|
|
2116
3375
|
collections = /* @__PURE__ */ new Map();
|
|
2117
|
-
subscriptionManager
|
|
3376
|
+
subscriptionManager;
|
|
2118
3377
|
sequenceManager = null;
|
|
2119
3378
|
schema;
|
|
2120
3379
|
adapter;
|
|
2121
3380
|
configNodeId;
|
|
3381
|
+
dbName;
|
|
3382
|
+
isolation;
|
|
2122
3383
|
emitter;
|
|
3384
|
+
localMutationHandler;
|
|
3385
|
+
relationEnforcer = null;
|
|
3386
|
+
causalTracker = null;
|
|
2123
3387
|
constructor(config) {
|
|
2124
3388
|
this.schema = config.schema;
|
|
2125
3389
|
this.adapter = config.adapter;
|
|
2126
3390
|
this.configNodeId = config.nodeId;
|
|
3391
|
+
this.dbName = config.dbName ?? "kora-db";
|
|
3392
|
+
this.isolation = config.isolation ?? "shared";
|
|
2127
3393
|
this.emitter = config.emitter ?? null;
|
|
3394
|
+
this.localMutationHandler = config.localMutationHandler ?? null;
|
|
3395
|
+
this.subscriptionManager = new SubscriptionManager({
|
|
3396
|
+
onQuerySubscribed: config.onQuerySubscribed
|
|
3397
|
+
});
|
|
2128
3398
|
}
|
|
2129
3399
|
/**
|
|
2130
3400
|
* Open the store: initialize the database, load or generate a node ID,
|
|
@@ -2134,18 +3404,18 @@ var Store = class {
|
|
|
2134
3404
|
await this.adapter.open(this.schema);
|
|
2135
3405
|
await this.runMigrationsIfNeeded();
|
|
2136
3406
|
this.nodeId = await this.loadOrGenerateNodeId();
|
|
2137
|
-
this.clock = new
|
|
3407
|
+
this.clock = new import_core17.HybridLogicalClock(this.nodeId);
|
|
3408
|
+
this.causalTracker = new import_core17.CausalTracker();
|
|
2138
3409
|
this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
|
|
2139
3410
|
this.sequenceNumber = await this.loadSequenceNumber();
|
|
2140
3411
|
this.versionVector = await this.loadVersionVector();
|
|
2141
3412
|
const hasRelations = Object.keys(this.schema.relations).length > 0;
|
|
2142
|
-
|
|
3413
|
+
this.relationEnforcer = hasRelations ? new RelationEnforcer({
|
|
2143
3414
|
schema: this.schema,
|
|
2144
3415
|
adapter: this.adapter,
|
|
2145
3416
|
clock: this.clock,
|
|
2146
|
-
nodeId: this.nodeId
|
|
2147
|
-
|
|
2148
|
-
}) : void 0;
|
|
3417
|
+
nodeId: this.nodeId
|
|
3418
|
+
}) : null;
|
|
2149
3419
|
for (const [name, definition] of Object.entries(this.schema.collections)) {
|
|
2150
3420
|
const col = new Collection(
|
|
2151
3421
|
name,
|
|
@@ -2154,14 +3424,17 @@ var Store = class {
|
|
|
2154
3424
|
this.adapter,
|
|
2155
3425
|
this.clock,
|
|
2156
3426
|
this.nodeId,
|
|
2157
|
-
() => this.
|
|
3427
|
+
() => this.allocateSequenceNumber(),
|
|
2158
3428
|
(collectionName, operation) => {
|
|
3429
|
+
this.recordOperationSequence(operation);
|
|
2159
3430
|
this.subscriptionManager.notify(collectionName, operation);
|
|
2160
3431
|
if (this.emitter) {
|
|
2161
3432
|
this.emitter.emit({ type: "operation:created", operation });
|
|
2162
3433
|
}
|
|
2163
3434
|
},
|
|
2164
|
-
relationEnforcer
|
|
3435
|
+
this.relationEnforcer,
|
|
3436
|
+
this.localMutationHandler,
|
|
3437
|
+
this.causalTracker
|
|
2165
3438
|
);
|
|
2166
3439
|
this.collections.set(name, col);
|
|
2167
3440
|
}
|
|
@@ -2209,112 +3482,358 @@ var Store = class {
|
|
|
2209
3482
|
};
|
|
2210
3483
|
}
|
|
2211
3484
|
/**
|
|
2212
|
-
* Get the current version vector.
|
|
3485
|
+
* Get the current version vector.
|
|
3486
|
+
*/
|
|
3487
|
+
getVersionVector() {
|
|
3488
|
+
this.ensureOpen();
|
|
3489
|
+
return new Map(this.versionVector);
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Get the node ID for this store instance.
|
|
3493
|
+
*/
|
|
3494
|
+
getNodeId() {
|
|
3495
|
+
this.ensureOpen();
|
|
3496
|
+
return this.nodeId;
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Apply a remote operation received from sync.
|
|
3500
|
+
* Checks for duplicates, applies to the data table, persists the operation,
|
|
3501
|
+
* and updates the version vector.
|
|
3502
|
+
*/
|
|
3503
|
+
async applyRemoteOperation(op, options) {
|
|
3504
|
+
this.ensureOpen();
|
|
3505
|
+
const collection = op.collection;
|
|
3506
|
+
const definition = this.schema.collections[collection];
|
|
3507
|
+
if (!definition) {
|
|
3508
|
+
return "skipped";
|
|
3509
|
+
}
|
|
3510
|
+
const existing = await this.adapter.query(
|
|
3511
|
+
`SELECT id FROM _kora_ops_${collection} WHERE id = ?`,
|
|
3512
|
+
[op.id]
|
|
3513
|
+
);
|
|
3514
|
+
if (existing.length > 0) {
|
|
3515
|
+
return "duplicate";
|
|
3516
|
+
}
|
|
3517
|
+
if (this.clock) {
|
|
3518
|
+
this.clock.receive(op.timestamp);
|
|
3519
|
+
}
|
|
3520
|
+
const remoteVersion = serializeRowVersion(op.timestamp);
|
|
3521
|
+
const wallTime = op.timestamp.wallTime;
|
|
3522
|
+
await this.adapter.transaction(async (tx) => {
|
|
3523
|
+
if (op.type === "insert" && op.data) {
|
|
3524
|
+
const existing2 = await tx.query(
|
|
3525
|
+
`SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
|
|
3526
|
+
[op.recordId]
|
|
3527
|
+
);
|
|
3528
|
+
const row = existing2[0];
|
|
3529
|
+
if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
|
|
3530
|
+
} else {
|
|
3531
|
+
const serializedData = serializeRecord(op.data, definition.fields);
|
|
3532
|
+
const record = {
|
|
3533
|
+
id: op.recordId,
|
|
3534
|
+
...serializedData,
|
|
3535
|
+
_created_at: wallTime,
|
|
3536
|
+
_updated_at: wallTime,
|
|
3537
|
+
_version: remoteVersion
|
|
3538
|
+
};
|
|
3539
|
+
const insertQuery = buildInsertQuery(collection, record);
|
|
3540
|
+
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
3541
|
+
}
|
|
3542
|
+
} else if (op.type === "update" && op.data) {
|
|
3543
|
+
const serializedChanges = serializeRecord(op.data, definition.fields);
|
|
3544
|
+
const updatePayload = {
|
|
3545
|
+
...serializedChanges,
|
|
3546
|
+
_updated_at: wallTime,
|
|
3547
|
+
_version: remoteVersion
|
|
3548
|
+
};
|
|
3549
|
+
if (options?.reactivateIfDeleted) {
|
|
3550
|
+
updatePayload._deleted = 0;
|
|
3551
|
+
}
|
|
3552
|
+
const updateQuery = buildLwwUpdateQuery(
|
|
3553
|
+
collection,
|
|
3554
|
+
op.recordId,
|
|
3555
|
+
updatePayload,
|
|
3556
|
+
remoteVersion
|
|
3557
|
+
);
|
|
3558
|
+
await tx.execute(updateQuery.sql, updateQuery.params);
|
|
3559
|
+
} else if (op.type === "delete") {
|
|
3560
|
+
const deleteQuery = buildLwwSoftDeleteQuery(
|
|
3561
|
+
collection,
|
|
3562
|
+
op.recordId,
|
|
3563
|
+
wallTime,
|
|
3564
|
+
remoteVersion
|
|
3565
|
+
);
|
|
3566
|
+
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
3567
|
+
}
|
|
3568
|
+
const opRow = serializeOperation(op);
|
|
3569
|
+
const opInsert = buildInsertQuery(
|
|
3570
|
+
`_kora_ops_${collection}`,
|
|
3571
|
+
opRow
|
|
3572
|
+
);
|
|
3573
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
3574
|
+
const currentSeq = this.versionVector.get(op.nodeId) ?? 0;
|
|
3575
|
+
if (op.sequenceNumber > currentSeq) {
|
|
3576
|
+
this.versionVector.set(op.nodeId, op.sequenceNumber);
|
|
3577
|
+
await tx.execute(
|
|
3578
|
+
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
3579
|
+
[op.nodeId, op.sequenceNumber]
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
});
|
|
3583
|
+
this.subscriptionManager.notify(collection, op);
|
|
3584
|
+
return "applied";
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Get operations from a node within a sequence number range.
|
|
3588
|
+
* Implements the OperationLog interface for computeDelta.
|
|
3589
|
+
*/
|
|
3590
|
+
async getRange(nodeId, fromSeq, toSeq) {
|
|
3591
|
+
return this.getOperationRange(nodeId, fromSeq, toSeq);
|
|
3592
|
+
}
|
|
3593
|
+
/**
|
|
3594
|
+
* Get operations from a node within a sequence number range.
|
|
3595
|
+
*/
|
|
3596
|
+
async getOperationRange(nodeId, fromSeq, toSeq) {
|
|
3597
|
+
this.ensureOpen();
|
|
3598
|
+
const allOps = [];
|
|
3599
|
+
for (const collectionName of Object.keys(this.schema.collections)) {
|
|
3600
|
+
const rows = await this.adapter.query(
|
|
3601
|
+
`SELECT * FROM _kora_ops_${collectionName} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
|
|
3602
|
+
[nodeId, fromSeq, toSeq]
|
|
3603
|
+
);
|
|
3604
|
+
for (const row of rows) {
|
|
3605
|
+
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
allOps.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
3609
|
+
return allOps;
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Load every operation from the local append-only log across all collections.
|
|
3613
|
+
* Used by sync delta computation, backup export, and time-travel replay.
|
|
3614
|
+
*/
|
|
3615
|
+
async getAllOperations() {
|
|
3616
|
+
this.ensureOpen();
|
|
3617
|
+
const allOps = [];
|
|
3618
|
+
for (const collectionName of Object.keys(this.schema.collections)) {
|
|
3619
|
+
const rows = await this.adapter.query(
|
|
3620
|
+
`SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
|
|
3621
|
+
);
|
|
3622
|
+
for (const row of rows) {
|
|
3623
|
+
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
return allOps;
|
|
3627
|
+
}
|
|
3628
|
+
/**
|
|
3629
|
+
* Rebuild an in-memory snapshot of materialized state at a causal cut in the op log.
|
|
3630
|
+
* Does not mutate the live store — intended for DevTools time-travel inspection.
|
|
3631
|
+
*
|
|
3632
|
+
* @param operationId - Content-addressed id of the operation to replay through (inclusive)
|
|
3633
|
+
* @throws {OperationError} When the operation id is not present in the local log
|
|
3634
|
+
*/
|
|
3635
|
+
async replayTo(operationId) {
|
|
3636
|
+
this.ensureOpen();
|
|
3637
|
+
const start = Date.now();
|
|
3638
|
+
const allOps = await this.getAllOperations();
|
|
3639
|
+
const snapshot = buildReplaySnapshot(this.schema, allOps, operationId);
|
|
3640
|
+
if (this.emitter) {
|
|
3641
|
+
this.emitter.emit({
|
|
3642
|
+
type: "replay:completed",
|
|
3643
|
+
targetOperationId: operationId,
|
|
3644
|
+
operationsApplied: snapshot.operationsApplied.length,
|
|
3645
|
+
duration: Date.now() - start
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
return snapshot;
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3651
|
+
* Persist a merge trace to the durable audit log.
|
|
3652
|
+
*/
|
|
3653
|
+
async appendAuditTrace(trace) {
|
|
3654
|
+
this.ensureOpen();
|
|
3655
|
+
const { appendAuditTrace: append } = await Promise.resolve().then(() => (init_audit_trace_store(), audit_trace_store_exports));
|
|
3656
|
+
await append(this.adapter, trace);
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Read persisted audit traces with optional filters.
|
|
3660
|
+
*/
|
|
3661
|
+
async getAuditTraces(query) {
|
|
3662
|
+
this.ensureOpen();
|
|
3663
|
+
const { readAuditTraces: readAuditTraces2 } = await Promise.resolve().then(() => (init_audit_trace_store(), audit_trace_store_exports));
|
|
3664
|
+
return readAuditTraces2(this.adapter, query);
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Export operations and merge traces as a portable audit bundle.
|
|
3668
|
+
*/
|
|
3669
|
+
async exportAudit(options) {
|
|
3670
|
+
this.ensureOpen();
|
|
3671
|
+
const { exportAudit: doExport } = await Promise.resolve().then(() => (init_export_audit(), export_audit_exports));
|
|
3672
|
+
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
3673
|
+
}
|
|
3674
|
+
/**
|
|
3675
|
+
* Get the schema definition.
|
|
3676
|
+
*/
|
|
3677
|
+
getSchema() {
|
|
3678
|
+
return this.schema;
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Route local CRUD through the unified apply pipeline (korajs ApplyPipeline).
|
|
3682
|
+
*/
|
|
3683
|
+
setLocalMutationHandler(handler) {
|
|
3684
|
+
this.localMutationHandler = handler;
|
|
3685
|
+
for (const col of this.collections.values()) {
|
|
3686
|
+
col.setMutationHandler(handler);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
/**
|
|
3690
|
+
* Build mutation context for a collection (used by ApplyPipeline side effects).
|
|
3691
|
+
*/
|
|
3692
|
+
createMutationContext(collection, options) {
|
|
3693
|
+
this.ensureOpen();
|
|
3694
|
+
const definition = this.schema.collections[collection];
|
|
3695
|
+
if (!definition || !this.clock) {
|
|
3696
|
+
throw new StoreNotOpenError();
|
|
3697
|
+
}
|
|
3698
|
+
return {
|
|
3699
|
+
collection,
|
|
3700
|
+
definition,
|
|
3701
|
+
schema: this.schema,
|
|
3702
|
+
adapter: this.adapter,
|
|
3703
|
+
clock: this.clock,
|
|
3704
|
+
nodeId: this.nodeId,
|
|
3705
|
+
allocateSequenceNumber: () => this.allocateSequenceNumber(),
|
|
3706
|
+
onMutation: (collectionName, operation) => {
|
|
3707
|
+
this.recordOperationSequence(operation);
|
|
3708
|
+
this.subscriptionManager.notify(collectionName, operation);
|
|
3709
|
+
if (this.emitter) {
|
|
3710
|
+
this.emitter.emit({ type: "operation:created", operation });
|
|
3711
|
+
}
|
|
3712
|
+
},
|
|
3713
|
+
relationEnforcer: this.relationEnforcer,
|
|
3714
|
+
causalTracker: this.causalTracker,
|
|
3715
|
+
inTransaction: options?.inTransaction ?? false,
|
|
3716
|
+
extraCausalDeps: options?.extraCausalDeps
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Load a materialized row by ID, including soft-deleted tombstones.
|
|
3721
|
+
*/
|
|
3722
|
+
async findMaterializedRow(collection, recordId) {
|
|
3723
|
+
this.ensureOpen();
|
|
3724
|
+
const definition = this.schema.collections[collection];
|
|
3725
|
+
if (!definition) {
|
|
3726
|
+
return null;
|
|
3727
|
+
}
|
|
3728
|
+
const rows = await this.adapter.query(
|
|
3729
|
+
`SELECT * FROM ${collection} WHERE id = ?`,
|
|
3730
|
+
[recordId]
|
|
3731
|
+
);
|
|
3732
|
+
const row = rows[0];
|
|
3733
|
+
if (!row) {
|
|
3734
|
+
return null;
|
|
3735
|
+
}
|
|
3736
|
+
return {
|
|
3737
|
+
record: deserializeRecord(row, definition.fields),
|
|
3738
|
+
deleted: row._deleted === 1
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Latest operation from this device for a record (used for delete-vs-update merge on sync).
|
|
2213
3743
|
*/
|
|
2214
|
-
|
|
3744
|
+
/**
|
|
3745
|
+
* Load the last server version vector acknowledged by this client (persisted in `_kora_meta`).
|
|
3746
|
+
*/
|
|
3747
|
+
async loadLastAckedServerVector() {
|
|
2215
3748
|
this.ensureOpen();
|
|
2216
|
-
return
|
|
3749
|
+
return loadLastAckedServerVector(this.adapter);
|
|
2217
3750
|
}
|
|
2218
3751
|
/**
|
|
2219
|
-
*
|
|
3752
|
+
* Persist the last server version vector this client believes the server has applied.
|
|
2220
3753
|
*/
|
|
2221
|
-
|
|
3754
|
+
async saveLastAckedServerVector(vector) {
|
|
2222
3755
|
this.ensureOpen();
|
|
2223
|
-
|
|
3756
|
+
await saveLastAckedServerVector(this.adapter, vector);
|
|
2224
3757
|
}
|
|
2225
3758
|
/**
|
|
2226
|
-
*
|
|
2227
|
-
* Checks for duplicates, applies to the data table, persists the operation,
|
|
2228
|
-
* and updates the version vector.
|
|
3759
|
+
* Load persisted delta cursor for resuming paginated initial sync.
|
|
2229
3760
|
*/
|
|
2230
|
-
async
|
|
3761
|
+
async loadDeltaCursor() {
|
|
2231
3762
|
this.ensureOpen();
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
3763
|
+
return loadDeltaCursor(this.adapter);
|
|
3764
|
+
}
|
|
3765
|
+
/**
|
|
3766
|
+
* Persist or clear the delta cursor for paginated initial sync resume.
|
|
3767
|
+
*/
|
|
3768
|
+
async saveDeltaCursor(cursor) {
|
|
3769
|
+
this.ensureOpen();
|
|
3770
|
+
await saveDeltaCursor(this.adapter, cursor);
|
|
3771
|
+
}
|
|
3772
|
+
/**
|
|
3773
|
+
* Local operations not yet reflected on the server version vector.
|
|
3774
|
+
*/
|
|
3775
|
+
async getUnsyncedOperations(serverVector) {
|
|
3776
|
+
this.ensureOpen();
|
|
3777
|
+
return collectOperationsAheadOfServer(
|
|
3778
|
+
this.getVersionVector(),
|
|
3779
|
+
serverVector,
|
|
3780
|
+
(nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
|
|
2240
3781
|
);
|
|
2241
|
-
if (existing.length > 0) {
|
|
2242
|
-
return "duplicate";
|
|
2243
|
-
}
|
|
2244
|
-
if (this.clock) {
|
|
2245
|
-
this.clock.receive(op.timestamp);
|
|
2246
|
-
}
|
|
2247
|
-
await this.adapter.transaction(async (tx) => {
|
|
2248
|
-
if (op.type === "insert" && op.data) {
|
|
2249
|
-
const serializedData = serializeRecord(op.data, definition.fields);
|
|
2250
|
-
const now = op.timestamp.wallTime;
|
|
2251
|
-
const record = {
|
|
2252
|
-
id: op.recordId,
|
|
2253
|
-
...serializedData,
|
|
2254
|
-
_created_at: now,
|
|
2255
|
-
_updated_at: now
|
|
2256
|
-
};
|
|
2257
|
-
const insertQuery = buildInsertQuery(collection, record);
|
|
2258
|
-
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
2259
|
-
} else if (op.type === "update" && op.data) {
|
|
2260
|
-
const serializedChanges = serializeRecord(op.data, definition.fields);
|
|
2261
|
-
const updateQuery = buildUpdateQuery(collection, op.recordId, {
|
|
2262
|
-
...serializedChanges,
|
|
2263
|
-
_updated_at: op.timestamp.wallTime
|
|
2264
|
-
});
|
|
2265
|
-
await tx.execute(updateQuery.sql, updateQuery.params);
|
|
2266
|
-
} else if (op.type === "delete") {
|
|
2267
|
-
const deleteQuery = buildSoftDeleteQuery(collection, op.recordId, op.timestamp.wallTime);
|
|
2268
|
-
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
2269
|
-
}
|
|
2270
|
-
const opRow = serializeOperation(op);
|
|
2271
|
-
const opInsert = buildInsertQuery(
|
|
2272
|
-
`_kora_ops_${collection}`,
|
|
2273
|
-
opRow
|
|
2274
|
-
);
|
|
2275
|
-
await tx.execute(opInsert.sql, opInsert.params);
|
|
2276
|
-
const currentSeq = this.versionVector.get(op.nodeId) ?? 0;
|
|
2277
|
-
if (op.sequenceNumber > currentSeq) {
|
|
2278
|
-
this.versionVector.set(op.nodeId, op.sequenceNumber);
|
|
2279
|
-
await tx.execute(
|
|
2280
|
-
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
2281
|
-
[op.nodeId, op.sequenceNumber]
|
|
2282
|
-
);
|
|
2283
|
-
}
|
|
2284
|
-
});
|
|
2285
|
-
this.subscriptionManager.notify(collection, op);
|
|
2286
|
-
return "applied";
|
|
2287
3782
|
}
|
|
2288
3783
|
/**
|
|
2289
|
-
*
|
|
2290
|
-
* Implements the OperationLog interface for computeDelta.
|
|
3784
|
+
* Count of local operations ahead of the server version vector.
|
|
2291
3785
|
*/
|
|
2292
|
-
|
|
2293
|
-
|
|
3786
|
+
async countUnsyncedOperations(serverVector) {
|
|
3787
|
+
const ops = await this.getUnsyncedOperations(serverVector);
|
|
3788
|
+
return ops.length;
|
|
2294
3789
|
}
|
|
2295
3790
|
/**
|
|
2296
|
-
*
|
|
3791
|
+
* Compact the local operation log using materialized rows as the baseline.
|
|
3792
|
+
* Only removes ops the server has acknowledged (per {@link CompactionStrategy}).
|
|
2297
3793
|
*/
|
|
2298
|
-
async
|
|
3794
|
+
async compact(strategy) {
|
|
2299
3795
|
this.ensureOpen();
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const rows = await this.adapter.query(
|
|
2303
|
-
`SELECT * FROM _kora_ops_${collectionName} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
|
|
2304
|
-
[nodeId, fromSeq, toSeq]
|
|
2305
|
-
);
|
|
2306
|
-
for (const row of rows) {
|
|
2307
|
-
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
2308
|
-
}
|
|
3796
|
+
if (strategy.mode === "never") {
|
|
3797
|
+
return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core17.createVersionVector)());
|
|
2309
3798
|
}
|
|
2310
|
-
|
|
2311
|
-
return
|
|
3799
|
+
const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
|
|
3800
|
+
return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
|
|
2312
3801
|
}
|
|
2313
3802
|
/**
|
|
2314
|
-
*
|
|
3803
|
+
* Merge session remote vector with persisted last-acked vector (max per node).
|
|
2315
3804
|
*/
|
|
2316
|
-
|
|
2317
|
-
return
|
|
3805
|
+
mergeServerVectors(sessionVector, persistedVector) {
|
|
3806
|
+
return mergeVersionVectors(persistedVector, sessionVector);
|
|
3807
|
+
}
|
|
3808
|
+
async getLatestLocalOperationForRecord(collection, recordId) {
|
|
3809
|
+
this.ensureOpen();
|
|
3810
|
+
const rows = await this.adapter.query(
|
|
3811
|
+
`SELECT * FROM _kora_ops_${collection} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
|
|
3812
|
+
[this.nodeId, recordId]
|
|
3813
|
+
);
|
|
3814
|
+
const row = rows[0];
|
|
3815
|
+
if (!row) {
|
|
3816
|
+
return null;
|
|
3817
|
+
}
|
|
3818
|
+
return deserializeOperationWithCollection(row, collection);
|
|
3819
|
+
}
|
|
3820
|
+
/**
|
|
3821
|
+
* Latest operation for a record from any node (for 3-way merge when local op log is empty).
|
|
3822
|
+
*/
|
|
3823
|
+
async getLatestOperationForRecord(collection, recordId) {
|
|
3824
|
+
this.ensureOpen();
|
|
3825
|
+
const rows = await this.adapter.query(
|
|
3826
|
+
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ?`,
|
|
3827
|
+
[recordId]
|
|
3828
|
+
);
|
|
3829
|
+
let latest = null;
|
|
3830
|
+
for (const row of rows) {
|
|
3831
|
+
const op = deserializeOperationWithCollection(row, collection);
|
|
3832
|
+
if (!latest || import_core17.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
|
|
3833
|
+
latest = op;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return latest;
|
|
2318
3837
|
}
|
|
2319
3838
|
/** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
|
|
2320
3839
|
getSubscriptionManager() {
|
|
@@ -2348,7 +3867,10 @@ var Store = class {
|
|
|
2348
3867
|
adapter: this.adapter,
|
|
2349
3868
|
clock: this.clock,
|
|
2350
3869
|
nodeId: this.nodeId,
|
|
2351
|
-
|
|
3870
|
+
sequenceAllocator: new TransactionSequenceAllocator(this.adapter, this.nodeId),
|
|
3871
|
+
relationEnforcer: this.relationEnforcer,
|
|
3872
|
+
causalTracker: this.causalTracker,
|
|
3873
|
+
localMutationHandler: this.localMutationHandler
|
|
2352
3874
|
});
|
|
2353
3875
|
}
|
|
2354
3876
|
/**
|
|
@@ -2363,10 +3885,13 @@ var Store = class {
|
|
|
2363
3885
|
*/
|
|
2364
3886
|
async transaction(fn) {
|
|
2365
3887
|
const tx = this.createTransaction();
|
|
3888
|
+
this.causalTracker?.beginTransaction();
|
|
2366
3889
|
try {
|
|
2367
3890
|
await fn(tx);
|
|
2368
|
-
const { operations
|
|
3891
|
+
const { operations } = await tx.commit();
|
|
3892
|
+
this.causalTracker?.clearTransaction();
|
|
2369
3893
|
for (const op of operations) {
|
|
3894
|
+
this.recordOperationSequence(op);
|
|
2370
3895
|
this.subscriptionManager.notify(op.collection, op);
|
|
2371
3896
|
if (this.emitter) {
|
|
2372
3897
|
this.emitter.emit({ type: "operation:created", operation: op });
|
|
@@ -2375,13 +3900,57 @@ var Store = class {
|
|
|
2375
3900
|
return operations;
|
|
2376
3901
|
} catch (error) {
|
|
2377
3902
|
tx.rollback();
|
|
3903
|
+
this.causalTracker?.clearTransaction();
|
|
2378
3904
|
throw error;
|
|
2379
3905
|
}
|
|
2380
3906
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
3907
|
+
/**
|
|
3908
|
+
* Export all data as a portable backup binary.
|
|
3909
|
+
* Includes operations, version vector, metadata, and optionally materialized records.
|
|
3910
|
+
*
|
|
3911
|
+
* @param options - Backup options (includeRecords, collections, onProgress)
|
|
3912
|
+
* @returns Backup as a Uint8Array
|
|
3913
|
+
*/
|
|
3914
|
+
async exportBackup(options) {
|
|
3915
|
+
this.ensureOpen();
|
|
3916
|
+
const { exportBackup: doExport } = await Promise.resolve().then(() => (init_backup(), backup_exports));
|
|
3917
|
+
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* Restore data from a backup binary.
|
|
3921
|
+
*
|
|
3922
|
+
* @param data - The backup data
|
|
3923
|
+
* @param options - Restore options (merge, collections, onProgress)
|
|
3924
|
+
* @returns Result of the restore operation
|
|
3925
|
+
*/
|
|
3926
|
+
async importBackup(data, options) {
|
|
3927
|
+
this.ensureOpen();
|
|
3928
|
+
const { restoreBackup: doRestore } = await Promise.resolve().then(() => (init_backup(), backup_exports));
|
|
3929
|
+
return doRestore(this.adapter, this.schema, data, options);
|
|
3930
|
+
}
|
|
3931
|
+
/**
|
|
3932
|
+
* Read backup manifest without loading the entire backup.
|
|
3933
|
+
*
|
|
3934
|
+
* @param data - The raw backup data
|
|
3935
|
+
* @returns The backup manifest
|
|
3936
|
+
*/
|
|
3937
|
+
static readBackupManifest(data) {
|
|
3938
|
+
return readBackupManifest(data);
|
|
3939
|
+
}
|
|
3940
|
+
recordOperationSequence(operation) {
|
|
3941
|
+
const prev = this.versionVector.get(operation.nodeId) ?? 0;
|
|
3942
|
+
if (operation.sequenceNumber > prev) {
|
|
3943
|
+
this.versionVector.set(operation.nodeId, operation.sequenceNumber);
|
|
3944
|
+
}
|
|
3945
|
+
if (operation.nodeId === this.nodeId) {
|
|
3946
|
+
this.sequenceNumber = Math.max(this.sequenceNumber, operation.sequenceNumber);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
async allocateSequenceNumber() {
|
|
3950
|
+
const seq = await allocateNextSequenceNumber(this.adapter, this.nodeId);
|
|
3951
|
+
this.sequenceNumber = seq;
|
|
3952
|
+
this.versionVector.set(this.nodeId, seq);
|
|
3953
|
+
return seq;
|
|
2385
3954
|
}
|
|
2386
3955
|
/**
|
|
2387
3956
|
* Check the stored schema version and run any pending migrations.
|
|
@@ -2403,7 +3972,7 @@ var Store = class {
|
|
|
2403
3972
|
for (let v = storedVersion + 1; v <= targetVersion; v++) {
|
|
2404
3973
|
const migration = migrations[v];
|
|
2405
3974
|
if (!migration) continue;
|
|
2406
|
-
const sqlStatements = (0,
|
|
3975
|
+
const sqlStatements = (0, import_core17.migrationStepsToSQL)(migration.steps);
|
|
2407
3976
|
for (const sql of sqlStatements) {
|
|
2408
3977
|
try {
|
|
2409
3978
|
await this.adapter.execute(sql);
|
|
@@ -2464,19 +4033,24 @@ var Store = class {
|
|
|
2464
4033
|
}
|
|
2465
4034
|
async loadOrGenerateNodeId() {
|
|
2466
4035
|
if (this.configNodeId) {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
4036
|
+
if (this.isolation !== "per-tab") {
|
|
4037
|
+
await this.adapter.execute(
|
|
4038
|
+
"INSERT OR REPLACE INTO _kora_meta (key, value) VALUES ('node_id', ?)",
|
|
4039
|
+
[this.configNodeId]
|
|
4040
|
+
);
|
|
4041
|
+
}
|
|
2471
4042
|
return this.configNodeId;
|
|
2472
4043
|
}
|
|
4044
|
+
if (this.isolation === "per-tab") {
|
|
4045
|
+
return resolvePerTabNodeId(this.dbName);
|
|
4046
|
+
}
|
|
2473
4047
|
const rows = await this.adapter.query(
|
|
2474
4048
|
"SELECT value FROM _kora_meta WHERE key = 'node_id'"
|
|
2475
4049
|
);
|
|
2476
4050
|
if (rows[0]) {
|
|
2477
4051
|
return rows[0].value;
|
|
2478
4052
|
}
|
|
2479
|
-
const newNodeId = (0,
|
|
4053
|
+
const newNodeId = (0, import_core17.generateUUIDv7)();
|
|
2480
4054
|
await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
|
|
2481
4055
|
newNodeId
|
|
2482
4056
|
]);
|
|
@@ -2493,7 +4067,7 @@ var Store = class {
|
|
|
2493
4067
|
const rows = await this.adapter.query(
|
|
2494
4068
|
"SELECT node_id, sequence_number FROM _kora_version_vector"
|
|
2495
4069
|
);
|
|
2496
|
-
const vector = (0,
|
|
4070
|
+
const vector = (0, import_core17.createVersionVector)();
|
|
2497
4071
|
for (const row of rows) {
|
|
2498
4072
|
vector.set(row.node_id, row.sequence_number);
|
|
2499
4073
|
}
|
|
@@ -2505,14 +4079,506 @@ var Store = class {
|
|
|
2505
4079
|
}
|
|
2506
4080
|
}
|
|
2507
4081
|
};
|
|
4082
|
+
|
|
4083
|
+
// src/reactivity/query-store.ts
|
|
4084
|
+
var EMPTY_ARRAY = Object.freeze([]);
|
|
4085
|
+
var QueryStore = class {
|
|
4086
|
+
snapshot = EMPTY_ARRAY;
|
|
4087
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4088
|
+
unsubscribeQuery = null;
|
|
4089
|
+
active = false;
|
|
4090
|
+
queryBuilder;
|
|
4091
|
+
constructor(queryBuilder) {
|
|
4092
|
+
this.queryBuilder = queryBuilder;
|
|
4093
|
+
}
|
|
4094
|
+
/**
|
|
4095
|
+
* Subscribe to snapshot changes.
|
|
4096
|
+
*
|
|
4097
|
+
* @returns Unsubscribe function
|
|
4098
|
+
*/
|
|
4099
|
+
subscribe = (onStoreChange) => {
|
|
4100
|
+
this.listeners.add(onStoreChange);
|
|
4101
|
+
if (!this.active) {
|
|
4102
|
+
this.startSubscription();
|
|
4103
|
+
}
|
|
4104
|
+
return () => {
|
|
4105
|
+
this.listeners.delete(onStoreChange);
|
|
4106
|
+
if (this.listeners.size === 0) {
|
|
4107
|
+
this.stopSubscription();
|
|
4108
|
+
}
|
|
4109
|
+
};
|
|
4110
|
+
};
|
|
4111
|
+
/** Synchronous read of the latest query results. */
|
|
4112
|
+
getSnapshot = () => {
|
|
4113
|
+
return this.snapshot;
|
|
4114
|
+
};
|
|
4115
|
+
/** Tear down listeners and the underlying query subscription. */
|
|
4116
|
+
destroy() {
|
|
4117
|
+
this.stopSubscription();
|
|
4118
|
+
this.listeners.clear();
|
|
4119
|
+
this.snapshot = EMPTY_ARRAY;
|
|
4120
|
+
}
|
|
4121
|
+
startSubscription() {
|
|
4122
|
+
this.active = true;
|
|
4123
|
+
this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
|
|
4124
|
+
if (!this.active) return;
|
|
4125
|
+
this.snapshot = Object.freeze([...results]);
|
|
4126
|
+
this.notifyListeners();
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
stopSubscription() {
|
|
4130
|
+
this.active = false;
|
|
4131
|
+
if (this.unsubscribeQuery) {
|
|
4132
|
+
this.unsubscribeQuery();
|
|
4133
|
+
this.unsubscribeQuery = null;
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
notifyListeners() {
|
|
4137
|
+
for (const listener of this.listeners) {
|
|
4138
|
+
listener();
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
|
|
4143
|
+
// src/reactivity/assert-query-ready.ts
|
|
4144
|
+
var import_core18 = require("@korajs/core");
|
|
4145
|
+
var LEGACY_PENDING_COLLECTION = "__pending__";
|
|
4146
|
+
function assertQueryReady(query) {
|
|
4147
|
+
const descriptor = query.getDescriptor();
|
|
4148
|
+
if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
|
|
4149
|
+
throw new import_core18.AppNotReadyError(
|
|
4150
|
+
"Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
// src/reactivity/query-store-cache.ts
|
|
4156
|
+
var QueryStoreCache = class {
|
|
4157
|
+
constructor(scopeKey = "default") {
|
|
4158
|
+
this.scopeKey = scopeKey;
|
|
4159
|
+
}
|
|
4160
|
+
scopeKey;
|
|
4161
|
+
entries = /* @__PURE__ */ new Map();
|
|
4162
|
+
getOrCreate(queryBuilder) {
|
|
4163
|
+
const key = this.getKey(queryBuilder);
|
|
4164
|
+
const existing = this.entries.get(key);
|
|
4165
|
+
if (existing) {
|
|
4166
|
+
existing.refCount++;
|
|
4167
|
+
return existing.queryStore;
|
|
4168
|
+
}
|
|
4169
|
+
const queryStore = new QueryStore(queryBuilder);
|
|
4170
|
+
this.entries.set(key, {
|
|
4171
|
+
queryStore,
|
|
4172
|
+
refCount: 1
|
|
4173
|
+
});
|
|
4174
|
+
return queryStore;
|
|
4175
|
+
}
|
|
4176
|
+
release(queryBuilder) {
|
|
4177
|
+
const key = this.getKey(queryBuilder);
|
|
4178
|
+
const entry = this.entries.get(key);
|
|
4179
|
+
if (!entry) {
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
entry.refCount--;
|
|
4183
|
+
if (entry.refCount <= 0) {
|
|
4184
|
+
entry.queryStore.destroy();
|
|
4185
|
+
this.entries.delete(key);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
clear() {
|
|
4189
|
+
for (const entry of this.entries.values()) {
|
|
4190
|
+
entry.queryStore.destroy();
|
|
4191
|
+
}
|
|
4192
|
+
this.entries.clear();
|
|
4193
|
+
}
|
|
4194
|
+
get size() {
|
|
4195
|
+
return this.entries.size;
|
|
4196
|
+
}
|
|
4197
|
+
getKey(queryBuilder) {
|
|
4198
|
+
return `${this.scopeKey}:${JSON.stringify(queryBuilder.getDescriptor())}`;
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4201
|
+
var sharedCache = null;
|
|
4202
|
+
function getSharedQueryStoreCache() {
|
|
4203
|
+
if (!sharedCache) {
|
|
4204
|
+
sharedCache = new QueryStoreCache();
|
|
4205
|
+
}
|
|
4206
|
+
return sharedCache;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// src/richtext/create-richtext-controller.ts
|
|
4210
|
+
var Y2 = __toESM(require("yjs"), 1);
|
|
4211
|
+
init_richtext_serializer();
|
|
4212
|
+
var LOAD_ORIGIN = "kora-load";
|
|
4213
|
+
var REMOTE_ORIGIN = "kora-remote";
|
|
4214
|
+
var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
|
|
4215
|
+
var TEXT_KEY2 = "content";
|
|
4216
|
+
var COMPACT_AFTER_DELTAS = 20;
|
|
4217
|
+
var PERSIST_DEBOUNCE_MS = 400;
|
|
4218
|
+
function createRichTextController(options) {
|
|
4219
|
+
const {
|
|
4220
|
+
collection,
|
|
4221
|
+
collectionName,
|
|
4222
|
+
recordId,
|
|
4223
|
+
fieldName,
|
|
4224
|
+
store,
|
|
4225
|
+
syncEngine = null,
|
|
4226
|
+
useDocChannel
|
|
4227
|
+
} = options;
|
|
4228
|
+
const doc = new Y2.Doc();
|
|
4229
|
+
const text = doc.getText(TEXT_KEY2);
|
|
4230
|
+
const undoManager = new Y2.UndoManager(text);
|
|
4231
|
+
let user = options.user;
|
|
4232
|
+
let disposed = false;
|
|
4233
|
+
let ready = false;
|
|
4234
|
+
let error = null;
|
|
4235
|
+
let canUndo = false;
|
|
4236
|
+
let canRedo = false;
|
|
4237
|
+
let cursors = [];
|
|
4238
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4239
|
+
let snapshot = {
|
|
4240
|
+
ready: false,
|
|
4241
|
+
error: null,
|
|
4242
|
+
canUndo: false,
|
|
4243
|
+
canRedo: false,
|
|
4244
|
+
cursors: []
|
|
4245
|
+
};
|
|
4246
|
+
const baseUpdateRef = { current: null };
|
|
4247
|
+
const pendingDeltasRef = { current: [] };
|
|
4248
|
+
const docChannelActiveRef = { current: false };
|
|
4249
|
+
let persistTimer = null;
|
|
4250
|
+
let recordUnsubscribe = null;
|
|
4251
|
+
let docChannelUnsubscribe = null;
|
|
4252
|
+
let awarenessUnsubscribe = null;
|
|
4253
|
+
const refreshSnapshot = () => {
|
|
4254
|
+
snapshot = {
|
|
4255
|
+
ready,
|
|
4256
|
+
error,
|
|
4257
|
+
canUndo,
|
|
4258
|
+
canRedo,
|
|
4259
|
+
cursors: [...cursors]
|
|
4260
|
+
};
|
|
4261
|
+
};
|
|
4262
|
+
const notify = () => {
|
|
4263
|
+
refreshSnapshot();
|
|
4264
|
+
for (const listener of listeners) {
|
|
4265
|
+
listener();
|
|
4266
|
+
}
|
|
4267
|
+
};
|
|
4268
|
+
const syncHistoryState = () => {
|
|
4269
|
+
canUndo = undoManager.undoStack.length > 0;
|
|
4270
|
+
canRedo = undoManager.redoStack.length > 0;
|
|
4271
|
+
notify();
|
|
4272
|
+
};
|
|
4273
|
+
const getSnapshot = () => snapshot;
|
|
4274
|
+
const resolveAwarenessUser = () => {
|
|
4275
|
+
if (user) {
|
|
4276
|
+
return user;
|
|
4277
|
+
}
|
|
4278
|
+
if (syncEngine) {
|
|
4279
|
+
const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
|
|
4280
|
+
if (existing) {
|
|
4281
|
+
return existing;
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
return {
|
|
4285
|
+
name: "Anonymous",
|
|
4286
|
+
color: "#6366f1"
|
|
4287
|
+
};
|
|
4288
|
+
};
|
|
4289
|
+
const setCursor = (anchor, head) => {
|
|
4290
|
+
if (!syncEngine) {
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4294
|
+
const state = {
|
|
4295
|
+
user: resolveAwarenessUser(),
|
|
4296
|
+
cursor: {
|
|
4297
|
+
collection: collectionName,
|
|
4298
|
+
recordId,
|
|
4299
|
+
field: fieldName,
|
|
4300
|
+
anchor,
|
|
4301
|
+
head
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4304
|
+
awareness.setLocalState(state);
|
|
4305
|
+
};
|
|
4306
|
+
const clearCursor = () => {
|
|
4307
|
+
if (!syncEngine) {
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4311
|
+
const current = awareness.getLocalState();
|
|
4312
|
+
if (!current?.cursor) {
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
awareness.setLocalState({ user: current.user });
|
|
4319
|
+
};
|
|
4320
|
+
const applyRemoteSnapshot = (value) => {
|
|
4321
|
+
const encoded = encodeRichtextInput(value);
|
|
4322
|
+
if (!encoded) {
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
const currentSnapshot = Y2.encodeStateAsUpdate(doc);
|
|
4326
|
+
if (updatesEqual(currentSnapshot, encoded)) {
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
Y2.applyUpdate(doc, encoded, REMOTE_ORIGIN);
|
|
4330
|
+
baseUpdateRef.current = encoded;
|
|
4331
|
+
pendingDeltasRef.current = [];
|
|
4332
|
+
syncHistoryState();
|
|
4333
|
+
};
|
|
4334
|
+
const flushPersist = async () => {
|
|
4335
|
+
const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
|
|
4336
|
+
if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
|
|
4337
|
+
baseUpdateRef.current = snapshot2;
|
|
4338
|
+
pendingDeltasRef.current = [];
|
|
4339
|
+
}
|
|
4340
|
+
try {
|
|
4341
|
+
await collection.update(recordId, {
|
|
4342
|
+
[fieldName]: snapshot2
|
|
4343
|
+
});
|
|
4344
|
+
} catch (cause) {
|
|
4345
|
+
if (!disposed) {
|
|
4346
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
4347
|
+
notify();
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
};
|
|
4351
|
+
const schedulePersist = () => {
|
|
4352
|
+
if (persistTimer) {
|
|
4353
|
+
clearTimeout(persistTimer);
|
|
4354
|
+
}
|
|
4355
|
+
persistTimer = setTimeout(() => {
|
|
4356
|
+
persistTimer = null;
|
|
4357
|
+
void flushPersist();
|
|
4358
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
4359
|
+
};
|
|
4360
|
+
const onDocUpdate = (update, origin) => {
|
|
4361
|
+
syncHistoryState();
|
|
4362
|
+
if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
pendingDeltasRef.current.push(update);
|
|
4366
|
+
if (docChannelActiveRef.current && syncEngine) {
|
|
4367
|
+
syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
|
|
4368
|
+
schedulePersist();
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
void flushPersist();
|
|
4372
|
+
};
|
|
4373
|
+
const subscribeRecordChanges = () => {
|
|
4374
|
+
recordUnsubscribe?.();
|
|
4375
|
+
recordUnsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
|
|
4376
|
+
const record = results[0];
|
|
4377
|
+
if (!record) {
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
applyRemoteSnapshot(record[fieldName]);
|
|
4381
|
+
});
|
|
4382
|
+
};
|
|
4383
|
+
const subscribeDocChannel = () => {
|
|
4384
|
+
docChannelUnsubscribe?.();
|
|
4385
|
+
if (!ready || !syncEngine || !docChannelActiveRef.current) {
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
const channel = syncEngine.getRichtextDocChannel?.();
|
|
4389
|
+
if (!channel) {
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
docChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {
|
|
4393
|
+
Y2.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
|
|
4394
|
+
syncHistoryState();
|
|
4395
|
+
});
|
|
4396
|
+
};
|
|
4397
|
+
const updateCursors = () => {
|
|
4398
|
+
if (!syncEngine) {
|
|
4399
|
+
cursors = [];
|
|
4400
|
+
notify();
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4404
|
+
const localClientId = awareness.clientId;
|
|
4405
|
+
const states = awareness.getStates();
|
|
4406
|
+
const fieldCursors = [];
|
|
4407
|
+
for (const [clientId, state] of states) {
|
|
4408
|
+
if (clientId === localClientId) continue;
|
|
4409
|
+
if (!state.cursor) continue;
|
|
4410
|
+
if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
|
|
4411
|
+
continue;
|
|
4412
|
+
}
|
|
4413
|
+
fieldCursors.push({
|
|
4414
|
+
clientId,
|
|
4415
|
+
userName: state.user.name,
|
|
4416
|
+
color: state.user.color,
|
|
4417
|
+
anchor: state.cursor.anchor,
|
|
4418
|
+
head: state.cursor.head
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
cursors = fieldCursors;
|
|
4422
|
+
notify();
|
|
4423
|
+
};
|
|
4424
|
+
const subscribeAwareness = () => {
|
|
4425
|
+
awarenessUnsubscribe?.();
|
|
4426
|
+
if (!syncEngine) {
|
|
4427
|
+
cursors = [];
|
|
4428
|
+
notify();
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
4432
|
+
awarenessUnsubscribe = awareness.on("change", () => {
|
|
4433
|
+
updateCursors();
|
|
4434
|
+
});
|
|
4435
|
+
updateCursors();
|
|
4436
|
+
};
|
|
4437
|
+
const initialize = async () => {
|
|
4438
|
+
ready = false;
|
|
4439
|
+
error = null;
|
|
4440
|
+
notify();
|
|
4441
|
+
try {
|
|
4442
|
+
const record = await collection.findById(recordId);
|
|
4443
|
+
if (disposed) return;
|
|
4444
|
+
doc.transact(() => {
|
|
4445
|
+
const target = doc.getText(TEXT_KEY2);
|
|
4446
|
+
target.delete(0, target.length);
|
|
4447
|
+
}, LOAD_ORIGIN);
|
|
4448
|
+
const encoded = encodeRichtextInput(record?.[fieldName]);
|
|
4449
|
+
baseUpdateRef.current = encoded;
|
|
4450
|
+
pendingDeltasRef.current = [];
|
|
4451
|
+
if (encoded) {
|
|
4452
|
+
Y2.applyUpdate(doc, encoded, LOAD_ORIGIN);
|
|
4453
|
+
}
|
|
4454
|
+
const channel = syncEngine?.getRichtextDocChannel?.();
|
|
4455
|
+
docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false;
|
|
4456
|
+
ready = true;
|
|
4457
|
+
syncHistoryState();
|
|
4458
|
+
subscribeRecordChanges();
|
|
4459
|
+
subscribeDocChannel();
|
|
4460
|
+
subscribeAwareness();
|
|
4461
|
+
notify();
|
|
4462
|
+
} catch (cause) {
|
|
4463
|
+
if (disposed) return;
|
|
4464
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
4465
|
+
notify();
|
|
4466
|
+
}
|
|
4467
|
+
};
|
|
4468
|
+
doc.on("update", onDocUpdate);
|
|
4469
|
+
void initialize();
|
|
4470
|
+
return {
|
|
4471
|
+
doc,
|
|
4472
|
+
text,
|
|
4473
|
+
getSnapshot,
|
|
4474
|
+
subscribe(listener) {
|
|
4475
|
+
listeners.add(listener);
|
|
4476
|
+
return () => {
|
|
4477
|
+
listeners.delete(listener);
|
|
4478
|
+
};
|
|
4479
|
+
},
|
|
4480
|
+
undo() {
|
|
4481
|
+
undoManager.undo();
|
|
4482
|
+
syncHistoryState();
|
|
4483
|
+
},
|
|
4484
|
+
redo() {
|
|
4485
|
+
undoManager.redo();
|
|
4486
|
+
syncHistoryState();
|
|
4487
|
+
},
|
|
4488
|
+
setCursor,
|
|
4489
|
+
clearCursor,
|
|
4490
|
+
setUser(nextUser) {
|
|
4491
|
+
user = nextUser;
|
|
4492
|
+
},
|
|
4493
|
+
destroy() {
|
|
4494
|
+
if (disposed) {
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
disposed = true;
|
|
4498
|
+
if (persistTimer) {
|
|
4499
|
+
clearTimeout(persistTimer);
|
|
4500
|
+
persistTimer = null;
|
|
4501
|
+
}
|
|
4502
|
+
doc.off("update", onDocUpdate);
|
|
4503
|
+
recordUnsubscribe?.();
|
|
4504
|
+
docChannelUnsubscribe?.();
|
|
4505
|
+
awarenessUnsubscribe?.();
|
|
4506
|
+
clearCursor();
|
|
4507
|
+
undoManager.destroy();
|
|
4508
|
+
baseUpdateRef.current = null;
|
|
4509
|
+
pendingDeltasRef.current = [];
|
|
4510
|
+
docChannelActiveRef.current = false;
|
|
4511
|
+
listeners.clear();
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
function encodeRichtextInput(value) {
|
|
4516
|
+
if (value === null || value === void 0) {
|
|
4517
|
+
return null;
|
|
4518
|
+
}
|
|
4519
|
+
try {
|
|
4520
|
+
return encodeRichtext(value);
|
|
4521
|
+
} catch {
|
|
4522
|
+
if (typeof value === "string") {
|
|
4523
|
+
const fallbackDoc = new Y2.Doc();
|
|
4524
|
+
fallbackDoc.getText(TEXT_KEY2).insert(0, value);
|
|
4525
|
+
return Y2.encodeStateAsUpdate(fallbackDoc);
|
|
4526
|
+
}
|
|
4527
|
+
throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
function composeRichtextSnapshot(base, deltas) {
|
|
4531
|
+
const mergedDoc = new Y2.Doc();
|
|
4532
|
+
if (base) {
|
|
4533
|
+
Y2.applyUpdate(mergedDoc, base);
|
|
4534
|
+
}
|
|
4535
|
+
for (const delta of deltas) {
|
|
4536
|
+
Y2.applyUpdate(mergedDoc, delta);
|
|
4537
|
+
}
|
|
4538
|
+
return Y2.encodeStateAsUpdate(mergedDoc);
|
|
4539
|
+
}
|
|
4540
|
+
function updatesEqual(left, right) {
|
|
4541
|
+
if (left.length !== right.length) {
|
|
4542
|
+
return false;
|
|
4543
|
+
}
|
|
4544
|
+
for (let index = 0; index < left.length; index++) {
|
|
4545
|
+
if (left[index] !== right[index]) {
|
|
4546
|
+
return false;
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
return true;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
// src/richtext/as-rich-text-sync-engine.ts
|
|
4553
|
+
function asRichTextSyncEngine(engine) {
|
|
4554
|
+
if (engine == null) {
|
|
4555
|
+
return null;
|
|
4556
|
+
}
|
|
4557
|
+
return engine;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
// src/index.ts
|
|
4561
|
+
init_richtext_serializer();
|
|
4562
|
+
|
|
4563
|
+
// src/backup/index.ts
|
|
4564
|
+
init_backup();
|
|
4565
|
+
|
|
4566
|
+
// src/audit/index.ts
|
|
4567
|
+
init_audit_trace_store();
|
|
4568
|
+
init_audit_json();
|
|
4569
|
+
init_export_audit();
|
|
2508
4570
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2509
4571
|
0 && (module.exports = {
|
|
2510
4572
|
AdapterError,
|
|
4573
|
+
COMPACTION_BASELINE_META_KEY,
|
|
2511
4574
|
Collection,
|
|
2512
4575
|
InvalidStateTransitionError,
|
|
4576
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
2513
4577
|
PersistenceError,
|
|
2514
4578
|
QueryBuilder,
|
|
2515
4579
|
QueryError,
|
|
4580
|
+
QueryStore,
|
|
4581
|
+
QueryStoreCache,
|
|
2516
4582
|
RecordNotFoundError,
|
|
2517
4583
|
SequenceManager,
|
|
2518
4584
|
Store,
|
|
@@ -2522,12 +4588,31 @@ var Store = class {
|
|
|
2522
4588
|
TransactionContext,
|
|
2523
4589
|
WorkerInitError,
|
|
2524
4590
|
WorkerTimeoutError,
|
|
4591
|
+
asRichTextSyncEngine,
|
|
4592
|
+
assertQueryReady,
|
|
4593
|
+
collectOperationsAheadOfServer,
|
|
4594
|
+
compactOperationLog,
|
|
4595
|
+
computeAckCompactionWatermark,
|
|
4596
|
+
createRichTextController,
|
|
4597
|
+
decodeAuditExport,
|
|
2525
4598
|
decodeRichtext,
|
|
4599
|
+
deserializeVersionVectorFromMeta,
|
|
2526
4600
|
encodeRichtext,
|
|
4601
|
+
exportBackup,
|
|
4602
|
+
getSharedQueryStoreCache,
|
|
4603
|
+
mergeVersionVectors,
|
|
4604
|
+
persistedAuditTraceFromEvent,
|
|
2527
4605
|
pluralize,
|
|
4606
|
+
readAuditExportManifest,
|
|
4607
|
+
readBackupManifest,
|
|
4608
|
+
restoreBackup,
|
|
4609
|
+
richtextStatesEqual,
|
|
2528
4610
|
richtextToPlainText,
|
|
4611
|
+
serializeVersionVectorToMeta,
|
|
2529
4612
|
singularize,
|
|
2530
4613
|
validateStateTransition,
|
|
2531
|
-
validateUpdateStateMachine
|
|
4614
|
+
validateUpdateStateMachine,
|
|
4615
|
+
verifyAuditExportChecksum,
|
|
4616
|
+
verifyBackupChecksum
|
|
2532
4617
|
});
|
|
2533
4618
|
//# sourceMappingURL=index.cjs.map
|