@korajs/store 0.4.0 → 0.5.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 +2183 -587
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +423 -187
- package/dist/index.d.ts +423 -187
- package/dist/index.js +886 -726
- 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,243 +30,7 @@ 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
|
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
AdapterError: () => AdapterError,
|
|
34
|
-
Collection: () => Collection,
|
|
35
|
-
InvalidStateTransitionError: () => InvalidStateTransitionError,
|
|
36
|
-
PersistenceError: () => PersistenceError,
|
|
37
|
-
QueryBuilder: () => QueryBuilder,
|
|
38
|
-
QueryError: () => QueryError,
|
|
39
|
-
RecordNotFoundError: () => RecordNotFoundError,
|
|
40
|
-
SequenceManager: () => SequenceManager,
|
|
41
|
-
Store: () => Store,
|
|
42
|
-
StoreNotOpenError: () => StoreNotOpenError,
|
|
43
|
-
SubscriptionBloomFilter: () => SubscriptionBloomFilter,
|
|
44
|
-
SubscriptionManager: () => SubscriptionManager,
|
|
45
|
-
TransactionContext: () => TransactionContext,
|
|
46
|
-
WorkerInitError: () => WorkerInitError,
|
|
47
|
-
WorkerTimeoutError: () => WorkerTimeoutError,
|
|
48
|
-
decodeRichtext: () => decodeRichtext,
|
|
49
|
-
encodeRichtext: () => encodeRichtext,
|
|
50
|
-
pluralize: () => pluralize,
|
|
51
|
-
richtextToPlainText: () => richtextToPlainText,
|
|
52
|
-
singularize: () => singularize,
|
|
53
|
-
validateStateTransition: () => validateStateTransition,
|
|
54
|
-
validateUpdateStateMachine: () => validateUpdateStateMachine
|
|
55
|
-
});
|
|
56
|
-
module.exports = __toCommonJS(index_exports);
|
|
57
|
-
|
|
58
|
-
// src/errors.ts
|
|
59
|
-
var import_core = require("@korajs/core");
|
|
60
|
-
var QueryError = class extends import_core.KoraError {
|
|
61
|
-
constructor(message, context) {
|
|
62
|
-
super(message, "QUERY_ERROR", context);
|
|
63
|
-
this.name = "QueryError";
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
var RecordNotFoundError = class extends import_core.KoraError {
|
|
67
|
-
constructor(collection, recordId) {
|
|
68
|
-
super(`Record "${recordId}" not found in collection "${collection}"`, "RECORD_NOT_FOUND", {
|
|
69
|
-
collection,
|
|
70
|
-
recordId
|
|
71
|
-
});
|
|
72
|
-
this.name = "RecordNotFoundError";
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
var AdapterError = class extends import_core.KoraError {
|
|
76
|
-
constructor(message, context) {
|
|
77
|
-
super(message, "ADAPTER_ERROR", context);
|
|
78
|
-
this.name = "AdapterError";
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
var StoreNotOpenError = class extends import_core.KoraError {
|
|
82
|
-
constructor() {
|
|
83
|
-
super("Store is not open. Call store.open() before performing operations.", "STORE_NOT_OPEN");
|
|
84
|
-
this.name = "StoreNotOpenError";
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
var WorkerInitError = class extends import_core.KoraError {
|
|
88
|
-
constructor(message, context) {
|
|
89
|
-
super(`Worker initialization failed: ${message}`, "WORKER_INIT_ERROR", context);
|
|
90
|
-
this.name = "WorkerInitError";
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
var WorkerTimeoutError = class extends import_core.KoraError {
|
|
94
|
-
constructor(operation, timeoutMs) {
|
|
95
|
-
super(
|
|
96
|
-
`Worker did not respond within ${timeoutMs}ms for operation "${operation}"`,
|
|
97
|
-
"WORKER_TIMEOUT",
|
|
98
|
-
{ operation, timeoutMs }
|
|
99
|
-
);
|
|
100
|
-
this.name = "WorkerTimeoutError";
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
var PersistenceError = class extends import_core.KoraError {
|
|
104
|
-
constructor(message, context) {
|
|
105
|
-
super(`Persistence error: ${message}`, "PERSISTENCE_ERROR", context);
|
|
106
|
-
this.name = "PersistenceError";
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// src/store/store.ts
|
|
111
|
-
var import_core8 = require("@korajs/core");
|
|
112
|
-
|
|
113
|
-
// src/collection/collection.ts
|
|
114
|
-
var import_core4 = require("@korajs/core");
|
|
115
|
-
|
|
116
|
-
// src/query/sql-builder.ts
|
|
117
|
-
function buildSelectQuery(descriptor, fields) {
|
|
118
|
-
const params = [];
|
|
119
|
-
const parts = [`SELECT * FROM ${descriptor.collection}`];
|
|
120
|
-
const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
|
|
121
|
-
const deletedFilter = "_deleted = 0";
|
|
122
|
-
if (whereClause) {
|
|
123
|
-
parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
|
|
124
|
-
} else {
|
|
125
|
-
parts.push(`WHERE ${deletedFilter}`);
|
|
126
|
-
}
|
|
127
|
-
if (descriptor.orderBy.length > 0) {
|
|
128
|
-
const orderParts = descriptor.orderBy.map((o) => {
|
|
129
|
-
validateFieldName(o.field, fields);
|
|
130
|
-
return `${o.field} ${o.direction.toUpperCase()}`;
|
|
131
|
-
});
|
|
132
|
-
parts.push(`ORDER BY ${orderParts.join(", ")}`);
|
|
133
|
-
}
|
|
134
|
-
if (descriptor.limit !== void 0) {
|
|
135
|
-
parts.push(`LIMIT ${descriptor.limit}`);
|
|
136
|
-
}
|
|
137
|
-
if (descriptor.offset !== void 0) {
|
|
138
|
-
parts.push(`OFFSET ${descriptor.offset}`);
|
|
139
|
-
}
|
|
140
|
-
return { sql: parts.join(" "), params };
|
|
141
|
-
}
|
|
142
|
-
function buildCountQuery(descriptor, fields) {
|
|
143
|
-
const params = [];
|
|
144
|
-
const parts = [`SELECT COUNT(*) as count FROM ${descriptor.collection}`];
|
|
145
|
-
const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
|
|
146
|
-
const deletedFilter = "_deleted = 0";
|
|
147
|
-
if (whereClause) {
|
|
148
|
-
parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
|
|
149
|
-
} else {
|
|
150
|
-
parts.push(`WHERE ${deletedFilter}`);
|
|
151
|
-
}
|
|
152
|
-
return { sql: parts.join(" "), params };
|
|
153
|
-
}
|
|
154
|
-
function buildInsertQuery(collection, record) {
|
|
155
|
-
const columns = Object.keys(record);
|
|
156
|
-
const placeholders = columns.map(() => "?");
|
|
157
|
-
const params = Object.values(record);
|
|
158
|
-
const sql = `INSERT INTO ${collection} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
|
|
159
|
-
return { sql, params };
|
|
160
|
-
}
|
|
161
|
-
function buildUpdateQuery(collection, id, changes) {
|
|
162
|
-
const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
|
|
163
|
-
const params = [...Object.values(changes), id];
|
|
164
|
-
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
165
|
-
return { sql, params };
|
|
166
|
-
}
|
|
167
|
-
function buildSoftDeleteQuery(collection, id, updatedAt) {
|
|
168
|
-
return {
|
|
169
|
-
sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
|
|
170
|
-
params: [updatedAt, id]
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
var VALID_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"]);
|
|
174
|
-
function buildWhereClauseParts(where, fields, params) {
|
|
175
|
-
const conditions = [];
|
|
176
|
-
for (const [fieldName, value] of Object.entries(where)) {
|
|
177
|
-
validateFieldName(fieldName, fields);
|
|
178
|
-
const descriptor = fields[fieldName];
|
|
179
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
180
|
-
const ops = value;
|
|
181
|
-
for (const [op, opValue] of Object.entries(ops)) {
|
|
182
|
-
if (!VALID_OPERATORS.has(op)) {
|
|
183
|
-
throw new QueryError(`Unknown operator "${op}" on field "${fieldName}"`, {
|
|
184
|
-
field: fieldName,
|
|
185
|
-
operator: op,
|
|
186
|
-
validOperators: [...VALID_OPERATORS]
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
conditions.push(buildOperatorCondition(fieldName, op, opValue, descriptor, params));
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
conditions.push(buildOperatorCondition(fieldName, "$eq", value, descriptor, params));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (conditions.length === 0) return null;
|
|
196
|
-
return conditions.join(" AND ");
|
|
197
|
-
}
|
|
198
|
-
function buildOperatorCondition(fieldName, operator, value, descriptor, params) {
|
|
199
|
-
const sqlValue = descriptor?.kind === "boolean" && typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
200
|
-
switch (operator) {
|
|
201
|
-
case "$eq":
|
|
202
|
-
if (sqlValue === null) {
|
|
203
|
-
return `${fieldName} IS NULL`;
|
|
204
|
-
}
|
|
205
|
-
params.push(sqlValue);
|
|
206
|
-
return `${fieldName} = ?`;
|
|
207
|
-
case "$ne":
|
|
208
|
-
if (sqlValue === null) {
|
|
209
|
-
return `${fieldName} IS NOT NULL`;
|
|
210
|
-
}
|
|
211
|
-
params.push(sqlValue);
|
|
212
|
-
return `${fieldName} != ?`;
|
|
213
|
-
case "$gt":
|
|
214
|
-
params.push(sqlValue);
|
|
215
|
-
return `${fieldName} > ?`;
|
|
216
|
-
case "$gte":
|
|
217
|
-
params.push(sqlValue);
|
|
218
|
-
return `${fieldName} >= ?`;
|
|
219
|
-
case "$lt":
|
|
220
|
-
params.push(sqlValue);
|
|
221
|
-
return `${fieldName} < ?`;
|
|
222
|
-
case "$lte":
|
|
223
|
-
params.push(sqlValue);
|
|
224
|
-
return `${fieldName} <= ?`;
|
|
225
|
-
case "$in": {
|
|
226
|
-
if (!Array.isArray(sqlValue)) {
|
|
227
|
-
throw new QueryError(`$in operator requires an array value for field "${fieldName}"`, {
|
|
228
|
-
field: fieldName,
|
|
229
|
-
received: typeof sqlValue
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
const placeholders = sqlValue.map(() => "?");
|
|
233
|
-
for (const item of sqlValue) {
|
|
234
|
-
params.push(
|
|
235
|
-
descriptor?.kind === "boolean" && typeof item === "boolean" ? item ? 1 : 0 : item
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
return `${fieldName} IN (${placeholders.join(", ")})`;
|
|
239
|
-
}
|
|
240
|
-
default:
|
|
241
|
-
throw new QueryError(`Unknown operator "${operator}"`, { operator });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
function validateFieldName(fieldName, fields) {
|
|
245
|
-
const allowedFields = /* @__PURE__ */ new Set([
|
|
246
|
-
...Object.keys(fields),
|
|
247
|
-
"id",
|
|
248
|
-
"createdAt",
|
|
249
|
-
"updatedAt",
|
|
250
|
-
"_created_at",
|
|
251
|
-
"_updated_at"
|
|
252
|
-
]);
|
|
253
|
-
if (!allowedFields.has(fieldName)) {
|
|
254
|
-
throw new QueryError(
|
|
255
|
-
`Unknown field "${fieldName}" in query. Available fields: ${[...allowedFields].join(", ")}`,
|
|
256
|
-
{ field: fieldName }
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// src/serialization/serializer.ts
|
|
262
|
-
var import_core2 = require("@korajs/core");
|
|
263
|
-
|
|
264
33
|
// src/serialization/richtext-serializer.ts
|
|
265
|
-
var Y = __toESM(require("yjs"), 1);
|
|
266
|
-
var TEXT_KEY = "content";
|
|
267
34
|
function encodeRichtext(value) {
|
|
268
35
|
if (value === null || value === void 0) {
|
|
269
36
|
return null;
|
|
@@ -305,6 +72,21 @@ function richtextToPlainText(value) {
|
|
|
305
72
|
Y.applyUpdate(doc, encoded);
|
|
306
73
|
return doc.getText(TEXT_KEY).toString();
|
|
307
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
|
+
});
|
|
308
90
|
|
|
309
91
|
// src/serialization/serializer.ts
|
|
310
92
|
function serializeRecord(data, fields) {
|
|
@@ -335,9 +117,6 @@ function deserializeRecord(row, fields) {
|
|
|
335
117
|
}
|
|
336
118
|
return result;
|
|
337
119
|
}
|
|
338
|
-
var ATOMIC_OPS_KEY = "__kora_atomic_ops__";
|
|
339
|
-
var TX_ID_KEY = "__kora_tx_id__";
|
|
340
|
-
var MUTATION_NAME_KEY = "__kora_mutation__";
|
|
341
120
|
function serializeOperation(op) {
|
|
342
121
|
const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
|
|
343
122
|
let dataPayload = null;
|
|
@@ -368,7 +147,7 @@ function serializeOperation(op) {
|
|
|
368
147
|
record_id: op.recordId,
|
|
369
148
|
data: dataPayload ? JSON.stringify(dataPayload) : null,
|
|
370
149
|
previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
|
|
371
|
-
timestamp:
|
|
150
|
+
timestamp: import_core4.HybridLogicalClock.serialize(op.timestamp),
|
|
372
151
|
sequence_number: op.sequenceNumber,
|
|
373
152
|
causal_deps: JSON.stringify(op.causalDeps),
|
|
374
153
|
schema_version: op.schemaVersion
|
|
@@ -402,7 +181,7 @@ function deserializeOperation(row) {
|
|
|
402
181
|
recordId: row.record_id,
|
|
403
182
|
data,
|
|
404
183
|
previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
|
|
405
|
-
timestamp:
|
|
184
|
+
timestamp: import_core4.HybridLogicalClock.deserialize(row.timestamp),
|
|
406
185
|
sequenceNumber: row.sequence_number,
|
|
407
186
|
causalDeps: JSON.parse(row.causal_deps),
|
|
408
187
|
schemaVersion: row.schema_version,
|
|
@@ -445,28 +224,1343 @@ function deserializeValue(value, descriptor) {
|
|
|
445
224
|
return value;
|
|
446
225
|
}
|
|
447
226
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
);
|
|
458
|
-
this.collection = collection;
|
|
459
|
-
this.recordId = recordId;
|
|
460
|
-
this.field = field;
|
|
461
|
-
this.fromState = fromState;
|
|
462
|
-
this.toState = toState;
|
|
463
|
-
this.allowedStates = allowedStates;
|
|
464
|
-
this.name = "InvalidStateTransitionError";
|
|
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__";
|
|
465
236
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
|
|
935
|
+
// src/index.ts
|
|
936
|
+
var index_exports = {};
|
|
937
|
+
__export(index_exports, {
|
|
938
|
+
AdapterError: () => AdapterError,
|
|
939
|
+
COMPACTION_BASELINE_META_KEY: () => COMPACTION_BASELINE_META_KEY,
|
|
940
|
+
Collection: () => Collection,
|
|
941
|
+
InvalidStateTransitionError: () => InvalidStateTransitionError,
|
|
942
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY: () => LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
943
|
+
PersistenceError: () => PersistenceError,
|
|
944
|
+
QueryBuilder: () => QueryBuilder,
|
|
945
|
+
QueryError: () => QueryError,
|
|
946
|
+
RecordNotFoundError: () => RecordNotFoundError,
|
|
947
|
+
SequenceManager: () => SequenceManager,
|
|
948
|
+
Store: () => Store,
|
|
949
|
+
StoreNotOpenError: () => StoreNotOpenError,
|
|
950
|
+
SubscriptionBloomFilter: () => SubscriptionBloomFilter,
|
|
951
|
+
SubscriptionManager: () => SubscriptionManager,
|
|
952
|
+
TransactionContext: () => TransactionContext,
|
|
953
|
+
WorkerInitError: () => WorkerInitError,
|
|
954
|
+
WorkerTimeoutError: () => WorkerTimeoutError,
|
|
955
|
+
collectOperationsAheadOfServer: () => collectOperationsAheadOfServer,
|
|
956
|
+
compactOperationLog: () => compactOperationLog,
|
|
957
|
+
computeAckCompactionWatermark: () => computeAckCompactionWatermark,
|
|
958
|
+
decodeAuditExport: () => decodeAuditExport,
|
|
959
|
+
decodeRichtext: () => decodeRichtext,
|
|
960
|
+
deserializeVersionVectorFromMeta: () => deserializeVersionVectorFromMeta,
|
|
961
|
+
encodeRichtext: () => encodeRichtext,
|
|
962
|
+
exportBackup: () => exportBackup,
|
|
963
|
+
mergeVersionVectors: () => mergeVersionVectors,
|
|
964
|
+
persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
|
|
965
|
+
pluralize: () => pluralize,
|
|
966
|
+
readAuditExportManifest: () => readAuditExportManifest,
|
|
967
|
+
readBackupManifest: () => readBackupManifest,
|
|
968
|
+
restoreBackup: () => restoreBackup,
|
|
969
|
+
richtextStatesEqual: () => richtextStatesEqual,
|
|
970
|
+
richtextToPlainText: () => richtextToPlainText,
|
|
971
|
+
serializeVersionVectorToMeta: () => serializeVersionVectorToMeta,
|
|
972
|
+
singularize: () => singularize,
|
|
973
|
+
validateStateTransition: () => validateStateTransition,
|
|
974
|
+
validateUpdateStateMachine: () => validateUpdateStateMachine,
|
|
975
|
+
verifyAuditExportChecksum: () => verifyAuditExportChecksum,
|
|
976
|
+
verifyBackupChecksum: () => verifyBackupChecksum
|
|
977
|
+
});
|
|
978
|
+
module.exports = __toCommonJS(index_exports);
|
|
979
|
+
|
|
980
|
+
// src/errors.ts
|
|
981
|
+
var import_core = require("@korajs/core");
|
|
982
|
+
var QueryError = class extends import_core.KoraError {
|
|
983
|
+
constructor(message, context) {
|
|
984
|
+
super(message, "QUERY_ERROR", context);
|
|
985
|
+
this.name = "QueryError";
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
var RecordNotFoundError = class extends import_core.KoraError {
|
|
989
|
+
constructor(collection, recordId) {
|
|
990
|
+
super(`Record "${recordId}" not found in collection "${collection}"`, "RECORD_NOT_FOUND", {
|
|
991
|
+
collection,
|
|
992
|
+
recordId
|
|
993
|
+
});
|
|
994
|
+
this.name = "RecordNotFoundError";
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
var AdapterError = class extends import_core.KoraError {
|
|
998
|
+
constructor(message, context) {
|
|
999
|
+
super(message, "ADAPTER_ERROR", context);
|
|
1000
|
+
this.name = "AdapterError";
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var StoreNotOpenError = class extends import_core.KoraError {
|
|
1004
|
+
constructor() {
|
|
1005
|
+
super("Store is not open. Call store.open() before performing operations.", "STORE_NOT_OPEN");
|
|
1006
|
+
this.name = "StoreNotOpenError";
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
var WorkerInitError = class extends import_core.KoraError {
|
|
1010
|
+
constructor(message, context) {
|
|
1011
|
+
super(`Worker initialization failed: ${message}`, "WORKER_INIT_ERROR", context);
|
|
1012
|
+
this.name = "WorkerInitError";
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
var WorkerTimeoutError = class extends import_core.KoraError {
|
|
1016
|
+
constructor(operation, timeoutMs) {
|
|
1017
|
+
super(
|
|
1018
|
+
`Worker did not respond within ${timeoutMs}ms for operation "${operation}"`,
|
|
1019
|
+
"WORKER_TIMEOUT",
|
|
1020
|
+
{ operation, timeoutMs }
|
|
1021
|
+
);
|
|
1022
|
+
this.name = "WorkerTimeoutError";
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
var PersistenceError = class extends import_core.KoraError {
|
|
1026
|
+
constructor(message, context) {
|
|
1027
|
+
super(`Persistence error: ${message}`, "PERSISTENCE_ERROR", context);
|
|
1028
|
+
this.name = "PersistenceError";
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/compaction/types.ts
|
|
1033
|
+
var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
|
|
1034
|
+
|
|
1035
|
+
// src/compaction/compact-operation-log.ts
|
|
1036
|
+
var import_core2 = require("@korajs/core");
|
|
1037
|
+
function computeAckCompactionWatermark(serverVector) {
|
|
1038
|
+
const watermark = (0, import_core2.createVersionVector)();
|
|
1039
|
+
for (const [nodeId, seq] of serverVector) {
|
|
1040
|
+
if (seq > 0) {
|
|
1041
|
+
watermark.set(nodeId, seq);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return watermark;
|
|
1045
|
+
}
|
|
1046
|
+
async function compactOperationLog(adapter, schema, strategy, serverVector) {
|
|
1047
|
+
if (strategy.mode === "never") {
|
|
1048
|
+
return { deletedCount: 0, watermark: (0, import_core2.createVersionVector)() };
|
|
1049
|
+
}
|
|
1050
|
+
const watermark = computeAckCompactionWatermark(serverVector);
|
|
1051
|
+
if (watermark.size === 0) {
|
|
1052
|
+
return { deletedCount: 0, watermark };
|
|
1053
|
+
}
|
|
1054
|
+
const ageCutoffWall = strategy.mode === "after-days" ? Date.now() - strategy.days * 24 * 60 * 60 * 1e3 : null;
|
|
1055
|
+
const ageCutoffSerialized = ageCutoffWall !== null ? import_core2.HybridLogicalClock.serialize({
|
|
1056
|
+
wallTime: ageCutoffWall,
|
|
1057
|
+
logical: 0,
|
|
1058
|
+
nodeId: ""
|
|
1059
|
+
}).slice(0, 15) : null;
|
|
1060
|
+
let deletedCount = 0;
|
|
1061
|
+
const collectionNames = Object.keys(schema.collections);
|
|
1062
|
+
await adapter.transaction(async (tx) => {
|
|
1063
|
+
for (const collectionName of collectionNames) {
|
|
1064
|
+
const table = `_kora_ops_${collectionName}`;
|
|
1065
|
+
for (const [nodeId, maxSeq] of watermark) {
|
|
1066
|
+
if (maxSeq <= 0) continue;
|
|
1067
|
+
const countSql = buildCountSql(table, ageCutoffSerialized !== null);
|
|
1068
|
+
const countParams = ageCutoffSerialized !== null ? [nodeId, maxSeq, ageCutoffSerialized] : [nodeId, maxSeq];
|
|
1069
|
+
const countRows = await tx.query(countSql, countParams);
|
|
1070
|
+
deletedCount += countRows[0]?.count ?? 0;
|
|
1071
|
+
const deleteSql = buildDeleteSql(table, ageCutoffSerialized !== null);
|
|
1072
|
+
await tx.execute(deleteSql, countParams);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
1076
|
+
COMPACTION_BASELINE_META_KEY,
|
|
1077
|
+
String(Date.now())
|
|
1078
|
+
]);
|
|
1079
|
+
});
|
|
1080
|
+
return { deletedCount, watermark };
|
|
1081
|
+
}
|
|
1082
|
+
function buildCountSql(table, withAge) {
|
|
1083
|
+
if (withAge) {
|
|
1084
|
+
return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
|
|
1085
|
+
}
|
|
1086
|
+
return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
|
|
1087
|
+
}
|
|
1088
|
+
function buildDeleteSql(table, withAge) {
|
|
1089
|
+
if (withAge) {
|
|
1090
|
+
return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
|
|
1091
|
+
}
|
|
1092
|
+
return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/sync/sync-state.ts
|
|
1096
|
+
var import_core3 = require("@korajs/core");
|
|
1097
|
+
var LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
|
|
1098
|
+
var DELTA_CURSOR_META_KEY = "delta_cursor";
|
|
1099
|
+
function serializeVersionVectorToMeta(vector) {
|
|
1100
|
+
const record = {};
|
|
1101
|
+
for (const [nodeId, seq] of vector) {
|
|
1102
|
+
record[nodeId] = seq;
|
|
1103
|
+
}
|
|
1104
|
+
return JSON.stringify(record);
|
|
1105
|
+
}
|
|
1106
|
+
function deserializeVersionVectorFromMeta(value) {
|
|
1107
|
+
const parsed = JSON.parse(value);
|
|
1108
|
+
const vector = (0, import_core3.createVersionVector)();
|
|
1109
|
+
for (const [nodeId, seq] of Object.entries(parsed)) {
|
|
1110
|
+
if (typeof seq === "number" && seq >= 0) {
|
|
1111
|
+
vector.set(nodeId, seq);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return vector;
|
|
1115
|
+
}
|
|
1116
|
+
function mergeVersionVectors(a, b) {
|
|
1117
|
+
const merged = new Map(a);
|
|
1118
|
+
for (const [nodeId, seq] of b) {
|
|
1119
|
+
merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, seq));
|
|
1120
|
+
}
|
|
1121
|
+
return merged;
|
|
1122
|
+
}
|
|
1123
|
+
async function collectOperationsAheadOfServer(localVector, serverVector, fetchRange) {
|
|
1124
|
+
const missing = [];
|
|
1125
|
+
for (const [nodeId, localSeq] of localVector) {
|
|
1126
|
+
const serverSeq = serverVector.get(nodeId) ?? 0;
|
|
1127
|
+
if (localSeq > serverSeq) {
|
|
1128
|
+
const ops = await fetchRange(nodeId, serverSeq + 1, localSeq);
|
|
1129
|
+
missing.push(...ops);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
missing.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
1133
|
+
return missing;
|
|
1134
|
+
}
|
|
1135
|
+
async function loadLastAckedServerVector(adapter) {
|
|
1136
|
+
const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
|
|
1137
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY
|
|
1138
|
+
]);
|
|
1139
|
+
const row = rows[0];
|
|
1140
|
+
if (!row?.value) {
|
|
1141
|
+
return (0, import_core3.createVersionVector)();
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
return deserializeVersionVectorFromMeta(row.value);
|
|
1145
|
+
} catch {
|
|
1146
|
+
return (0, import_core3.createVersionVector)();
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async function saveLastAckedServerVector(adapter, vector) {
|
|
1150
|
+
await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
1151
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
1152
|
+
serializeVersionVectorToMeta(vector)
|
|
1153
|
+
]);
|
|
1154
|
+
}
|
|
1155
|
+
async function loadDeltaCursor(adapter) {
|
|
1156
|
+
const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
|
|
1157
|
+
DELTA_CURSOR_META_KEY
|
|
1158
|
+
]);
|
|
1159
|
+
return rows[0]?.value ?? null;
|
|
1160
|
+
}
|
|
1161
|
+
async function saveDeltaCursor(adapter, cursor) {
|
|
1162
|
+
if (cursor === null) {
|
|
1163
|
+
await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [DELTA_CURSOR_META_KEY]);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
|
|
1167
|
+
DELTA_CURSOR_META_KEY,
|
|
1168
|
+
cursor
|
|
1169
|
+
]);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/store/store.ts
|
|
1173
|
+
var import_core17 = require("@korajs/core");
|
|
1174
|
+
init_backup();
|
|
1175
|
+
|
|
1176
|
+
// src/mutations/execute-delete.ts
|
|
1177
|
+
var import_core7 = require("@korajs/core");
|
|
1178
|
+
|
|
1179
|
+
// src/lww/row-version.ts
|
|
1180
|
+
var import_core6 = require("@korajs/core");
|
|
1181
|
+
var EMPTY_ROW_VERSION = "";
|
|
1182
|
+
function serializeRowVersion(timestamp) {
|
|
1183
|
+
return import_core6.HybridLogicalClock.serialize(timestamp);
|
|
1184
|
+
}
|
|
1185
|
+
function rowVersionFromRecord(row) {
|
|
1186
|
+
const version = row._version;
|
|
1187
|
+
if (typeof version === "string" && version.length > 0) {
|
|
1188
|
+
return import_core6.HybridLogicalClock.deserialize(version);
|
|
1189
|
+
}
|
|
1190
|
+
return { wallTime: row._updated_at, logical: 0, nodeId: "" };
|
|
1191
|
+
}
|
|
1192
|
+
function isIncomingNewerThanRow(incoming, row) {
|
|
1193
|
+
return import_core6.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
|
|
1194
|
+
}
|
|
1195
|
+
function lwwVersionWhereClause(remoteVersion) {
|
|
1196
|
+
return {
|
|
1197
|
+
sql: "(_version = ? OR _version < ?)",
|
|
1198
|
+
params: [EMPTY_ROW_VERSION, remoteVersion]
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/query/sql-builder.ts
|
|
1203
|
+
function buildSelectQuery(descriptor, fields) {
|
|
1204
|
+
const params = [];
|
|
1205
|
+
const parts = [`SELECT * FROM ${descriptor.collection}`];
|
|
1206
|
+
const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
|
|
1207
|
+
const deletedFilter = "_deleted = 0";
|
|
1208
|
+
if (whereClause) {
|
|
1209
|
+
parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
|
|
1210
|
+
} else {
|
|
1211
|
+
parts.push(`WHERE ${deletedFilter}`);
|
|
1212
|
+
}
|
|
1213
|
+
if (descriptor.orderBy.length > 0) {
|
|
1214
|
+
const orderParts = descriptor.orderBy.map((o) => {
|
|
1215
|
+
validateFieldName(o.field, fields);
|
|
1216
|
+
return `${o.field} ${o.direction.toUpperCase()}`;
|
|
1217
|
+
});
|
|
1218
|
+
parts.push(`ORDER BY ${orderParts.join(", ")}`);
|
|
1219
|
+
}
|
|
1220
|
+
if (descriptor.limit !== void 0) {
|
|
1221
|
+
parts.push(`LIMIT ${descriptor.limit}`);
|
|
1222
|
+
}
|
|
1223
|
+
if (descriptor.offset !== void 0) {
|
|
1224
|
+
parts.push(`OFFSET ${descriptor.offset}`);
|
|
1225
|
+
}
|
|
1226
|
+
return { sql: parts.join(" "), params };
|
|
1227
|
+
}
|
|
1228
|
+
function buildCountQuery(descriptor, fields) {
|
|
1229
|
+
const params = [];
|
|
1230
|
+
const parts = [`SELECT COUNT(*) as count FROM ${descriptor.collection}`];
|
|
1231
|
+
const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
|
|
1232
|
+
const deletedFilter = "_deleted = 0";
|
|
1233
|
+
if (whereClause) {
|
|
1234
|
+
parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
|
|
1235
|
+
} else {
|
|
1236
|
+
parts.push(`WHERE ${deletedFilter}`);
|
|
1237
|
+
}
|
|
1238
|
+
return { sql: parts.join(" "), params };
|
|
1239
|
+
}
|
|
1240
|
+
function buildInsertQuery(collection, record) {
|
|
1241
|
+
const columns = Object.keys(record);
|
|
1242
|
+
const placeholders = columns.map(() => "?");
|
|
1243
|
+
const params = Object.values(record);
|
|
1244
|
+
const sql = `INSERT INTO ${collection} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
|
|
1245
|
+
return { sql, params };
|
|
1246
|
+
}
|
|
1247
|
+
function buildUpdateQuery(collection, id, changes) {
|
|
1248
|
+
const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
|
|
1249
|
+
const params = [...Object.values(changes), id];
|
|
1250
|
+
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1251
|
+
return { sql, params };
|
|
1252
|
+
}
|
|
1253
|
+
function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1254
|
+
if (version !== void 0) {
|
|
1255
|
+
return {
|
|
1256
|
+
sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ?`,
|
|
1257
|
+
params: [updatedAt, version, id]
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
|
|
1262
|
+
params: [updatedAt, id]
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
|
|
1266
|
+
const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
|
|
1267
|
+
const lww = lwwVersionWhereClause(remoteVersion);
|
|
1268
|
+
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
|
|
1269
|
+
const params = [...Object.values(changes), id, ...lww.params];
|
|
1270
|
+
return { sql, params };
|
|
1271
|
+
}
|
|
1272
|
+
function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1273
|
+
const lww = lwwVersionWhereClause(version);
|
|
1274
|
+
return {
|
|
1275
|
+
sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ? AND ${lww.sql}`,
|
|
1276
|
+
params: [updatedAt, version, id, ...lww.params]
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
var VALID_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"]);
|
|
1280
|
+
function buildWhereClauseParts(where, fields, params) {
|
|
1281
|
+
const conditions = [];
|
|
1282
|
+
for (const [fieldName, value] of Object.entries(where)) {
|
|
1283
|
+
validateFieldName(fieldName, fields);
|
|
1284
|
+
const descriptor = fields[fieldName];
|
|
1285
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1286
|
+
const ops = value;
|
|
1287
|
+
for (const [op, opValue] of Object.entries(ops)) {
|
|
1288
|
+
if (!VALID_OPERATORS.has(op)) {
|
|
1289
|
+
throw new QueryError(`Unknown operator "${op}" on field "${fieldName}"`, {
|
|
1290
|
+
field: fieldName,
|
|
1291
|
+
operator: op,
|
|
1292
|
+
validOperators: [...VALID_OPERATORS]
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
conditions.push(buildOperatorCondition(fieldName, op, opValue, descriptor, params));
|
|
1296
|
+
}
|
|
1297
|
+
} else {
|
|
1298
|
+
conditions.push(buildOperatorCondition(fieldName, "$eq", value, descriptor, params));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (conditions.length === 0) return null;
|
|
1302
|
+
return conditions.join(" AND ");
|
|
1303
|
+
}
|
|
1304
|
+
function buildOperatorCondition(fieldName, operator, value, descriptor, params) {
|
|
1305
|
+
const sqlValue = descriptor?.kind === "boolean" && typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
1306
|
+
switch (operator) {
|
|
1307
|
+
case "$eq":
|
|
1308
|
+
if (sqlValue === null) {
|
|
1309
|
+
return `${fieldName} IS NULL`;
|
|
1310
|
+
}
|
|
1311
|
+
params.push(sqlValue);
|
|
1312
|
+
return `${fieldName} = ?`;
|
|
1313
|
+
case "$ne":
|
|
1314
|
+
if (sqlValue === null) {
|
|
1315
|
+
return `${fieldName} IS NOT NULL`;
|
|
1316
|
+
}
|
|
1317
|
+
params.push(sqlValue);
|
|
1318
|
+
return `${fieldName} != ?`;
|
|
1319
|
+
case "$gt":
|
|
1320
|
+
params.push(sqlValue);
|
|
1321
|
+
return `${fieldName} > ?`;
|
|
1322
|
+
case "$gte":
|
|
1323
|
+
params.push(sqlValue);
|
|
1324
|
+
return `${fieldName} >= ?`;
|
|
1325
|
+
case "$lt":
|
|
1326
|
+
params.push(sqlValue);
|
|
1327
|
+
return `${fieldName} < ?`;
|
|
1328
|
+
case "$lte":
|
|
1329
|
+
params.push(sqlValue);
|
|
1330
|
+
return `${fieldName} <= ?`;
|
|
1331
|
+
case "$in": {
|
|
1332
|
+
if (!Array.isArray(sqlValue)) {
|
|
1333
|
+
throw new QueryError(`$in operator requires an array value for field "${fieldName}"`, {
|
|
1334
|
+
field: fieldName,
|
|
1335
|
+
received: typeof sqlValue
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
const placeholders = sqlValue.map(() => "?");
|
|
1339
|
+
for (const item of sqlValue) {
|
|
1340
|
+
params.push(
|
|
1341
|
+
descriptor?.kind === "boolean" && typeof item === "boolean" ? item ? 1 : 0 : item
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return `${fieldName} IN (${placeholders.join(", ")})`;
|
|
1345
|
+
}
|
|
1346
|
+
default:
|
|
1347
|
+
throw new QueryError(`Unknown operator "${operator}"`, { operator });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function validateFieldName(fieldName, fields) {
|
|
1351
|
+
const allowedFields = /* @__PURE__ */ new Set([
|
|
1352
|
+
...Object.keys(fields),
|
|
1353
|
+
"id",
|
|
1354
|
+
"createdAt",
|
|
1355
|
+
"updatedAt",
|
|
1356
|
+
"_created_at",
|
|
1357
|
+
"_updated_at"
|
|
1358
|
+
]);
|
|
1359
|
+
if (!allowedFields.has(fieldName)) {
|
|
1360
|
+
throw new QueryError(
|
|
1361
|
+
`Unknown field "${fieldName}" in query. Available fields: ${[...allowedFields].join(", ")}`,
|
|
1362
|
+
{ field: fieldName }
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/mutations/execute-delete.ts
|
|
1368
|
+
init_serializer();
|
|
1369
|
+
|
|
1370
|
+
// src/store/sequence-allocator.ts
|
|
1371
|
+
async function allocateNextSequenceNumber(adapter, nodeId) {
|
|
1372
|
+
const rows = await adapter.query(
|
|
1373
|
+
`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
|
|
1374
|
+
ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
|
|
1375
|
+
RETURNING sequence_number`,
|
|
1376
|
+
[nodeId]
|
|
1377
|
+
);
|
|
1378
|
+
const seq = rows[0]?.sequence_number;
|
|
1379
|
+
if (seq === void 0) {
|
|
1380
|
+
throw new Error(`Failed to allocate sequence number for node "${nodeId}"`);
|
|
1381
|
+
}
|
|
1382
|
+
return seq;
|
|
1383
|
+
}
|
|
1384
|
+
async function allocateNextSequenceInTransaction(tx, nodeId) {
|
|
1385
|
+
const rows = await tx.query(
|
|
1386
|
+
`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
|
|
1387
|
+
ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
|
|
1388
|
+
RETURNING sequence_number`,
|
|
1389
|
+
[nodeId]
|
|
1390
|
+
);
|
|
1391
|
+
const seq = rows[0]?.sequence_number;
|
|
1392
|
+
if (seq === void 0) {
|
|
1393
|
+
throw new Error(`Failed to allocate sequence number for node "${nodeId}" in transaction`);
|
|
1394
|
+
}
|
|
1395
|
+
return seq;
|
|
1396
|
+
}
|
|
1397
|
+
async function readSequenceNumber(adapter, nodeId) {
|
|
1398
|
+
const rows = await adapter.query(
|
|
1399
|
+
"SELECT sequence_number FROM _kora_version_vector WHERE node_id = ?",
|
|
1400
|
+
[nodeId]
|
|
1401
|
+
);
|
|
1402
|
+
return rows[0]?.sequence_number ?? 0;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/mutations/resolve-causal-deps.ts
|
|
1406
|
+
function resolveCausalDeps(ctx) {
|
|
1407
|
+
const merged = [];
|
|
1408
|
+
for (const id of ctx.extraCausalDeps ?? []) {
|
|
1409
|
+
if (!merged.includes(id)) {
|
|
1410
|
+
merged.push(id);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
for (const id of ctx.causalTracker?.nextCausalDeps(ctx.collection, ctx.inTransaction) ?? []) {
|
|
1414
|
+
if (!merged.includes(id)) {
|
|
1415
|
+
merged.push(id);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return merged;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/mutations/execute-delete.ts
|
|
1422
|
+
async function executeDelete(ctx, id, options) {
|
|
1423
|
+
const currentRows = await ctx.adapter.query(
|
|
1424
|
+
`SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
|
|
1425
|
+
[id]
|
|
1426
|
+
);
|
|
1427
|
+
if (!currentRows[0]) {
|
|
1428
|
+
throw new RecordNotFoundError(ctx.collection, id);
|
|
1429
|
+
}
|
|
1430
|
+
const causalDeps = resolveCausalDeps(ctx);
|
|
1431
|
+
let operation = options?.operation;
|
|
1432
|
+
const cascadedOps = [];
|
|
1433
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
1434
|
+
if (!operation) {
|
|
1435
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1436
|
+
operation = await (0, import_core7.createOperation)(
|
|
1437
|
+
{
|
|
1438
|
+
nodeId: ctx.nodeId,
|
|
1439
|
+
type: "delete",
|
|
1440
|
+
collection: ctx.collection,
|
|
1441
|
+
recordId: id,
|
|
1442
|
+
data: null,
|
|
1443
|
+
previousData: null,
|
|
1444
|
+
sequenceNumber,
|
|
1445
|
+
causalDeps,
|
|
1446
|
+
schemaVersion: ctx.schema.version
|
|
1447
|
+
},
|
|
1448
|
+
ctx.clock
|
|
1449
|
+
);
|
|
1450
|
+
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1451
|
+
}
|
|
1452
|
+
if (ctx.relationEnforcer && !options?.skipReferentialEnforcement) {
|
|
1453
|
+
const enforcementResult = await ctx.relationEnforcer.enforceDelete(ctx.collection, id, tx, [
|
|
1454
|
+
operation.id
|
|
1455
|
+
]);
|
|
1456
|
+
cascadedOps.push(...enforcementResult.operations);
|
|
1457
|
+
}
|
|
1458
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1459
|
+
const deleteQuery = buildSoftDeleteQuery(
|
|
1460
|
+
ctx.collection,
|
|
1461
|
+
id,
|
|
1462
|
+
operation.timestamp.wallTime,
|
|
1463
|
+
version
|
|
1464
|
+
);
|
|
1465
|
+
const opInsert = buildInsertQuery(
|
|
1466
|
+
`_kora_ops_${ctx.collection}`,
|
|
1467
|
+
serializeOperation(operation)
|
|
1468
|
+
);
|
|
1469
|
+
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
1470
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1471
|
+
});
|
|
1472
|
+
if (!operation) {
|
|
1473
|
+
throw new Error("Delete operation was not created");
|
|
1474
|
+
}
|
|
1475
|
+
ctx.onMutation(ctx.collection, operation);
|
|
1476
|
+
for (const cascadedOp of cascadedOps) {
|
|
1477
|
+
ctx.onMutation(cascadedOp.collection, cascadedOp);
|
|
1478
|
+
}
|
|
1479
|
+
return [operation, ...cascadedOps];
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/mutations/execute-insert.ts
|
|
1483
|
+
var import_core8 = require("@korajs/core");
|
|
1484
|
+
init_serializer();
|
|
1485
|
+
async function executeInsert(ctx, data) {
|
|
1486
|
+
const validated = (0, import_core8.validateRecord)(ctx.collection, ctx.definition, data, "insert");
|
|
1487
|
+
const recordId = (0, import_core8.generateUUIDv7)();
|
|
1488
|
+
for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
|
|
1489
|
+
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
1490
|
+
validated[fieldName] = Date.now();
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const causalDeps = resolveCausalDeps(ctx);
|
|
1494
|
+
let operation;
|
|
1495
|
+
let record;
|
|
1496
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
1497
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1498
|
+
operation = await (0, import_core8.createOperation)(
|
|
1499
|
+
{
|
|
1500
|
+
nodeId: ctx.nodeId,
|
|
1501
|
+
type: "insert",
|
|
1502
|
+
collection: ctx.collection,
|
|
1503
|
+
recordId,
|
|
1504
|
+
data: { ...validated },
|
|
1505
|
+
previousData: null,
|
|
1506
|
+
sequenceNumber,
|
|
1507
|
+
causalDeps,
|
|
1508
|
+
schemaVersion: ctx.schema.version
|
|
1509
|
+
},
|
|
1510
|
+
ctx.clock
|
|
1511
|
+
);
|
|
1512
|
+
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1513
|
+
const serializedData = serializeRecord(validated, ctx.definition.fields);
|
|
1514
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1515
|
+
record = {
|
|
1516
|
+
id: recordId,
|
|
1517
|
+
...serializedData,
|
|
1518
|
+
_created_at: operation.timestamp.wallTime,
|
|
1519
|
+
_updated_at: operation.timestamp.wallTime,
|
|
1520
|
+
_version: version
|
|
1521
|
+
};
|
|
1522
|
+
const builtInsert = buildInsertQuery(ctx.collection, record);
|
|
1523
|
+
const opInsert = buildInsertQuery(
|
|
1524
|
+
`_kora_ops_${ctx.collection}`,
|
|
1525
|
+
serializeOperation(operation)
|
|
1526
|
+
);
|
|
1527
|
+
await tx.execute(builtInsert.sql, builtInsert.params);
|
|
1528
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1529
|
+
});
|
|
1530
|
+
ctx.onMutation(ctx.collection, operation);
|
|
1531
|
+
return {
|
|
1532
|
+
id: recordId,
|
|
1533
|
+
...validated,
|
|
1534
|
+
createdAt: operation.timestamp.wallTime,
|
|
1535
|
+
updatedAt: operation.timestamp.wallTime
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/mutations/execute-update.ts
|
|
1540
|
+
var import_core10 = require("@korajs/core");
|
|
1541
|
+
init_serializer();
|
|
1542
|
+
|
|
1543
|
+
// src/state-machine/state-validator.ts
|
|
1544
|
+
var import_core9 = require("@korajs/core");
|
|
1545
|
+
var InvalidStateTransitionError = class extends import_core9.KoraError {
|
|
1546
|
+
constructor(collection, recordId, field, fromState, toState, allowedStates) {
|
|
1547
|
+
super(
|
|
1548
|
+
`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)"}`,
|
|
1549
|
+
"INVALID_STATE_TRANSITION",
|
|
1550
|
+
{ collection, recordId, field, fromState, toState, allowedStates }
|
|
1551
|
+
);
|
|
1552
|
+
this.collection = collection;
|
|
1553
|
+
this.recordId = recordId;
|
|
1554
|
+
this.field = field;
|
|
1555
|
+
this.fromState = fromState;
|
|
1556
|
+
this.toState = toState;
|
|
1557
|
+
this.allowedStates = allowedStates;
|
|
1558
|
+
this.name = "InvalidStateTransitionError";
|
|
1559
|
+
}
|
|
1560
|
+
collection;
|
|
1561
|
+
recordId;
|
|
1562
|
+
field;
|
|
1563
|
+
fromState;
|
|
470
1564
|
toState;
|
|
471
1565
|
allowedStates;
|
|
472
1566
|
};
|
|
@@ -482,7 +1576,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
|
|
|
482
1576
|
collection: collectionName,
|
|
483
1577
|
transitions: stateMachine.transitions
|
|
484
1578
|
};
|
|
485
|
-
const transitionResult = (0,
|
|
1579
|
+
const transitionResult = (0, import_core9.validateTransition)(constraint, currentState, newState);
|
|
486
1580
|
if (transitionResult.valid) {
|
|
487
1581
|
return { valid: true, allowedStates: transitionResult.allowedTargets };
|
|
488
1582
|
}
|
|
@@ -528,18 +1622,103 @@ function validateUpdateStateMachine(collectionName, recordId, collectionDef, cur
|
|
|
528
1622
|
return filtered;
|
|
529
1623
|
}
|
|
530
1624
|
|
|
1625
|
+
// src/mutations/execute-update.ts
|
|
1626
|
+
async function executeUpdate(ctx, id, data) {
|
|
1627
|
+
const currentRows = await ctx.adapter.query(
|
|
1628
|
+
`SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
|
|
1629
|
+
[id]
|
|
1630
|
+
);
|
|
1631
|
+
const currentRow = currentRows[0];
|
|
1632
|
+
if (!currentRow) {
|
|
1633
|
+
throw new RecordNotFoundError(ctx.collection, id);
|
|
1634
|
+
}
|
|
1635
|
+
let validated = (0, import_core10.validateRecord)(ctx.collection, ctx.definition, data, "update");
|
|
1636
|
+
const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
|
|
1637
|
+
validated = validateUpdateStateMachine(
|
|
1638
|
+
ctx.collection,
|
|
1639
|
+
id,
|
|
1640
|
+
ctx.definition,
|
|
1641
|
+
currentRecord,
|
|
1642
|
+
validated
|
|
1643
|
+
);
|
|
1644
|
+
if (Object.keys(validated).length === 0) {
|
|
1645
|
+
return { ...currentRecord };
|
|
1646
|
+
}
|
|
1647
|
+
const previousData = {};
|
|
1648
|
+
const resolvedData = {};
|
|
1649
|
+
const atomicOps = {};
|
|
1650
|
+
for (const key of Object.keys(validated)) {
|
|
1651
|
+
const value = validated[key];
|
|
1652
|
+
previousData[key] = currentRecord[key];
|
|
1653
|
+
if ((0, import_core10.isAtomicOp)(value)) {
|
|
1654
|
+
resolvedData[key] = (0, import_core10.resolveAtomicOp)(currentRecord[key], value);
|
|
1655
|
+
atomicOps[key] = (0, import_core10.toAtomicOp)(value);
|
|
1656
|
+
} else {
|
|
1657
|
+
resolvedData[key] = value;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
1661
|
+
const causalDeps = resolveCausalDeps(ctx);
|
|
1662
|
+
let operation;
|
|
1663
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
1664
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1665
|
+
operation = await (0, import_core10.createOperation)(
|
|
1666
|
+
{
|
|
1667
|
+
nodeId: ctx.nodeId,
|
|
1668
|
+
type: "update",
|
|
1669
|
+
collection: ctx.collection,
|
|
1670
|
+
recordId: id,
|
|
1671
|
+
data: { ...resolvedData },
|
|
1672
|
+
previousData,
|
|
1673
|
+
sequenceNumber,
|
|
1674
|
+
causalDeps,
|
|
1675
|
+
schemaVersion: ctx.schema.version,
|
|
1676
|
+
...hasAtomicOps ? { atomicOps } : {}
|
|
1677
|
+
},
|
|
1678
|
+
ctx.clock
|
|
1679
|
+
);
|
|
1680
|
+
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1681
|
+
const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
|
|
1682
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1683
|
+
const updateQuery = buildUpdateQuery(ctx.collection, id, {
|
|
1684
|
+
...serializedChanges,
|
|
1685
|
+
_updated_at: operation.timestamp.wallTime,
|
|
1686
|
+
_version: version
|
|
1687
|
+
});
|
|
1688
|
+
const opInsert = buildInsertQuery(
|
|
1689
|
+
`_kora_ops_${ctx.collection}`,
|
|
1690
|
+
serializeOperation(operation)
|
|
1691
|
+
);
|
|
1692
|
+
await tx.execute(updateQuery.sql, updateQuery.params);
|
|
1693
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
1694
|
+
});
|
|
1695
|
+
ctx.onMutation(ctx.collection, operation);
|
|
1696
|
+
const rows = await ctx.adapter.query(
|
|
1697
|
+
`SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
|
|
1698
|
+
[id]
|
|
1699
|
+
);
|
|
1700
|
+
const row = rows[0];
|
|
1701
|
+
if (!row) {
|
|
1702
|
+
throw new RecordNotFoundError(ctx.collection, id);
|
|
1703
|
+
}
|
|
1704
|
+
return deserializeRecord(row, ctx.definition.fields);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
531
1707
|
// src/collection/collection.ts
|
|
1708
|
+
init_serializer();
|
|
532
1709
|
var Collection = class {
|
|
533
|
-
constructor(name, definition, schema, adapter, clock, nodeId,
|
|
1710
|
+
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
|
|
534
1711
|
this.name = name;
|
|
535
1712
|
this.definition = definition;
|
|
536
1713
|
this.schema = schema;
|
|
537
1714
|
this.adapter = adapter;
|
|
538
1715
|
this.clock = clock;
|
|
539
1716
|
this.nodeId = nodeId;
|
|
540
|
-
this.
|
|
1717
|
+
this.allocateSequenceNumber = allocateSequenceNumber;
|
|
541
1718
|
this.onMutation = onMutation;
|
|
542
|
-
this.relationEnforcer = relationEnforcer
|
|
1719
|
+
this.relationEnforcer = relationEnforcer;
|
|
1720
|
+
this.mutationHandler = mutationHandler;
|
|
1721
|
+
this.causalTracker = causalTracker;
|
|
543
1722
|
}
|
|
544
1723
|
name;
|
|
545
1724
|
definition;
|
|
@@ -547,72 +1726,32 @@ var Collection = class {
|
|
|
547
1726
|
adapter;
|
|
548
1727
|
clock;
|
|
549
1728
|
nodeId;
|
|
550
|
-
|
|
1729
|
+
allocateSequenceNumber;
|
|
551
1730
|
onMutation;
|
|
552
1731
|
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);
|
|
1732
|
+
mutationHandler;
|
|
1733
|
+
causalTracker;
|
|
1734
|
+
mutationContext() {
|
|
606
1735
|
return {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1736
|
+
collection: this.name,
|
|
1737
|
+
definition: this.definition,
|
|
1738
|
+
schema: this.schema,
|
|
1739
|
+
adapter: this.adapter,
|
|
1740
|
+
clock: this.clock,
|
|
1741
|
+
nodeId: this.nodeId,
|
|
1742
|
+
allocateSequenceNumber: this.allocateSequenceNumber,
|
|
1743
|
+
onMutation: this.onMutation,
|
|
1744
|
+
relationEnforcer: this.relationEnforcer,
|
|
1745
|
+
causalTracker: this.causalTracker,
|
|
1746
|
+
inTransaction: false
|
|
611
1747
|
};
|
|
612
1748
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1749
|
+
async insert(data) {
|
|
1750
|
+
if (this.mutationHandler) {
|
|
1751
|
+
return this.mutationHandler.insert(this.name, data);
|
|
1752
|
+
}
|
|
1753
|
+
return executeInsert(this.mutationContext(), data);
|
|
1754
|
+
}
|
|
616
1755
|
async findById(id) {
|
|
617
1756
|
const rows = await this.adapter.query(
|
|
618
1757
|
`SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
|
|
@@ -622,175 +1761,34 @@ var Collection = class {
|
|
|
622
1761
|
if (!row) return null;
|
|
623
1762
|
return deserializeRecord(row, this.definition.fields);
|
|
624
1763
|
}
|
|
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
1764
|
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;
|
|
1765
|
+
if (this.mutationHandler) {
|
|
1766
|
+
return this.mutationHandler.update(this.name, id, data);
|
|
658
1767
|
}
|
|
659
|
-
|
|
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);
|
|
711
|
-
}
|
|
712
|
-
return updatedRow;
|
|
1768
|
+
return executeUpdate(this.mutationContext(), id, data);
|
|
713
1769
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
async delete(id) {
|
|
732
|
-
const currentRows = await this.adapter.query(
|
|
733
|
-
`SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
|
|
734
|
-
[id]
|
|
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);
|
|
1770
|
+
async delete(id) {
|
|
1771
|
+
if (this.mutationHandler) {
|
|
1772
|
+
await this.mutationHandler.delete(this.name, id);
|
|
1773
|
+
return;
|
|
782
1774
|
}
|
|
1775
|
+
await executeDelete(this.mutationContext(), id);
|
|
783
1776
|
}
|
|
784
|
-
/** Get the collection name */
|
|
785
1777
|
getName() {
|
|
786
1778
|
return this.name;
|
|
787
1779
|
}
|
|
788
|
-
/** Get the collection definition */
|
|
789
1780
|
getDefinition() {
|
|
790
1781
|
return this.definition;
|
|
791
1782
|
}
|
|
1783
|
+
/** Replace the mutation handler (e.g. after ApplyPipeline is wired in createApp). */
|
|
1784
|
+
setMutationHandler(handler) {
|
|
1785
|
+
this.mutationHandler = handler;
|
|
1786
|
+
}
|
|
792
1787
|
};
|
|
793
1788
|
|
|
1789
|
+
// src/query/query-builder.ts
|
|
1790
|
+
init_serializer();
|
|
1791
|
+
|
|
794
1792
|
// src/query/pluralize.ts
|
|
795
1793
|
function isVowel(char) {
|
|
796
1794
|
if (!char) return false;
|
|
@@ -1085,7 +2083,8 @@ var QueryError2 = class extends Error {
|
|
|
1085
2083
|
};
|
|
1086
2084
|
|
|
1087
2085
|
// src/relations/relation-enforcer.ts
|
|
1088
|
-
var
|
|
2086
|
+
var import_core11 = require("@korajs/core");
|
|
2087
|
+
init_serializer();
|
|
1089
2088
|
|
|
1090
2089
|
// src/relations/relation-lookup.ts
|
|
1091
2090
|
function buildRelationLookup(schema) {
|
|
@@ -1109,7 +2108,7 @@ function getIncomingRelations(lookup, collection) {
|
|
|
1109
2108
|
}
|
|
1110
2109
|
|
|
1111
2110
|
// src/relations/relation-enforcer.ts
|
|
1112
|
-
var ReferentialIntegrityError = class extends
|
|
2111
|
+
var ReferentialIntegrityError = class extends import_core11.KoraError {
|
|
1113
2112
|
constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
|
|
1114
2113
|
super(
|
|
1115
2114
|
`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 +2130,11 @@ var RelationEnforcer = class {
|
|
|
1131
2130
|
adapter;
|
|
1132
2131
|
clock;
|
|
1133
2132
|
nodeId;
|
|
1134
|
-
getSequenceNumber;
|
|
1135
2133
|
constructor(config) {
|
|
1136
2134
|
this.schema = config.schema;
|
|
1137
2135
|
this.adapter = config.adapter;
|
|
1138
2136
|
this.clock = config.clock;
|
|
1139
2137
|
this.nodeId = config.nodeId;
|
|
1140
|
-
this.getSequenceNumber = config.getSequenceNumber;
|
|
1141
2138
|
this.lookup = buildRelationLookup(config.schema);
|
|
1142
2139
|
}
|
|
1143
2140
|
/**
|
|
@@ -1199,8 +2196,8 @@ var RelationEnforcer = class {
|
|
|
1199
2196
|
const operations = [];
|
|
1200
2197
|
for (const row of referencingRows) {
|
|
1201
2198
|
const now = Date.now();
|
|
1202
|
-
const sequenceNumber = this.
|
|
1203
|
-
const operation = await (0,
|
|
2199
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2200
|
+
const operation = await (0, import_core11.createOperation)(
|
|
1204
2201
|
{
|
|
1205
2202
|
nodeId: this.nodeId,
|
|
1206
2203
|
type: "delete",
|
|
@@ -1216,16 +2213,11 @@ var RelationEnforcer = class {
|
|
|
1216
2213
|
);
|
|
1217
2214
|
const deleteQuery = buildSoftDeleteQuery(sourceCollection, row.id, now);
|
|
1218
2215
|
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
1219
|
-
const opRow = serializeOperation(operation);
|
|
1220
2216
|
const opInsert = buildInsertQuery(
|
|
1221
2217
|
`_kora_ops_${sourceCollection}`,
|
|
1222
|
-
|
|
2218
|
+
serializeOperation(operation)
|
|
1223
2219
|
);
|
|
1224
2220
|
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
2221
|
operations.push(operation);
|
|
1230
2222
|
const cascadeResult = await this.enforceDelete(sourceCollection, row.id, tx, [operation.id]);
|
|
1231
2223
|
operations.push(...cascadeResult.operations);
|
|
@@ -1251,10 +2243,10 @@ var RelationEnforcer = class {
|
|
|
1251
2243
|
const operations = [];
|
|
1252
2244
|
for (const row of referencingRows) {
|
|
1253
2245
|
const now = Date.now();
|
|
1254
|
-
const sequenceNumber = this.
|
|
2246
|
+
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
1255
2247
|
const updateData = { [foreignKeyField]: null };
|
|
1256
2248
|
const previousData = { [foreignKeyField]: deletedRecordId };
|
|
1257
|
-
const operation = await (0,
|
|
2249
|
+
const operation = await (0, import_core11.createOperation)(
|
|
1258
2250
|
{
|
|
1259
2251
|
nodeId: this.nodeId,
|
|
1260
2252
|
type: "update",
|
|
@@ -1280,10 +2272,6 @@ var RelationEnforcer = class {
|
|
|
1280
2272
|
opRow
|
|
1281
2273
|
);
|
|
1282
2274
|
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
2275
|
operations.push(operation);
|
|
1288
2276
|
}
|
|
1289
2277
|
return operations;
|
|
@@ -1318,8 +2306,172 @@ var RelationEnforcer = class {
|
|
|
1318
2306
|
}
|
|
1319
2307
|
};
|
|
1320
2308
|
|
|
2309
|
+
// src/replay/replay-to.ts
|
|
2310
|
+
var import_core12 = require("@korajs/core");
|
|
2311
|
+
var import_internal = require("@korajs/core/internal");
|
|
2312
|
+
function collectCausalClosure(allOps, targetOperationId) {
|
|
2313
|
+
const opMap = /* @__PURE__ */ new Map();
|
|
2314
|
+
for (const op of allOps) {
|
|
2315
|
+
opMap.set(op.id, op);
|
|
2316
|
+
}
|
|
2317
|
+
const target = opMap.get(targetOperationId);
|
|
2318
|
+
if (!target) {
|
|
2319
|
+
throw new import_core12.OperationError(
|
|
2320
|
+
`Operation "${targetOperationId}" not found in the local operation log`,
|
|
2321
|
+
{
|
|
2322
|
+
operationId: targetOperationId
|
|
2323
|
+
}
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
const included = /* @__PURE__ */ new Set();
|
|
2327
|
+
const stack = [targetOperationId];
|
|
2328
|
+
while (stack.length > 0) {
|
|
2329
|
+
const id = stack.pop();
|
|
2330
|
+
if (id === void 0 || included.has(id)) {
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
included.add(id);
|
|
2334
|
+
const op = opMap.get(id);
|
|
2335
|
+
if (!op) {
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
for (const depId of op.causalDeps) {
|
|
2339
|
+
if (opMap.has(depId)) {
|
|
2340
|
+
stack.push(depId);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
const subset = allOps.filter((op) => included.has(op.id));
|
|
2345
|
+
return (0, import_internal.topologicalSort)(subset);
|
|
2346
|
+
}
|
|
2347
|
+
function buildReplaySnapshot(schema, allOps, targetOperationId) {
|
|
2348
|
+
const operationsApplied = collectCausalClosure(allOps, targetOperationId);
|
|
2349
|
+
const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
|
|
2350
|
+
if (!targetOperation) {
|
|
2351
|
+
throw new import_core12.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
|
|
2352
|
+
operationId: targetOperationId
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
const memory = /* @__PURE__ */ new Map();
|
|
2356
|
+
for (const op of operationsApplied) {
|
|
2357
|
+
applyOperationToMemory(memory, op, schema);
|
|
2358
|
+
}
|
|
2359
|
+
const collections = materializeCollections(schema, memory);
|
|
2360
|
+
return {
|
|
2361
|
+
targetOperation,
|
|
2362
|
+
operationsApplied,
|
|
2363
|
+
collections,
|
|
2364
|
+
findRecord(collection, recordId) {
|
|
2365
|
+
const colMap = memory.get(collection);
|
|
2366
|
+
const record = colMap?.get(recordId);
|
|
2367
|
+
if (!record || record.deleted) {
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
const definition = schema.collections[collection];
|
|
2371
|
+
if (!definition) {
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
return toCollectionRecord(record, definition);
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
function applyOperationToMemory(state, op, schema) {
|
|
2379
|
+
const definition = schema.collections[op.collection];
|
|
2380
|
+
if (!definition) {
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
let colMap = state.get(op.collection);
|
|
2384
|
+
if (!colMap) {
|
|
2385
|
+
colMap = /* @__PURE__ */ new Map();
|
|
2386
|
+
state.set(op.collection, colMap);
|
|
2387
|
+
}
|
|
2388
|
+
const wallTime = op.timestamp.wallTime;
|
|
2389
|
+
switch (op.type) {
|
|
2390
|
+
case "insert": {
|
|
2391
|
+
if (!op.data) {
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
colMap.set(op.recordId, {
|
|
2395
|
+
id: op.recordId,
|
|
2396
|
+
fields: { ...op.data },
|
|
2397
|
+
deleted: false,
|
|
2398
|
+
createdAt: wallTime,
|
|
2399
|
+
updatedAt: wallTime
|
|
2400
|
+
});
|
|
2401
|
+
break;
|
|
2402
|
+
}
|
|
2403
|
+
case "update": {
|
|
2404
|
+
if (!op.data) {
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const existing = colMap.get(op.recordId);
|
|
2408
|
+
if (existing && !existing.deleted) {
|
|
2409
|
+
existing.fields = { ...existing.fields, ...op.data };
|
|
2410
|
+
existing.updatedAt = wallTime;
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
if (existing?.deleted) {
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
colMap.set(op.recordId, {
|
|
2417
|
+
id: op.recordId,
|
|
2418
|
+
fields: { ...op.data },
|
|
2419
|
+
deleted: false,
|
|
2420
|
+
createdAt: wallTime,
|
|
2421
|
+
updatedAt: wallTime
|
|
2422
|
+
});
|
|
2423
|
+
break;
|
|
2424
|
+
}
|
|
2425
|
+
case "delete": {
|
|
2426
|
+
const existing = colMap.get(op.recordId);
|
|
2427
|
+
if (existing) {
|
|
2428
|
+
existing.deleted = true;
|
|
2429
|
+
existing.updatedAt = wallTime;
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
colMap.set(op.recordId, {
|
|
2433
|
+
id: op.recordId,
|
|
2434
|
+
fields: {},
|
|
2435
|
+
deleted: true,
|
|
2436
|
+
createdAt: wallTime,
|
|
2437
|
+
updatedAt: wallTime
|
|
2438
|
+
});
|
|
2439
|
+
break;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
function materializeCollections(schema, memory) {
|
|
2444
|
+
const collections = {};
|
|
2445
|
+
for (const [collectionName, definition] of Object.entries(schema.collections)) {
|
|
2446
|
+
const colMap = memory.get(collectionName);
|
|
2447
|
+
const records = [];
|
|
2448
|
+
if (colMap) {
|
|
2449
|
+
for (const record of colMap.values()) {
|
|
2450
|
+
if (!record.deleted) {
|
|
2451
|
+
records.push(toCollectionRecord(record, definition));
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
collections[collectionName] = records;
|
|
2456
|
+
}
|
|
2457
|
+
return collections;
|
|
2458
|
+
}
|
|
2459
|
+
function toCollectionRecord(record, definition) {
|
|
2460
|
+
const result = {
|
|
2461
|
+
id: record.id,
|
|
2462
|
+
createdAt: record.createdAt,
|
|
2463
|
+
updatedAt: record.updatedAt
|
|
2464
|
+
};
|
|
2465
|
+
for (const [fieldName] of Object.entries(definition.fields)) {
|
|
2466
|
+
if (fieldName in record.fields) {
|
|
2467
|
+
result[fieldName] = record.fields[fieldName];
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return result;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
1321
2473
|
// src/sequences/sequence-manager.ts
|
|
1322
|
-
var
|
|
2474
|
+
var import_core13 = require("@korajs/core");
|
|
1323
2475
|
var SequenceManager = class {
|
|
1324
2476
|
adapter;
|
|
1325
2477
|
nodeId;
|
|
@@ -1337,7 +2489,7 @@ var SequenceManager = class {
|
|
|
1337
2489
|
async next(name, config) {
|
|
1338
2490
|
const scope = config?.scope ?? "";
|
|
1339
2491
|
const startAt = config?.startAt ?? 1;
|
|
1340
|
-
const format = config?.format ?? (0,
|
|
2492
|
+
const format = config?.format ?? (0, import_core13.defaultSequenceFormat)(name);
|
|
1341
2493
|
let counter = 0;
|
|
1342
2494
|
await this.adapter.transaction(async (tx) => {
|
|
1343
2495
|
const rows = await tx.query(
|
|
@@ -1359,7 +2511,7 @@ var SequenceManager = class {
|
|
|
1359
2511
|
);
|
|
1360
2512
|
}
|
|
1361
2513
|
});
|
|
1362
|
-
return (0,
|
|
2514
|
+
return (0, import_core13.formatSequenceValue)(format, counter, this.nodeId);
|
|
1363
2515
|
}
|
|
1364
2516
|
/**
|
|
1365
2517
|
* Get the current counter value without incrementing.
|
|
@@ -1401,6 +2553,9 @@ var SequenceManager = class {
|
|
|
1401
2553
|
}
|
|
1402
2554
|
};
|
|
1403
2555
|
|
|
2556
|
+
// src/store/store.ts
|
|
2557
|
+
init_serializer();
|
|
2558
|
+
|
|
1404
2559
|
// src/subscription/bloom-filter.ts
|
|
1405
2560
|
var FNV_OFFSET_BASIS = 2166136261;
|
|
1406
2561
|
var FNV_PRIME = 16777619;
|
|
@@ -1561,6 +2716,7 @@ var SubscriptionManager = class {
|
|
|
1561
2716
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1562
2717
|
pendingCollections = /* @__PURE__ */ new Set();
|
|
1563
2718
|
flushScheduled = false;
|
|
2719
|
+
onQuerySubscribed;
|
|
1564
2720
|
// Bloom filter state
|
|
1565
2721
|
bloomFilter = null;
|
|
1566
2722
|
bloomDirty = false;
|
|
@@ -1577,6 +2733,7 @@ var SubscriptionManager = class {
|
|
|
1577
2733
|
this.bloomThreshold = options?.bloomThreshold ?? DEFAULT_BLOOM_THRESHOLD;
|
|
1578
2734
|
this.bloomExpectedItems = options?.bloomExpectedItems ?? DEFAULT_BLOOM_EXPECTED_ITEMS;
|
|
1579
2735
|
this.bloomFalsePositiveRate = options?.bloomFalsePositiveRate ?? DEFAULT_BLOOM_FALSE_POSITIVE_RATE;
|
|
2736
|
+
this.onQuerySubscribed = options?.onQuerySubscribed;
|
|
1580
2737
|
}
|
|
1581
2738
|
/**
|
|
1582
2739
|
* Register a new subscription.
|
|
@@ -1597,9 +2754,11 @@ var SubscriptionManager = class {
|
|
|
1597
2754
|
};
|
|
1598
2755
|
this.subscriptions.set(id, subscription);
|
|
1599
2756
|
this.bloomDirty = true;
|
|
2757
|
+
const externalCleanup = this.onQuerySubscribed?.(descriptor);
|
|
1600
2758
|
return () => {
|
|
1601
2759
|
this.subscriptions.delete(id);
|
|
1602
2760
|
this.bloomDirty = true;
|
|
2761
|
+
externalCleanup?.();
|
|
1603
2762
|
};
|
|
1604
2763
|
}
|
|
1605
2764
|
/**
|
|
@@ -1620,6 +2779,7 @@ var SubscriptionManager = class {
|
|
|
1620
2779
|
};
|
|
1621
2780
|
this.subscriptions.set(id, subscription);
|
|
1622
2781
|
this.bloomDirty = true;
|
|
2782
|
+
const externalCleanup = this.onQuerySubscribed?.(descriptor);
|
|
1623
2783
|
executeFn().then((results) => {
|
|
1624
2784
|
if (this.subscriptions.has(id)) {
|
|
1625
2785
|
subscription.lastResults = results;
|
|
@@ -1629,6 +2789,7 @@ var SubscriptionManager = class {
|
|
|
1629
2789
|
return () => {
|
|
1630
2790
|
this.subscriptions.delete(id);
|
|
1631
2791
|
this.bloomDirty = true;
|
|
2792
|
+
externalCleanup?.();
|
|
1632
2793
|
};
|
|
1633
2794
|
}
|
|
1634
2795
|
/**
|
|
@@ -1764,10 +2925,7 @@ var SubscriptionManager = class {
|
|
|
1764
2925
|
* any included collection dependencies.
|
|
1765
2926
|
*/
|
|
1766
2927
|
rebuildBloomFilter() {
|
|
1767
|
-
const filter = new SubscriptionBloomFilter(
|
|
1768
|
-
this.bloomExpectedItems,
|
|
1769
|
-
this.bloomFalsePositiveRate
|
|
1770
|
-
);
|
|
2928
|
+
const filter = new SubscriptionBloomFilter(this.bloomExpectedItems, this.bloomFalsePositiveRate);
|
|
1771
2929
|
for (const sub of this.subscriptions.values()) {
|
|
1772
2930
|
filter.add(sub.descriptor.collection);
|
|
1773
2931
|
if (sub.descriptor.includeCollections) {
|
|
@@ -1787,18 +2945,33 @@ var SubscriptionManager = class {
|
|
|
1787
2945
|
});
|
|
1788
2946
|
}
|
|
1789
2947
|
/**
|
|
1790
|
-
* Compare two result sets. Uses
|
|
1791
|
-
*
|
|
2948
|
+
* Compare two result sets. Uses ID-based comparison first (same length, same IDs),
|
|
2949
|
+
* then deep comparison of remaining fields when IDs match.
|
|
2950
|
+
* Avoids JSON.stringify which is O(n) and fragile to field ordering differences.
|
|
1792
2951
|
*/
|
|
1793
2952
|
resultsEqual(prev, next) {
|
|
1794
2953
|
if (prev.length !== next.length) return false;
|
|
1795
2954
|
if (prev.length === 0) return true;
|
|
1796
|
-
|
|
2955
|
+
for (let i = 0; i < prev.length; i++) {
|
|
2956
|
+
if (prev[i]?.id !== next[i]?.id) return false;
|
|
2957
|
+
}
|
|
2958
|
+
for (let i = 0; i < prev.length; i++) {
|
|
2959
|
+
const a = prev[i];
|
|
2960
|
+
const b = next[i];
|
|
2961
|
+
const keysA = Object.keys(a);
|
|
2962
|
+
const keysB = Object.keys(b);
|
|
2963
|
+
if (keysA.length !== keysB.length) return false;
|
|
2964
|
+
for (const key of keysA) {
|
|
2965
|
+
if (a[key] !== b[key]) return false;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
return true;
|
|
1797
2969
|
}
|
|
1798
2970
|
};
|
|
1799
2971
|
|
|
1800
2972
|
// src/transaction/transaction-context.ts
|
|
1801
|
-
var
|
|
2973
|
+
var import_core14 = require("@korajs/core");
|
|
2974
|
+
init_serializer();
|
|
1802
2975
|
var TransactionContext = class {
|
|
1803
2976
|
transactionId;
|
|
1804
2977
|
mutationName;
|
|
@@ -1808,7 +2981,7 @@ var TransactionContext = class {
|
|
|
1808
2981
|
config;
|
|
1809
2982
|
constructor(config) {
|
|
1810
2983
|
this.config = config;
|
|
1811
|
-
this.transactionId = (0,
|
|
2984
|
+
this.transactionId = (0, import_core14.generateUUIDv7)();
|
|
1812
2985
|
}
|
|
1813
2986
|
/**
|
|
1814
2987
|
* Set a human-readable mutation name for this transaction.
|
|
@@ -1855,16 +3028,48 @@ var TransactionContext = class {
|
|
|
1855
3028
|
if (this.buffer.length === 0) {
|
|
1856
3029
|
return { operations: [], affectedCollections: /* @__PURE__ */ new Set() };
|
|
1857
3030
|
}
|
|
3031
|
+
const handler = this.config.localMutationHandler;
|
|
3032
|
+
if (handler?.commitTransaction) {
|
|
3033
|
+
return handler.commitTransaction({
|
|
3034
|
+
entries: this.buffer.map((entry) => ({
|
|
3035
|
+
operation: entry.operation,
|
|
3036
|
+
commands: entry.commands,
|
|
3037
|
+
collection: entry.collection
|
|
3038
|
+
})),
|
|
3039
|
+
transactionId: this.transactionId,
|
|
3040
|
+
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
1858
3043
|
const operations = [];
|
|
1859
3044
|
const affectedCollections = /* @__PURE__ */ new Set();
|
|
1860
3045
|
await this.config.adapter.transaction(async (tx) => {
|
|
1861
3046
|
for (const entry of this.buffer) {
|
|
3047
|
+
if (entry.operation.type === "delete" && this.config.relationEnforcer) {
|
|
3048
|
+
const cascadeResult = await this.config.relationEnforcer.enforceDelete(
|
|
3049
|
+
entry.collection,
|
|
3050
|
+
entry.operation.recordId,
|
|
3051
|
+
tx,
|
|
3052
|
+
[entry.operation.id]
|
|
3053
|
+
);
|
|
3054
|
+
for (const cascadedOp of cascadeResult.operations) {
|
|
3055
|
+
operations.push(cascadedOp);
|
|
3056
|
+
affectedCollections.add(cascadedOp.collection);
|
|
3057
|
+
this.config.causalTracker?.afterOperation(cascadedOp.collection, cascadedOp.id, true);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
1862
3060
|
for (const cmd of entry.commands) {
|
|
1863
3061
|
await tx.execute(cmd.sql, cmd.params);
|
|
1864
3062
|
}
|
|
1865
3063
|
operations.push(entry.operation);
|
|
1866
3064
|
affectedCollections.add(entry.collection);
|
|
1867
3065
|
}
|
|
3066
|
+
const finalSeq = this.config.sequenceAllocator.getHighWaterMark();
|
|
3067
|
+
if (finalSeq > 0) {
|
|
3068
|
+
await tx.execute(
|
|
3069
|
+
"INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
3070
|
+
[this.config.nodeId, finalSeq]
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
1868
3073
|
});
|
|
1869
3074
|
return { operations, affectedCollections };
|
|
1870
3075
|
}
|
|
@@ -1891,16 +3096,17 @@ var TransactionContext = class {
|
|
|
1891
3096
|
}
|
|
1892
3097
|
async insert(collectionName, definition, data) {
|
|
1893
3098
|
this.ensureActive();
|
|
1894
|
-
const validated = (0,
|
|
1895
|
-
const recordId = (0,
|
|
3099
|
+
const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "insert");
|
|
3100
|
+
const recordId = (0, import_core14.generateUUIDv7)();
|
|
1896
3101
|
const now = Date.now();
|
|
1897
3102
|
for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
|
|
1898
3103
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
1899
3104
|
validated[fieldName] = now;
|
|
1900
3105
|
}
|
|
1901
3106
|
}
|
|
1902
|
-
const sequenceNumber = this.config.
|
|
1903
|
-
const
|
|
3107
|
+
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3108
|
+
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3109
|
+
const operation = await (0, import_core14.createOperation)(
|
|
1904
3110
|
{
|
|
1905
3111
|
nodeId: this.config.nodeId,
|
|
1906
3112
|
type: "insert",
|
|
@@ -1909,19 +3115,22 @@ var TransactionContext = class {
|
|
|
1909
3115
|
data: { ...validated },
|
|
1910
3116
|
previousData: null,
|
|
1911
3117
|
sequenceNumber,
|
|
1912
|
-
causalDeps
|
|
3118
|
+
causalDeps,
|
|
1913
3119
|
schemaVersion: this.config.schema.version,
|
|
1914
3120
|
transactionId: this.transactionId,
|
|
1915
3121
|
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
|
|
1916
3122
|
},
|
|
1917
3123
|
this.config.clock
|
|
1918
3124
|
);
|
|
3125
|
+
this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
|
|
1919
3126
|
const serializedData = serializeRecord(validated, definition.fields);
|
|
3127
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1920
3128
|
const record = {
|
|
1921
3129
|
id: recordId,
|
|
1922
3130
|
...serializedData,
|
|
1923
|
-
_created_at:
|
|
1924
|
-
_updated_at:
|
|
3131
|
+
_created_at: operation.timestamp.wallTime,
|
|
3132
|
+
_updated_at: operation.timestamp.wallTime,
|
|
3133
|
+
_version: version
|
|
1925
3134
|
};
|
|
1926
3135
|
const insertQuery = buildInsertQuery(collectionName, record);
|
|
1927
3136
|
const opRow = serializeOperation(operation);
|
|
@@ -1934,11 +3143,7 @@ var TransactionContext = class {
|
|
|
1934
3143
|
collection: collectionName,
|
|
1935
3144
|
commands: [
|
|
1936
3145
|
{ 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
|
-
}
|
|
3146
|
+
{ sql: opInsert.sql, params: opInsert.params }
|
|
1942
3147
|
]
|
|
1943
3148
|
});
|
|
1944
3149
|
return {
|
|
@@ -1954,7 +3159,7 @@ var TransactionContext = class {
|
|
|
1954
3159
|
if (!currentRecord) {
|
|
1955
3160
|
throw new RecordNotFoundError(collectionName, id);
|
|
1956
3161
|
}
|
|
1957
|
-
const validated = (0,
|
|
3162
|
+
const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "update");
|
|
1958
3163
|
const now = Date.now();
|
|
1959
3164
|
const previousData = {};
|
|
1960
3165
|
const resolvedData = {};
|
|
@@ -1962,16 +3167,17 @@ var TransactionContext = class {
|
|
|
1962
3167
|
for (const key of Object.keys(validated)) {
|
|
1963
3168
|
const value = validated[key];
|
|
1964
3169
|
previousData[key] = currentRecord[key];
|
|
1965
|
-
if ((0,
|
|
1966
|
-
resolvedData[key] = (0,
|
|
1967
|
-
atomicOps[key] = (0,
|
|
3170
|
+
if ((0, import_core14.isAtomicOp)(value)) {
|
|
3171
|
+
resolvedData[key] = (0, import_core14.resolveAtomicOp)(currentRecord[key], value);
|
|
3172
|
+
atomicOps[key] = (0, import_core14.toAtomicOp)(value);
|
|
1968
3173
|
} else {
|
|
1969
3174
|
resolvedData[key] = value;
|
|
1970
3175
|
}
|
|
1971
3176
|
}
|
|
1972
3177
|
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
1973
|
-
const sequenceNumber = this.config.
|
|
1974
|
-
const
|
|
3178
|
+
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3179
|
+
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3180
|
+
const operation = await (0, import_core14.createOperation)(
|
|
1975
3181
|
{
|
|
1976
3182
|
nodeId: this.config.nodeId,
|
|
1977
3183
|
type: "update",
|
|
@@ -1980,7 +3186,7 @@ var TransactionContext = class {
|
|
|
1980
3186
|
data: { ...resolvedData },
|
|
1981
3187
|
previousData,
|
|
1982
3188
|
sequenceNumber,
|
|
1983
|
-
causalDeps
|
|
3189
|
+
causalDeps,
|
|
1984
3190
|
schemaVersion: this.config.schema.version,
|
|
1985
3191
|
transactionId: this.transactionId,
|
|
1986
3192
|
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {},
|
|
@@ -1988,10 +3194,13 @@ var TransactionContext = class {
|
|
|
1988
3194
|
},
|
|
1989
3195
|
this.config.clock
|
|
1990
3196
|
);
|
|
3197
|
+
this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
|
|
1991
3198
|
const serializedChanges = serializeRecord(resolvedData, definition.fields);
|
|
3199
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
1992
3200
|
const updateQuery = buildUpdateQuery(collectionName, id, {
|
|
1993
3201
|
...serializedChanges,
|
|
1994
|
-
_updated_at:
|
|
3202
|
+
_updated_at: operation.timestamp.wallTime,
|
|
3203
|
+
_version: version
|
|
1995
3204
|
});
|
|
1996
3205
|
const opRow = serializeOperation(operation);
|
|
1997
3206
|
const opInsert = buildInsertQuery(
|
|
@@ -2003,11 +3212,7 @@ var TransactionContext = class {
|
|
|
2003
3212
|
collection: collectionName,
|
|
2004
3213
|
commands: [
|
|
2005
3214
|
{ 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
|
-
}
|
|
3215
|
+
{ sql: opInsert.sql, params: opInsert.params }
|
|
2011
3216
|
]
|
|
2012
3217
|
});
|
|
2013
3218
|
return {
|
|
@@ -2023,8 +3228,9 @@ var TransactionContext = class {
|
|
|
2023
3228
|
throw new RecordNotFoundError(collectionName, id);
|
|
2024
3229
|
}
|
|
2025
3230
|
const now = Date.now();
|
|
2026
|
-
const sequenceNumber = this.config.
|
|
2027
|
-
const
|
|
3231
|
+
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3232
|
+
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3233
|
+
const operation = await (0, import_core14.createOperation)(
|
|
2028
3234
|
{
|
|
2029
3235
|
nodeId: this.config.nodeId,
|
|
2030
3236
|
type: "delete",
|
|
@@ -2033,13 +3239,14 @@ var TransactionContext = class {
|
|
|
2033
3239
|
data: null,
|
|
2034
3240
|
previousData: null,
|
|
2035
3241
|
sequenceNumber,
|
|
2036
|
-
causalDeps
|
|
3242
|
+
causalDeps,
|
|
2037
3243
|
schemaVersion: this.config.schema.version,
|
|
2038
3244
|
transactionId: this.transactionId,
|
|
2039
3245
|
...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
|
|
2040
3246
|
},
|
|
2041
3247
|
this.config.clock
|
|
2042
3248
|
);
|
|
3249
|
+
this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
|
|
2043
3250
|
const deleteQuery = buildSoftDeleteQuery(collectionName, id, now);
|
|
2044
3251
|
const opRow = serializeOperation(operation);
|
|
2045
3252
|
const opInsert = buildInsertQuery(
|
|
@@ -2051,11 +3258,7 @@ var TransactionContext = class {
|
|
|
2051
3258
|
collection: collectionName,
|
|
2052
3259
|
commands: [
|
|
2053
3260
|
{ 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
|
-
}
|
|
3261
|
+
{ sql: opInsert.sql, params: opInsert.params }
|
|
2059
3262
|
]
|
|
2060
3263
|
});
|
|
2061
3264
|
}
|
|
@@ -2106,25 +3309,86 @@ var TransactionContext = class {
|
|
|
2106
3309
|
}
|
|
2107
3310
|
};
|
|
2108
3311
|
|
|
3312
|
+
// src/transaction/transaction-sequence.ts
|
|
3313
|
+
var TransactionSequenceAllocator = class {
|
|
3314
|
+
constructor(adapter, nodeId) {
|
|
3315
|
+
this.adapter = adapter;
|
|
3316
|
+
this.nodeId = nodeId;
|
|
3317
|
+
}
|
|
3318
|
+
adapter;
|
|
3319
|
+
nodeId;
|
|
3320
|
+
loaded = false;
|
|
3321
|
+
watermark = 0;
|
|
3322
|
+
async allocate() {
|
|
3323
|
+
if (!this.loaded) {
|
|
3324
|
+
this.watermark = await readSequenceNumber(this.adapter, this.nodeId);
|
|
3325
|
+
this.loaded = true;
|
|
3326
|
+
}
|
|
3327
|
+
this.watermark++;
|
|
3328
|
+
return this.watermark;
|
|
3329
|
+
}
|
|
3330
|
+
getHighWaterMark() {
|
|
3331
|
+
return this.watermark;
|
|
3332
|
+
}
|
|
3333
|
+
};
|
|
3334
|
+
|
|
3335
|
+
// src/store/tab-node-id.ts
|
|
3336
|
+
var import_core15 = require("@korajs/core");
|
|
3337
|
+
function perTabStorageKey(dbName) {
|
|
3338
|
+
return `kora-tab-node-${dbName}`;
|
|
3339
|
+
}
|
|
3340
|
+
function loadPerTabNodeId(dbName) {
|
|
3341
|
+
if (typeof sessionStorage === "undefined") {
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
return sessionStorage.getItem(perTabStorageKey(dbName));
|
|
3345
|
+
}
|
|
3346
|
+
function savePerTabNodeId(dbName, nodeId) {
|
|
3347
|
+
if (typeof sessionStorage === "undefined") {
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
sessionStorage.setItem(perTabStorageKey(dbName), nodeId);
|
|
3351
|
+
}
|
|
3352
|
+
function resolvePerTabNodeId(dbName) {
|
|
3353
|
+
const existing = loadPerTabNodeId(dbName);
|
|
3354
|
+
if (existing) {
|
|
3355
|
+
return existing;
|
|
3356
|
+
}
|
|
3357
|
+
const nodeId = (0, import_core15.generateUUIDv7)();
|
|
3358
|
+
savePerTabNodeId(dbName, nodeId);
|
|
3359
|
+
return nodeId;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
2109
3362
|
// src/store/store.ts
|
|
2110
3363
|
var Store = class {
|
|
2111
3364
|
opened = false;
|
|
2112
3365
|
nodeId = "";
|
|
2113
3366
|
sequenceNumber = 0;
|
|
2114
|
-
versionVector = (0,
|
|
3367
|
+
versionVector = (0, import_core17.createVersionVector)();
|
|
2115
3368
|
clock = null;
|
|
2116
3369
|
collections = /* @__PURE__ */ new Map();
|
|
2117
|
-
subscriptionManager
|
|
3370
|
+
subscriptionManager;
|
|
2118
3371
|
sequenceManager = null;
|
|
2119
3372
|
schema;
|
|
2120
3373
|
adapter;
|
|
2121
3374
|
configNodeId;
|
|
3375
|
+
dbName;
|
|
3376
|
+
isolation;
|
|
2122
3377
|
emitter;
|
|
3378
|
+
localMutationHandler;
|
|
3379
|
+
relationEnforcer = null;
|
|
3380
|
+
causalTracker = null;
|
|
2123
3381
|
constructor(config) {
|
|
2124
3382
|
this.schema = config.schema;
|
|
2125
3383
|
this.adapter = config.adapter;
|
|
2126
3384
|
this.configNodeId = config.nodeId;
|
|
3385
|
+
this.dbName = config.dbName ?? "kora-db";
|
|
3386
|
+
this.isolation = config.isolation ?? "shared";
|
|
2127
3387
|
this.emitter = config.emitter ?? null;
|
|
3388
|
+
this.localMutationHandler = config.localMutationHandler ?? null;
|
|
3389
|
+
this.subscriptionManager = new SubscriptionManager({
|
|
3390
|
+
onQuerySubscribed: config.onQuerySubscribed
|
|
3391
|
+
});
|
|
2128
3392
|
}
|
|
2129
3393
|
/**
|
|
2130
3394
|
* Open the store: initialize the database, load or generate a node ID,
|
|
@@ -2134,18 +3398,18 @@ var Store = class {
|
|
|
2134
3398
|
await this.adapter.open(this.schema);
|
|
2135
3399
|
await this.runMigrationsIfNeeded();
|
|
2136
3400
|
this.nodeId = await this.loadOrGenerateNodeId();
|
|
2137
|
-
this.clock = new
|
|
3401
|
+
this.clock = new import_core17.HybridLogicalClock(this.nodeId);
|
|
3402
|
+
this.causalTracker = new import_core17.CausalTracker();
|
|
2138
3403
|
this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
|
|
2139
3404
|
this.sequenceNumber = await this.loadSequenceNumber();
|
|
2140
3405
|
this.versionVector = await this.loadVersionVector();
|
|
2141
3406
|
const hasRelations = Object.keys(this.schema.relations).length > 0;
|
|
2142
|
-
|
|
3407
|
+
this.relationEnforcer = hasRelations ? new RelationEnforcer({
|
|
2143
3408
|
schema: this.schema,
|
|
2144
3409
|
adapter: this.adapter,
|
|
2145
3410
|
clock: this.clock,
|
|
2146
|
-
nodeId: this.nodeId
|
|
2147
|
-
|
|
2148
|
-
}) : void 0;
|
|
3411
|
+
nodeId: this.nodeId
|
|
3412
|
+
}) : null;
|
|
2149
3413
|
for (const [name, definition] of Object.entries(this.schema.collections)) {
|
|
2150
3414
|
const col = new Collection(
|
|
2151
3415
|
name,
|
|
@@ -2154,14 +3418,17 @@ var Store = class {
|
|
|
2154
3418
|
this.adapter,
|
|
2155
3419
|
this.clock,
|
|
2156
3420
|
this.nodeId,
|
|
2157
|
-
() => this.
|
|
3421
|
+
() => this.allocateSequenceNumber(),
|
|
2158
3422
|
(collectionName, operation) => {
|
|
3423
|
+
this.recordOperationSequence(operation);
|
|
2159
3424
|
this.subscriptionManager.notify(collectionName, operation);
|
|
2160
3425
|
if (this.emitter) {
|
|
2161
3426
|
this.emitter.emit({ type: "operation:created", operation });
|
|
2162
3427
|
}
|
|
2163
3428
|
},
|
|
2164
|
-
relationEnforcer
|
|
3429
|
+
this.relationEnforcer,
|
|
3430
|
+
this.localMutationHandler,
|
|
3431
|
+
this.causalTracker
|
|
2165
3432
|
);
|
|
2166
3433
|
this.collections.set(name, col);
|
|
2167
3434
|
}
|
|
@@ -2227,7 +3494,7 @@ var Store = class {
|
|
|
2227
3494
|
* Checks for duplicates, applies to the data table, persists the operation,
|
|
2228
3495
|
* and updates the version vector.
|
|
2229
3496
|
*/
|
|
2230
|
-
async applyRemoteOperation(op) {
|
|
3497
|
+
async applyRemoteOperation(op, options) {
|
|
2231
3498
|
this.ensureOpen();
|
|
2232
3499
|
const collection = op.collection;
|
|
2233
3500
|
const definition = this.schema.collections[collection];
|
|
@@ -2244,27 +3511,52 @@ var Store = class {
|
|
|
2244
3511
|
if (this.clock) {
|
|
2245
3512
|
this.clock.receive(op.timestamp);
|
|
2246
3513
|
}
|
|
3514
|
+
const remoteVersion = serializeRowVersion(op.timestamp);
|
|
3515
|
+
const wallTime = op.timestamp.wallTime;
|
|
2247
3516
|
await this.adapter.transaction(async (tx) => {
|
|
2248
3517
|
if (op.type === "insert" && op.data) {
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
3518
|
+
const existing2 = await tx.query(
|
|
3519
|
+
`SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
|
|
3520
|
+
[op.recordId]
|
|
3521
|
+
);
|
|
3522
|
+
const row = existing2[0];
|
|
3523
|
+
if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
|
|
3524
|
+
} else {
|
|
3525
|
+
const serializedData = serializeRecord(op.data, definition.fields);
|
|
3526
|
+
const record = {
|
|
3527
|
+
id: op.recordId,
|
|
3528
|
+
...serializedData,
|
|
3529
|
+
_created_at: wallTime,
|
|
3530
|
+
_updated_at: wallTime,
|
|
3531
|
+
_version: remoteVersion
|
|
3532
|
+
};
|
|
3533
|
+
const insertQuery = buildInsertQuery(collection, record);
|
|
3534
|
+
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
3535
|
+
}
|
|
2259
3536
|
} else if (op.type === "update" && op.data) {
|
|
2260
3537
|
const serializedChanges = serializeRecord(op.data, definition.fields);
|
|
2261
|
-
const
|
|
3538
|
+
const updatePayload = {
|
|
2262
3539
|
...serializedChanges,
|
|
2263
|
-
_updated_at:
|
|
2264
|
-
|
|
3540
|
+
_updated_at: wallTime,
|
|
3541
|
+
_version: remoteVersion
|
|
3542
|
+
};
|
|
3543
|
+
if (options?.reactivateIfDeleted) {
|
|
3544
|
+
updatePayload._deleted = 0;
|
|
3545
|
+
}
|
|
3546
|
+
const updateQuery = buildLwwUpdateQuery(
|
|
3547
|
+
collection,
|
|
3548
|
+
op.recordId,
|
|
3549
|
+
updatePayload,
|
|
3550
|
+
remoteVersion
|
|
3551
|
+
);
|
|
2265
3552
|
await tx.execute(updateQuery.sql, updateQuery.params);
|
|
2266
3553
|
} else if (op.type === "delete") {
|
|
2267
|
-
const deleteQuery =
|
|
3554
|
+
const deleteQuery = buildLwwSoftDeleteQuery(
|
|
3555
|
+
collection,
|
|
3556
|
+
op.recordId,
|
|
3557
|
+
wallTime,
|
|
3558
|
+
remoteVersion
|
|
3559
|
+
);
|
|
2268
3560
|
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
2269
3561
|
}
|
|
2270
3562
|
const opRow = serializeOperation(op);
|
|
@@ -2289,11 +3581,11 @@ var Store = class {
|
|
|
2289
3581
|
* Get operations from a node within a sequence number range.
|
|
2290
3582
|
* Implements the OperationLog interface for computeDelta.
|
|
2291
3583
|
*/
|
|
2292
|
-
getRange(nodeId, fromSeq, toSeq) {
|
|
2293
|
-
return
|
|
3584
|
+
async getRange(nodeId, fromSeq, toSeq) {
|
|
3585
|
+
return this.getOperationRange(nodeId, fromSeq, toSeq);
|
|
2294
3586
|
}
|
|
2295
3587
|
/**
|
|
2296
|
-
*
|
|
3588
|
+
* Get operations from a node within a sequence number range.
|
|
2297
3589
|
*/
|
|
2298
3590
|
async getOperationRange(nodeId, fromSeq, toSeq) {
|
|
2299
3591
|
this.ensureOpen();
|
|
@@ -2310,12 +3602,233 @@ var Store = class {
|
|
|
2310
3602
|
allOps.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
2311
3603
|
return allOps;
|
|
2312
3604
|
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Load every operation from the local append-only log across all collections.
|
|
3607
|
+
* Used by sync delta computation, backup export, and time-travel replay.
|
|
3608
|
+
*/
|
|
3609
|
+
async getAllOperations() {
|
|
3610
|
+
this.ensureOpen();
|
|
3611
|
+
const allOps = [];
|
|
3612
|
+
for (const collectionName of Object.keys(this.schema.collections)) {
|
|
3613
|
+
const rows = await this.adapter.query(
|
|
3614
|
+
`SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
|
|
3615
|
+
);
|
|
3616
|
+
for (const row of rows) {
|
|
3617
|
+
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return allOps;
|
|
3621
|
+
}
|
|
3622
|
+
/**
|
|
3623
|
+
* Rebuild an in-memory snapshot of materialized state at a causal cut in the op log.
|
|
3624
|
+
* Does not mutate the live store — intended for DevTools time-travel inspection.
|
|
3625
|
+
*
|
|
3626
|
+
* @param operationId - Content-addressed id of the operation to replay through (inclusive)
|
|
3627
|
+
* @throws {OperationError} When the operation id is not present in the local log
|
|
3628
|
+
*/
|
|
3629
|
+
async replayTo(operationId) {
|
|
3630
|
+
this.ensureOpen();
|
|
3631
|
+
const start = Date.now();
|
|
3632
|
+
const allOps = await this.getAllOperations();
|
|
3633
|
+
const snapshot = buildReplaySnapshot(this.schema, allOps, operationId);
|
|
3634
|
+
if (this.emitter) {
|
|
3635
|
+
this.emitter.emit({
|
|
3636
|
+
type: "replay:completed",
|
|
3637
|
+
targetOperationId: operationId,
|
|
3638
|
+
operationsApplied: snapshot.operationsApplied.length,
|
|
3639
|
+
duration: Date.now() - start
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
return snapshot;
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Persist a merge trace to the durable audit log.
|
|
3646
|
+
*/
|
|
3647
|
+
async appendAuditTrace(trace) {
|
|
3648
|
+
this.ensureOpen();
|
|
3649
|
+
const { appendAuditTrace: append } = await Promise.resolve().then(() => (init_audit_trace_store(), audit_trace_store_exports));
|
|
3650
|
+
await append(this.adapter, trace);
|
|
3651
|
+
}
|
|
3652
|
+
/**
|
|
3653
|
+
* Read persisted audit traces with optional filters.
|
|
3654
|
+
*/
|
|
3655
|
+
async getAuditTraces(query) {
|
|
3656
|
+
this.ensureOpen();
|
|
3657
|
+
const { readAuditTraces: readAuditTraces2 } = await Promise.resolve().then(() => (init_audit_trace_store(), audit_trace_store_exports));
|
|
3658
|
+
return readAuditTraces2(this.adapter, query);
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* Export operations and merge traces as a portable audit bundle.
|
|
3662
|
+
*/
|
|
3663
|
+
async exportAudit(options) {
|
|
3664
|
+
this.ensureOpen();
|
|
3665
|
+
const { exportAudit: doExport } = await Promise.resolve().then(() => (init_export_audit(), export_audit_exports));
|
|
3666
|
+
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
3667
|
+
}
|
|
2313
3668
|
/**
|
|
2314
3669
|
* Get the schema definition.
|
|
2315
3670
|
*/
|
|
2316
3671
|
getSchema() {
|
|
2317
3672
|
return this.schema;
|
|
2318
3673
|
}
|
|
3674
|
+
/**
|
|
3675
|
+
* Route local CRUD through the unified apply pipeline (korajs ApplyPipeline).
|
|
3676
|
+
*/
|
|
3677
|
+
setLocalMutationHandler(handler) {
|
|
3678
|
+
this.localMutationHandler = handler;
|
|
3679
|
+
for (const col of this.collections.values()) {
|
|
3680
|
+
col.setMutationHandler(handler);
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Build mutation context for a collection (used by ApplyPipeline side effects).
|
|
3685
|
+
*/
|
|
3686
|
+
createMutationContext(collection, options) {
|
|
3687
|
+
this.ensureOpen();
|
|
3688
|
+
const definition = this.schema.collections[collection];
|
|
3689
|
+
if (!definition || !this.clock) {
|
|
3690
|
+
throw new StoreNotOpenError();
|
|
3691
|
+
}
|
|
3692
|
+
return {
|
|
3693
|
+
collection,
|
|
3694
|
+
definition,
|
|
3695
|
+
schema: this.schema,
|
|
3696
|
+
adapter: this.adapter,
|
|
3697
|
+
clock: this.clock,
|
|
3698
|
+
nodeId: this.nodeId,
|
|
3699
|
+
allocateSequenceNumber: () => this.allocateSequenceNumber(),
|
|
3700
|
+
onMutation: (collectionName, operation) => {
|
|
3701
|
+
this.recordOperationSequence(operation);
|
|
3702
|
+
this.subscriptionManager.notify(collectionName, operation);
|
|
3703
|
+
if (this.emitter) {
|
|
3704
|
+
this.emitter.emit({ type: "operation:created", operation });
|
|
3705
|
+
}
|
|
3706
|
+
},
|
|
3707
|
+
relationEnforcer: this.relationEnforcer,
|
|
3708
|
+
causalTracker: this.causalTracker,
|
|
3709
|
+
inTransaction: options?.inTransaction ?? false,
|
|
3710
|
+
extraCausalDeps: options?.extraCausalDeps
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
/**
|
|
3714
|
+
* Load a materialized row by ID, including soft-deleted tombstones.
|
|
3715
|
+
*/
|
|
3716
|
+
async findMaterializedRow(collection, recordId) {
|
|
3717
|
+
this.ensureOpen();
|
|
3718
|
+
const definition = this.schema.collections[collection];
|
|
3719
|
+
if (!definition) {
|
|
3720
|
+
return null;
|
|
3721
|
+
}
|
|
3722
|
+
const rows = await this.adapter.query(
|
|
3723
|
+
`SELECT * FROM ${collection} WHERE id = ?`,
|
|
3724
|
+
[recordId]
|
|
3725
|
+
);
|
|
3726
|
+
const row = rows[0];
|
|
3727
|
+
if (!row) {
|
|
3728
|
+
return null;
|
|
3729
|
+
}
|
|
3730
|
+
return {
|
|
3731
|
+
record: deserializeRecord(row, definition.fields),
|
|
3732
|
+
deleted: row._deleted === 1
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Latest operation from this device for a record (used for delete-vs-update merge on sync).
|
|
3737
|
+
*/
|
|
3738
|
+
/**
|
|
3739
|
+
* Load the last server version vector acknowledged by this client (persisted in `_kora_meta`).
|
|
3740
|
+
*/
|
|
3741
|
+
async loadLastAckedServerVector() {
|
|
3742
|
+
this.ensureOpen();
|
|
3743
|
+
return loadLastAckedServerVector(this.adapter);
|
|
3744
|
+
}
|
|
3745
|
+
/**
|
|
3746
|
+
* Persist the last server version vector this client believes the server has applied.
|
|
3747
|
+
*/
|
|
3748
|
+
async saveLastAckedServerVector(vector) {
|
|
3749
|
+
this.ensureOpen();
|
|
3750
|
+
await saveLastAckedServerVector(this.adapter, vector);
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Load persisted delta cursor for resuming paginated initial sync.
|
|
3754
|
+
*/
|
|
3755
|
+
async loadDeltaCursor() {
|
|
3756
|
+
this.ensureOpen();
|
|
3757
|
+
return loadDeltaCursor(this.adapter);
|
|
3758
|
+
}
|
|
3759
|
+
/**
|
|
3760
|
+
* Persist or clear the delta cursor for paginated initial sync resume.
|
|
3761
|
+
*/
|
|
3762
|
+
async saveDeltaCursor(cursor) {
|
|
3763
|
+
this.ensureOpen();
|
|
3764
|
+
await saveDeltaCursor(this.adapter, cursor);
|
|
3765
|
+
}
|
|
3766
|
+
/**
|
|
3767
|
+
* Local operations not yet reflected on the server version vector.
|
|
3768
|
+
*/
|
|
3769
|
+
async getUnsyncedOperations(serverVector) {
|
|
3770
|
+
this.ensureOpen();
|
|
3771
|
+
return collectOperationsAheadOfServer(
|
|
3772
|
+
this.getVersionVector(),
|
|
3773
|
+
serverVector,
|
|
3774
|
+
(nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
|
|
3775
|
+
);
|
|
3776
|
+
}
|
|
3777
|
+
/**
|
|
3778
|
+
* Count of local operations ahead of the server version vector.
|
|
3779
|
+
*/
|
|
3780
|
+
async countUnsyncedOperations(serverVector) {
|
|
3781
|
+
const ops = await this.getUnsyncedOperations(serverVector);
|
|
3782
|
+
return ops.length;
|
|
3783
|
+
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Compact the local operation log using materialized rows as the baseline.
|
|
3786
|
+
* Only removes ops the server has acknowledged (per {@link CompactionStrategy}).
|
|
3787
|
+
*/
|
|
3788
|
+
async compact(strategy) {
|
|
3789
|
+
this.ensureOpen();
|
|
3790
|
+
if (strategy.mode === "never") {
|
|
3791
|
+
return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core17.createVersionVector)());
|
|
3792
|
+
}
|
|
3793
|
+
const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
|
|
3794
|
+
return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Merge session remote vector with persisted last-acked vector (max per node).
|
|
3798
|
+
*/
|
|
3799
|
+
mergeServerVectors(sessionVector, persistedVector) {
|
|
3800
|
+
return mergeVersionVectors(persistedVector, sessionVector);
|
|
3801
|
+
}
|
|
3802
|
+
async getLatestLocalOperationForRecord(collection, recordId) {
|
|
3803
|
+
this.ensureOpen();
|
|
3804
|
+
const rows = await this.adapter.query(
|
|
3805
|
+
`SELECT * FROM _kora_ops_${collection} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
|
|
3806
|
+
[this.nodeId, recordId]
|
|
3807
|
+
);
|
|
3808
|
+
const row = rows[0];
|
|
3809
|
+
if (!row) {
|
|
3810
|
+
return null;
|
|
3811
|
+
}
|
|
3812
|
+
return deserializeOperationWithCollection(row, collection);
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Latest operation for a record from any node (for 3-way merge when local op log is empty).
|
|
3816
|
+
*/
|
|
3817
|
+
async getLatestOperationForRecord(collection, recordId) {
|
|
3818
|
+
this.ensureOpen();
|
|
3819
|
+
const rows = await this.adapter.query(
|
|
3820
|
+
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ?`,
|
|
3821
|
+
[recordId]
|
|
3822
|
+
);
|
|
3823
|
+
let latest = null;
|
|
3824
|
+
for (const row of rows) {
|
|
3825
|
+
const op = deserializeOperationWithCollection(row, collection);
|
|
3826
|
+
if (!latest || import_core17.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
|
|
3827
|
+
latest = op;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
return latest;
|
|
3831
|
+
}
|
|
2319
3832
|
/** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
|
|
2320
3833
|
getSubscriptionManager() {
|
|
2321
3834
|
return this.subscriptionManager;
|
|
@@ -2348,7 +3861,10 @@ var Store = class {
|
|
|
2348
3861
|
adapter: this.adapter,
|
|
2349
3862
|
clock: this.clock,
|
|
2350
3863
|
nodeId: this.nodeId,
|
|
2351
|
-
|
|
3864
|
+
sequenceAllocator: new TransactionSequenceAllocator(this.adapter, this.nodeId),
|
|
3865
|
+
relationEnforcer: this.relationEnforcer,
|
|
3866
|
+
causalTracker: this.causalTracker,
|
|
3867
|
+
localMutationHandler: this.localMutationHandler
|
|
2352
3868
|
});
|
|
2353
3869
|
}
|
|
2354
3870
|
/**
|
|
@@ -2363,10 +3879,13 @@ var Store = class {
|
|
|
2363
3879
|
*/
|
|
2364
3880
|
async transaction(fn) {
|
|
2365
3881
|
const tx = this.createTransaction();
|
|
3882
|
+
this.causalTracker?.beginTransaction();
|
|
2366
3883
|
try {
|
|
2367
3884
|
await fn(tx);
|
|
2368
|
-
const { operations
|
|
3885
|
+
const { operations } = await tx.commit();
|
|
3886
|
+
this.causalTracker?.clearTransaction();
|
|
2369
3887
|
for (const op of operations) {
|
|
3888
|
+
this.recordOperationSequence(op);
|
|
2370
3889
|
this.subscriptionManager.notify(op.collection, op);
|
|
2371
3890
|
if (this.emitter) {
|
|
2372
3891
|
this.emitter.emit({ type: "operation:created", operation: op });
|
|
@@ -2375,13 +3894,57 @@ var Store = class {
|
|
|
2375
3894
|
return operations;
|
|
2376
3895
|
} catch (error) {
|
|
2377
3896
|
tx.rollback();
|
|
3897
|
+
this.causalTracker?.clearTransaction();
|
|
2378
3898
|
throw error;
|
|
2379
3899
|
}
|
|
2380
3900
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
3901
|
+
/**
|
|
3902
|
+
* Export all data as a portable backup binary.
|
|
3903
|
+
* Includes operations, version vector, metadata, and optionally materialized records.
|
|
3904
|
+
*
|
|
3905
|
+
* @param options - Backup options (includeRecords, collections, onProgress)
|
|
3906
|
+
* @returns Backup as a Uint8Array
|
|
3907
|
+
*/
|
|
3908
|
+
async exportBackup(options) {
|
|
3909
|
+
this.ensureOpen();
|
|
3910
|
+
const { exportBackup: doExport } = await Promise.resolve().then(() => (init_backup(), backup_exports));
|
|
3911
|
+
return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
|
|
3912
|
+
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Restore data from a backup binary.
|
|
3915
|
+
*
|
|
3916
|
+
* @param data - The backup data
|
|
3917
|
+
* @param options - Restore options (merge, collections, onProgress)
|
|
3918
|
+
* @returns Result of the restore operation
|
|
3919
|
+
*/
|
|
3920
|
+
async importBackup(data, options) {
|
|
3921
|
+
this.ensureOpen();
|
|
3922
|
+
const { restoreBackup: doRestore } = await Promise.resolve().then(() => (init_backup(), backup_exports));
|
|
3923
|
+
return doRestore(this.adapter, this.schema, data, options);
|
|
3924
|
+
}
|
|
3925
|
+
/**
|
|
3926
|
+
* Read backup manifest without loading the entire backup.
|
|
3927
|
+
*
|
|
3928
|
+
* @param data - The raw backup data
|
|
3929
|
+
* @returns The backup manifest
|
|
3930
|
+
*/
|
|
3931
|
+
static readBackupManifest(data) {
|
|
3932
|
+
return readBackupManifest(data);
|
|
3933
|
+
}
|
|
3934
|
+
recordOperationSequence(operation) {
|
|
3935
|
+
const prev = this.versionVector.get(operation.nodeId) ?? 0;
|
|
3936
|
+
if (operation.sequenceNumber > prev) {
|
|
3937
|
+
this.versionVector.set(operation.nodeId, operation.sequenceNumber);
|
|
3938
|
+
}
|
|
3939
|
+
if (operation.nodeId === this.nodeId) {
|
|
3940
|
+
this.sequenceNumber = Math.max(this.sequenceNumber, operation.sequenceNumber);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
async allocateSequenceNumber() {
|
|
3944
|
+
const seq = await allocateNextSequenceNumber(this.adapter, this.nodeId);
|
|
3945
|
+
this.sequenceNumber = seq;
|
|
3946
|
+
this.versionVector.set(this.nodeId, seq);
|
|
3947
|
+
return seq;
|
|
2385
3948
|
}
|
|
2386
3949
|
/**
|
|
2387
3950
|
* Check the stored schema version and run any pending migrations.
|
|
@@ -2403,7 +3966,7 @@ var Store = class {
|
|
|
2403
3966
|
for (let v = storedVersion + 1; v <= targetVersion; v++) {
|
|
2404
3967
|
const migration = migrations[v];
|
|
2405
3968
|
if (!migration) continue;
|
|
2406
|
-
const sqlStatements = (0,
|
|
3969
|
+
const sqlStatements = (0, import_core17.migrationStepsToSQL)(migration.steps);
|
|
2407
3970
|
for (const sql of sqlStatements) {
|
|
2408
3971
|
try {
|
|
2409
3972
|
await this.adapter.execute(sql);
|
|
@@ -2464,19 +4027,24 @@ var Store = class {
|
|
|
2464
4027
|
}
|
|
2465
4028
|
async loadOrGenerateNodeId() {
|
|
2466
4029
|
if (this.configNodeId) {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
4030
|
+
if (this.isolation !== "per-tab") {
|
|
4031
|
+
await this.adapter.execute(
|
|
4032
|
+
"INSERT OR REPLACE INTO _kora_meta (key, value) VALUES ('node_id', ?)",
|
|
4033
|
+
[this.configNodeId]
|
|
4034
|
+
);
|
|
4035
|
+
}
|
|
2471
4036
|
return this.configNodeId;
|
|
2472
4037
|
}
|
|
4038
|
+
if (this.isolation === "per-tab") {
|
|
4039
|
+
return resolvePerTabNodeId(this.dbName);
|
|
4040
|
+
}
|
|
2473
4041
|
const rows = await this.adapter.query(
|
|
2474
4042
|
"SELECT value FROM _kora_meta WHERE key = 'node_id'"
|
|
2475
4043
|
);
|
|
2476
4044
|
if (rows[0]) {
|
|
2477
4045
|
return rows[0].value;
|
|
2478
4046
|
}
|
|
2479
|
-
const newNodeId = (0,
|
|
4047
|
+
const newNodeId = (0, import_core17.generateUUIDv7)();
|
|
2480
4048
|
await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
|
|
2481
4049
|
newNodeId
|
|
2482
4050
|
]);
|
|
@@ -2493,7 +4061,7 @@ var Store = class {
|
|
|
2493
4061
|
const rows = await this.adapter.query(
|
|
2494
4062
|
"SELECT node_id, sequence_number FROM _kora_version_vector"
|
|
2495
4063
|
);
|
|
2496
|
-
const vector = (0,
|
|
4064
|
+
const vector = (0, import_core17.createVersionVector)();
|
|
2497
4065
|
for (const row of rows) {
|
|
2498
4066
|
vector.set(row.node_id, row.sequence_number);
|
|
2499
4067
|
}
|
|
@@ -2505,11 +4073,24 @@ var Store = class {
|
|
|
2505
4073
|
}
|
|
2506
4074
|
}
|
|
2507
4075
|
};
|
|
4076
|
+
|
|
4077
|
+
// src/index.ts
|
|
4078
|
+
init_richtext_serializer();
|
|
4079
|
+
|
|
4080
|
+
// src/backup/index.ts
|
|
4081
|
+
init_backup();
|
|
4082
|
+
|
|
4083
|
+
// src/audit/index.ts
|
|
4084
|
+
init_audit_trace_store();
|
|
4085
|
+
init_audit_json();
|
|
4086
|
+
init_export_audit();
|
|
2508
4087
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2509
4088
|
0 && (module.exports = {
|
|
2510
4089
|
AdapterError,
|
|
4090
|
+
COMPACTION_BASELINE_META_KEY,
|
|
2511
4091
|
Collection,
|
|
2512
4092
|
InvalidStateTransitionError,
|
|
4093
|
+
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
2513
4094
|
PersistenceError,
|
|
2514
4095
|
QueryBuilder,
|
|
2515
4096
|
QueryError,
|
|
@@ -2522,12 +4103,27 @@ var Store = class {
|
|
|
2522
4103
|
TransactionContext,
|
|
2523
4104
|
WorkerInitError,
|
|
2524
4105
|
WorkerTimeoutError,
|
|
4106
|
+
collectOperationsAheadOfServer,
|
|
4107
|
+
compactOperationLog,
|
|
4108
|
+
computeAckCompactionWatermark,
|
|
4109
|
+
decodeAuditExport,
|
|
2525
4110
|
decodeRichtext,
|
|
4111
|
+
deserializeVersionVectorFromMeta,
|
|
2526
4112
|
encodeRichtext,
|
|
4113
|
+
exportBackup,
|
|
4114
|
+
mergeVersionVectors,
|
|
4115
|
+
persistedAuditTraceFromEvent,
|
|
2527
4116
|
pluralize,
|
|
4117
|
+
readAuditExportManifest,
|
|
4118
|
+
readBackupManifest,
|
|
4119
|
+
restoreBackup,
|
|
4120
|
+
richtextStatesEqual,
|
|
2528
4121
|
richtextToPlainText,
|
|
4122
|
+
serializeVersionVectorToMeta,
|
|
2529
4123
|
singularize,
|
|
2530
4124
|
validateStateTransition,
|
|
2531
|
-
validateUpdateStateMachine
|
|
4125
|
+
validateUpdateStateMachine,
|
|
4126
|
+
verifyAuditExportChecksum,
|
|
4127
|
+
verifyBackupChecksum
|
|
2532
4128
|
});
|
|
2533
4129
|
//# sourceMappingURL=index.cjs.map
|