@korajs/store 1.0.0-beta.11 → 1.0.0-beta.13
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/README.md +29 -37
- package/dist/adapters/better-sqlite3.cjs +90 -4
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +21 -12
- package/dist/adapters/better-sqlite3.d.ts +21 -12
- package/dist/adapters/better-sqlite3.js +69 -5
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +1316 -610
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +29 -3
- package/dist/adapters/indexeddb.d.ts +29 -3
- package/dist/adapters/indexeddb.js +247 -221
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +803 -62
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +480 -62
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +1032 -344
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +239 -18
- package/dist/adapters/sqlite-wasm.d.ts +239 -18
- package/dist/adapters/sqlite-wasm.js +111 -5
- package/dist/adapters/sqlite-wasm.js.map +1 -1
- package/dist/backup-4NUPDEMB.js +25 -0
- package/dist/chunk-32EDKWMI.js +876 -0
- package/dist/chunk-32EDKWMI.js.map +1 -0
- package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
- package/dist/chunk-A6GFH5IG.js +36 -0
- package/dist/chunk-A6GFH5IG.js.map +1 -0
- package/dist/chunk-DUKTWVZW.js +175 -0
- package/dist/chunk-DUKTWVZW.js.map +1 -0
- package/dist/chunk-HZTDBLDB.js +607 -0
- package/dist/chunk-HZTDBLDB.js.map +1 -0
- package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
- package/dist/chunk-J5AGXF3D.js.map +1 -0
- package/dist/chunk-MCQKEHOD.js +112 -0
- package/dist/chunk-MCQKEHOD.js.map +1 -0
- package/dist/chunk-P4V375FY.js +810 -0
- package/dist/chunk-P4V375FY.js.map +1 -0
- package/dist/chunk-QDUSJMFV.js +1095 -0
- package/dist/chunk-QDUSJMFV.js.map +1 -0
- package/dist/chunk-SJP2HTYZ.js +1003 -0
- package/dist/chunk-SJP2HTYZ.js.map +1 -0
- package/dist/chunk-W5F2LWG6.js +591 -0
- package/dist/chunk-W5F2LWG6.js.map +1 -0
- package/dist/chunk-Y5LRH7KI.js +5341 -0
- package/dist/chunk-Y5LRH7KI.js.map +1 -0
- package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
- package/dist/index.cjs +9354 -4157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1064 -108
- package/dist/index.d.ts +1064 -108
- package/dist/index.js +249 -3092
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +580 -264
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +15 -85
- package/dist/internal.d.ts +15 -85
- package/dist/internal.js +10 -8
- package/dist/restore-FR7XIKT2.js +299 -0
- package/dist/restore-FR7XIKT2.js.map +1 -0
- package/dist/sql-builder-C4u55jKW.d.cts +608 -0
- package/dist/sql-builder-CGckdBDL.d.ts +608 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
- package/dist/sqlite-wasm-channel-MLHLKEED.js +12 -0
- package/dist/store-IMWKZLWB.js +16 -0
- package/dist/store-IMWKZLWB.js.map +1 -0
- package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
- package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
- package/package.json +3 -2
- package/dist/backup-5S7K2Z35.js +0 -14
- package/dist/chunk-GSJ7EEUC.js.map +0 -1
- package/dist/chunk-HS6DBMST.js +0 -383
- package/dist/chunk-HS6DBMST.js.map +0 -1
- package/dist/chunk-RJIEKEN4.js +0 -751
- package/dist/chunk-RJIEKEN4.js.map +0 -1
- package/dist/chunk-TUR7RGEP.js +0 -84
- package/dist/chunk-TUR7RGEP.js.map +0 -1
- package/dist/chunk-VDBJQ75K.js +0 -78
- package/dist/chunk-VDBJQ75K.js.map +0 -1
- package/dist/chunk-YT7MRFCP.js +0 -687
- package/dist/chunk-YT7MRFCP.js.map +0 -1
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
- package/dist/types-BOO9EPt1.d.ts +0 -170
- package/dist/types-C_GDavG-.d.cts +0 -170
- /package/dist/{backup-5S7K2Z35.js.map → backup-4NUPDEMB.js.map} +0 -0
- /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
- /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-MLHLKEED.js.map} +0 -0
package/dist/internal.cjs
CHANGED
|
@@ -44,8 +44,8 @@ __export(internal_exports, {
|
|
|
44
44
|
executeDelete: () => executeDelete,
|
|
45
45
|
executeInsert: () => executeInsert,
|
|
46
46
|
executeUpdate: () => executeUpdate,
|
|
47
|
+
hasUnsyncedOwnOperations: () => hasUnsyncedOwnOperations,
|
|
47
48
|
isIncomingNewerThanRow: () => isIncomingNewerThanRow,
|
|
48
|
-
resolveCausalDeps: () => resolveCausalDeps,
|
|
49
49
|
rowVersionFromRecord: () => rowVersionFromRecord,
|
|
50
50
|
serializeOperation: () => serializeOperation,
|
|
51
51
|
serializeRecord: () => serializeRecord,
|
|
@@ -149,8 +149,10 @@ function deserializeRecord(row, fields) {
|
|
|
149
149
|
var ATOMIC_OPS_KEY = "__kora_atomic_ops__";
|
|
150
150
|
var TX_ID_KEY = "__kora_tx_id__";
|
|
151
151
|
var MUTATION_NAME_KEY = "__kora_mutation__";
|
|
152
|
+
var FIELD_VERSIONS_KEY = "__kora_field_versions__";
|
|
153
|
+
var HASH_VERSION_KEY = "__kora_hash_version__";
|
|
152
154
|
function serializeOperation(op) {
|
|
153
|
-
const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
|
|
155
|
+
const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0 || op.hashVersion !== void 0;
|
|
154
156
|
let dataPayload = null;
|
|
155
157
|
if (op.data) {
|
|
156
158
|
dataPayload = { ...op.data };
|
|
@@ -163,6 +165,12 @@ function serializeOperation(op) {
|
|
|
163
165
|
if (op.mutationName !== void 0) {
|
|
164
166
|
dataPayload[MUTATION_NAME_KEY] = op.mutationName;
|
|
165
167
|
}
|
|
168
|
+
if (op.fieldVersions !== void 0) {
|
|
169
|
+
dataPayload[FIELD_VERSIONS_KEY] = op.fieldVersions;
|
|
170
|
+
}
|
|
171
|
+
if (op.hashVersion !== void 0) {
|
|
172
|
+
dataPayload[HASH_VERSION_KEY] = op.hashVersion;
|
|
173
|
+
}
|
|
166
174
|
} else if (hasMetadata) {
|
|
167
175
|
dataPayload = {};
|
|
168
176
|
if (op.transactionId !== void 0) {
|
|
@@ -171,6 +179,9 @@ function serializeOperation(op) {
|
|
|
171
179
|
if (op.mutationName !== void 0) {
|
|
172
180
|
dataPayload[MUTATION_NAME_KEY] = op.mutationName;
|
|
173
181
|
}
|
|
182
|
+
if (op.hashVersion !== void 0) {
|
|
183
|
+
dataPayload[HASH_VERSION_KEY] = op.hashVersion;
|
|
184
|
+
}
|
|
174
185
|
}
|
|
175
186
|
return {
|
|
176
187
|
id: op.id,
|
|
@@ -190,6 +201,8 @@ function deserializeOperation(row) {
|
|
|
190
201
|
let atomicOps;
|
|
191
202
|
let transactionId;
|
|
192
203
|
let mutationName;
|
|
204
|
+
let fieldVersions;
|
|
205
|
+
let hashVersion;
|
|
193
206
|
if (row.data) {
|
|
194
207
|
const parsed = JSON.parse(row.data);
|
|
195
208
|
if (ATOMIC_OPS_KEY in parsed) {
|
|
@@ -201,8 +214,21 @@ function deserializeOperation(row) {
|
|
|
201
214
|
if (MUTATION_NAME_KEY in parsed) {
|
|
202
215
|
mutationName = parsed[MUTATION_NAME_KEY];
|
|
203
216
|
}
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
if (FIELD_VERSIONS_KEY in parsed) {
|
|
218
|
+
fieldVersions = parsed[FIELD_VERSIONS_KEY];
|
|
219
|
+
}
|
|
220
|
+
if (HASH_VERSION_KEY in parsed) {
|
|
221
|
+
hashVersion = parsed[HASH_VERSION_KEY];
|
|
222
|
+
}
|
|
223
|
+
const {
|
|
224
|
+
[ATOMIC_OPS_KEY]: _a,
|
|
225
|
+
[TX_ID_KEY]: _t,
|
|
226
|
+
[MUTATION_NAME_KEY]: _m,
|
|
227
|
+
[FIELD_VERSIONS_KEY]: _f,
|
|
228
|
+
[HASH_VERSION_KEY]: _h,
|
|
229
|
+
...rest
|
|
230
|
+
} = parsed;
|
|
231
|
+
data = row.type !== "delete" || Object.keys(rest).length > 0 ? rest : null;
|
|
206
232
|
}
|
|
207
233
|
return {
|
|
208
234
|
id: row.id,
|
|
@@ -219,13 +245,26 @@ function deserializeOperation(row) {
|
|
|
219
245
|
schemaVersion: row.schema_version,
|
|
220
246
|
...atomicOps !== void 0 ? { atomicOps } : {},
|
|
221
247
|
...transactionId !== void 0 ? { transactionId } : {},
|
|
222
|
-
...mutationName !== void 0 ? { mutationName } : {}
|
|
248
|
+
...mutationName !== void 0 ? { mutationName } : {},
|
|
249
|
+
...fieldVersions !== void 0 ? { fieldVersions } : {},
|
|
250
|
+
...hashVersion !== void 0 ? { hashVersion } : {}
|
|
223
251
|
};
|
|
224
252
|
}
|
|
225
253
|
function deserializeOperationWithCollection(row, collection) {
|
|
226
254
|
const op = deserializeOperation(row);
|
|
227
255
|
return { ...op, collection };
|
|
228
256
|
}
|
|
257
|
+
function toRichtextReadShape(data, fields) {
|
|
258
|
+
let out = data;
|
|
259
|
+
for (const [name, descriptor] of Object.entries(fields)) {
|
|
260
|
+
if (descriptor.kind !== "richtext" || !(name in data)) continue;
|
|
261
|
+
const value = data[name];
|
|
262
|
+
if (value === null || value === void 0 || value instanceof Uint8Array) continue;
|
|
263
|
+
if (out === data) out = { ...data };
|
|
264
|
+
out[name] = encodeRichtext(value);
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
229
268
|
function serializeValue(value, descriptor) {
|
|
230
269
|
if (value === null || value === void 0) {
|
|
231
270
|
return null;
|
|
@@ -241,9 +280,16 @@ function serializeValue(value, descriptor) {
|
|
|
241
280
|
case "richtext":
|
|
242
281
|
return encodeRichtext(value);
|
|
243
282
|
default:
|
|
244
|
-
return value;
|
|
283
|
+
return typeof value === "string" && isRawTextKind(descriptor.kind) ? (0, import_core2.encodeStoredText)(value) : value;
|
|
245
284
|
}
|
|
246
285
|
}
|
|
286
|
+
var RAW_TEXT_KINDS = /* @__PURE__ */ new Set(["string", "secret"]);
|
|
287
|
+
function isRawTextKind(kind) {
|
|
288
|
+
return RAW_TEXT_KINDS.has(kind);
|
|
289
|
+
}
|
|
290
|
+
function encodeStoredFilterValue(value, descriptor) {
|
|
291
|
+
return typeof value === "string" && descriptor !== void 0 && isRawTextKind(descriptor.kind) ? (0, import_core2.encodeStoredText)(value) : value;
|
|
292
|
+
}
|
|
247
293
|
function deserializeValue(value, descriptor) {
|
|
248
294
|
switch (descriptor.kind) {
|
|
249
295
|
case "boolean":
|
|
@@ -263,7 +309,7 @@ function deserializeValue(value, descriptor) {
|
|
|
263
309
|
case "richtext":
|
|
264
310
|
return decodeRichtext(value);
|
|
265
311
|
default:
|
|
266
|
-
return value;
|
|
312
|
+
return typeof value === "string" && isRawTextKind(descriptor.kind) ? (0, import_core2.decodeStoredText)(value) : value;
|
|
267
313
|
}
|
|
268
314
|
}
|
|
269
315
|
|
|
@@ -325,15 +371,20 @@ function buildSelectQuery(descriptor, fields) {
|
|
|
325
371
|
if (descriptor.orderBy.length > 0) {
|
|
326
372
|
const orderParts = descriptor.orderBy.map((o) => {
|
|
327
373
|
validateFieldName(o.field, fields);
|
|
328
|
-
return `${(0, import_core5.quoteIdent)(o.field)} ${o.direction.
|
|
374
|
+
return `${(0, import_core5.quoteIdent)(resolveColumnName(o.field, fields))} ${sqlSortDirection(o.direction, o.field)}`;
|
|
329
375
|
});
|
|
330
376
|
parts.push(`ORDER BY ${orderParts.join(", ")}`);
|
|
331
377
|
}
|
|
332
|
-
|
|
333
|
-
|
|
378
|
+
const limit = descriptor.limit !== void 0 ? assertQueryCount(descriptor.limit, "limit") : null;
|
|
379
|
+
const offset = descriptor.offset !== void 0 ? assertQueryCount(descriptor.offset, "offset") : null;
|
|
380
|
+
if (limit !== null) {
|
|
381
|
+
parts.push("LIMIT ?");
|
|
382
|
+
params.push(limit);
|
|
334
383
|
}
|
|
335
|
-
if (
|
|
336
|
-
parts.push(
|
|
384
|
+
if (offset !== null) {
|
|
385
|
+
if (limit === null) parts.push("LIMIT -1");
|
|
386
|
+
parts.push("OFFSET ?");
|
|
387
|
+
params.push(offset);
|
|
337
388
|
}
|
|
338
389
|
return { sql: parts.join(" "), params };
|
|
339
390
|
}
|
|
@@ -401,6 +452,7 @@ function buildWhereClauseParts(where, fields, params) {
|
|
|
401
452
|
for (const [fieldName, value] of Object.entries(where)) {
|
|
402
453
|
validateFieldName(fieldName, fields);
|
|
403
454
|
const descriptor = fields[fieldName];
|
|
455
|
+
const column = resolveColumnName(fieldName, fields);
|
|
404
456
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
405
457
|
const ops = value;
|
|
406
458
|
for (const [op, opValue] of Object.entries(ops)) {
|
|
@@ -411,18 +463,18 @@ function buildWhereClauseParts(where, fields, params) {
|
|
|
411
463
|
validOperators: [...VALID_OPERATORS]
|
|
412
464
|
});
|
|
413
465
|
}
|
|
414
|
-
conditions.push(buildOperatorCondition(fieldName, op, opValue, descriptor, params));
|
|
466
|
+
conditions.push(buildOperatorCondition(fieldName, column, op, opValue, descriptor, params));
|
|
415
467
|
}
|
|
416
468
|
} else {
|
|
417
|
-
conditions.push(buildOperatorCondition(fieldName, "$eq", value, descriptor, params));
|
|
469
|
+
conditions.push(buildOperatorCondition(fieldName, column, "$eq", value, descriptor, params));
|
|
418
470
|
}
|
|
419
471
|
}
|
|
420
472
|
if (conditions.length === 0) return null;
|
|
421
473
|
return conditions.join(" AND ");
|
|
422
474
|
}
|
|
423
|
-
function buildOperatorCondition(fieldName, operator, value, descriptor, params) {
|
|
424
|
-
const sqlValue =
|
|
425
|
-
const column = (0, import_core5.quoteIdent)(
|
|
475
|
+
function buildOperatorCondition(fieldName, columnName, operator, value, descriptor, params) {
|
|
476
|
+
const sqlValue = toSqlFilterValue(value, descriptor);
|
|
477
|
+
const column = (0, import_core5.quoteIdent)(columnName);
|
|
426
478
|
switch (operator) {
|
|
427
479
|
case "$eq":
|
|
428
480
|
if (sqlValue === null) {
|
|
@@ -457,9 +509,7 @@ function buildOperatorCondition(fieldName, operator, value, descriptor, params)
|
|
|
457
509
|
}
|
|
458
510
|
const placeholders = sqlValue.map(() => "?");
|
|
459
511
|
for (const item of sqlValue) {
|
|
460
|
-
params.push(
|
|
461
|
-
descriptor?.kind === "boolean" && typeof item === "boolean" ? item ? 1 : 0 : item
|
|
462
|
-
);
|
|
512
|
+
params.push(toSqlFilterValue(item, descriptor));
|
|
463
513
|
}
|
|
464
514
|
return `${column} IN (${placeholders.join(", ")})`;
|
|
465
515
|
}
|
|
@@ -467,12 +517,54 @@ function buildOperatorCondition(fieldName, operator, value, descriptor, params)
|
|
|
467
517
|
throw new QueryError(`Unknown operator "${operator}"`, { operator });
|
|
468
518
|
}
|
|
469
519
|
}
|
|
520
|
+
function toSqlFilterValue(value, descriptor) {
|
|
521
|
+
if (descriptor?.kind === "boolean" && typeof value === "boolean") return value ? 1 : 0;
|
|
522
|
+
return encodeStoredFilterValue(value, descriptor);
|
|
523
|
+
}
|
|
524
|
+
var VIRTUAL_TIMESTAMP_FIELDS = Object.freeze({
|
|
525
|
+
createdAt: "_created_at",
|
|
526
|
+
updatedAt: "_updated_at"
|
|
527
|
+
});
|
|
528
|
+
var VIRTUAL_TIMESTAMP_FIELD_NAMES = Object.freeze([
|
|
529
|
+
"createdAt",
|
|
530
|
+
"updatedAt"
|
|
531
|
+
]);
|
|
532
|
+
var SORT_DIRECTIONS = Object.freeze({
|
|
533
|
+
asc: "ASC",
|
|
534
|
+
desc: "DESC"
|
|
535
|
+
});
|
|
536
|
+
function resolveColumnName(fieldName, fields) {
|
|
537
|
+
if (Object.hasOwn(fields, fieldName)) return fieldName;
|
|
538
|
+
if (fieldName === "createdAt" || fieldName === "updatedAt") {
|
|
539
|
+
return VIRTUAL_TIMESTAMP_FIELDS[fieldName];
|
|
540
|
+
}
|
|
541
|
+
return fieldName;
|
|
542
|
+
}
|
|
543
|
+
function sqlSortDirection(direction, field) {
|
|
544
|
+
const key = typeof direction === "string" ? direction.toLowerCase() : "";
|
|
545
|
+
const keyword = Object.hasOwn(SORT_DIRECTIONS, key) ? SORT_DIRECTIONS[key] : void 0;
|
|
546
|
+
if (keyword === void 0) {
|
|
547
|
+
throw new QueryError(`Invalid sort direction for field "${field}". Use 'asc' or 'desc'.`, {
|
|
548
|
+
field,
|
|
549
|
+
direction: typeof direction === "string" ? direction.slice(0, 64) : typeof direction
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return keyword;
|
|
553
|
+
}
|
|
554
|
+
function assertQueryCount(value, clause) {
|
|
555
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
556
|
+
throw new QueryError(
|
|
557
|
+
`Invalid ${clause}: expected a non-negative safe integer. Pass a whole number, for example .${clause}(10).`,
|
|
558
|
+
{ clause, received: typeof value === "number" ? value : typeof value }
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
470
563
|
function validateFieldName(fieldName, fields) {
|
|
471
564
|
const allowedFields = /* @__PURE__ */ new Set([
|
|
472
565
|
...Object.keys(fields),
|
|
473
566
|
"id",
|
|
474
|
-
|
|
475
|
-
"updatedAt",
|
|
567
|
+
...VIRTUAL_TIMESTAMP_FIELD_NAMES,
|
|
476
568
|
"_created_at",
|
|
477
569
|
"_updated_at"
|
|
478
570
|
]);
|
|
@@ -484,8 +576,137 @@ function validateFieldName(fieldName, fields) {
|
|
|
484
576
|
}
|
|
485
577
|
}
|
|
486
578
|
|
|
487
|
-
// src/mutations/
|
|
579
|
+
// src/mutations/run-local-write.ts
|
|
580
|
+
var import_core7 = require("@korajs/core");
|
|
581
|
+
|
|
582
|
+
// src/mutations/write-context.ts
|
|
488
583
|
var import_core6 = require("@korajs/core");
|
|
584
|
+
var CausalScope = class {
|
|
585
|
+
constructor(tracker, chainOperations) {
|
|
586
|
+
this.tracker = tracker;
|
|
587
|
+
this.chainOperations = chainOperations;
|
|
588
|
+
}
|
|
589
|
+
tracker;
|
|
590
|
+
chainOperations;
|
|
591
|
+
lastByCollection = /* @__PURE__ */ new Map();
|
|
592
|
+
lastInScope = null;
|
|
593
|
+
recorded = [];
|
|
594
|
+
/**
|
|
595
|
+
* Direct causal parents for the next operation in `collection`.
|
|
596
|
+
*
|
|
597
|
+
* @param collection - Collection of the operation being built
|
|
598
|
+
* @param extra - Explicit parents (e.g. the delete that caused a cascade)
|
|
599
|
+
*/
|
|
600
|
+
depsFor(collection, extra = []) {
|
|
601
|
+
const deps = [];
|
|
602
|
+
const add = (id) => {
|
|
603
|
+
if (id && !deps.includes(id)) deps.push(id);
|
|
604
|
+
};
|
|
605
|
+
for (const id of extra) add(id);
|
|
606
|
+
const local = this.lastByCollection.get(collection);
|
|
607
|
+
if (local !== void 0) {
|
|
608
|
+
add(local);
|
|
609
|
+
} else {
|
|
610
|
+
for (const id of this.tracker?.nextCausalDeps(collection, false) ?? []) add(id);
|
|
611
|
+
}
|
|
612
|
+
if (this.chainOperations) add(this.lastInScope);
|
|
613
|
+
return deps;
|
|
614
|
+
}
|
|
615
|
+
/** Record an operation built in this scope. */
|
|
616
|
+
record(collection, operationId) {
|
|
617
|
+
this.lastByCollection.set(collection, operationId);
|
|
618
|
+
this.lastInScope = operationId;
|
|
619
|
+
this.recorded.push({ collection, id: operationId });
|
|
620
|
+
}
|
|
621
|
+
/** Publish this scope's heads to the shared tracker. Call only after commit. */
|
|
622
|
+
publish() {
|
|
623
|
+
for (const { collection, id } of this.recorded) {
|
|
624
|
+
this.tracker?.afterOperation(collection, id, false);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
var SequenceBlock = class {
|
|
629
|
+
constructor(tx, nodeId) {
|
|
630
|
+
this.tx = tx;
|
|
631
|
+
this.nodeId = nodeId;
|
|
632
|
+
}
|
|
633
|
+
tx;
|
|
634
|
+
nodeId;
|
|
635
|
+
last = null;
|
|
636
|
+
persisted = 0;
|
|
637
|
+
/** The next number of the block. */
|
|
638
|
+
async take() {
|
|
639
|
+
if (this.last === null) {
|
|
640
|
+
const rows = await this.tx.query(
|
|
641
|
+
`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
|
|
642
|
+
ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
|
|
643
|
+
RETURNING sequence_number`,
|
|
644
|
+
[this.nodeId]
|
|
645
|
+
);
|
|
646
|
+
const first = rows[0]?.sequence_number;
|
|
647
|
+
if (first === void 0) {
|
|
648
|
+
throw new import_core6.KoraError(
|
|
649
|
+
`Failed to reserve a sequence number for node "${this.nodeId}"`,
|
|
650
|
+
"SEQUENCE_RESERVATION_FAILED",
|
|
651
|
+
{ nodeId: this.nodeId }
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
this.last = first;
|
|
655
|
+
this.persisted = first;
|
|
656
|
+
return first;
|
|
657
|
+
}
|
|
658
|
+
this.last += 1;
|
|
659
|
+
return this.last;
|
|
660
|
+
}
|
|
661
|
+
/** Persist the end of the block. Must run inside the transaction, before commit. */
|
|
662
|
+
async close() {
|
|
663
|
+
if (this.last === null || this.last === this.persisted) return;
|
|
664
|
+
await this.tx.execute(
|
|
665
|
+
"UPDATE _kora_version_vector SET sequence_number = MAX(sequence_number, ?) WHERE node_id = ?",
|
|
666
|
+
[this.last, this.nodeId]
|
|
667
|
+
);
|
|
668
|
+
this.persisted = this.last;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
async function withWriteScope(tx, nodeId, causal, write) {
|
|
672
|
+
const scope = { tx, sequence: new SequenceBlock(tx, nodeId), causal };
|
|
673
|
+
const result = await write(scope);
|
|
674
|
+
await scope.sequence.close();
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/mutations/run-local-write.ts
|
|
679
|
+
async function runLocalWrite(ctx, write) {
|
|
680
|
+
const causal = new CausalScope(ctx.causalTracker, false);
|
|
681
|
+
let result;
|
|
682
|
+
try {
|
|
683
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
684
|
+
result = await withWriteScope(tx, ctx.nodeId, causal, write);
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
ctx.onStorageError?.(error);
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
if (!result) {
|
|
691
|
+
throw new import_core7.KoraError("Local write transaction completed without a result", "WRITE_INCOMPLETE", {
|
|
692
|
+
collection: ctx.collection
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
causal.publish();
|
|
696
|
+
publishOperations(ctx, result);
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
function publishOperations(ctx, result) {
|
|
700
|
+
const ops = [];
|
|
701
|
+
if (result.operation) ops.push(result.operation);
|
|
702
|
+
ops.push(...result.sideEffects);
|
|
703
|
+
for (const op of ops) {
|
|
704
|
+
ctx.onMutation(op.collection, op);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/mutations/write-ops.ts
|
|
709
|
+
var import_core10 = require("@korajs/core");
|
|
489
710
|
|
|
490
711
|
// src/lww/field-versions.ts
|
|
491
712
|
function parseFieldVersions(raw) {
|
|
@@ -511,119 +732,20 @@ function parseFieldVersions(raw) {
|
|
|
511
732
|
function serializeFieldVersions(versions) {
|
|
512
733
|
return JSON.stringify(versions);
|
|
513
734
|
}
|
|
514
|
-
function
|
|
515
|
-
const versions =
|
|
516
|
-
for (const field of
|
|
517
|
-
versions[field]
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// src/store/sequence-allocator.ts
|
|
523
|
-
async function allocateNextSequenceInTransaction(tx, nodeId) {
|
|
524
|
-
const rows = await tx.query(
|
|
525
|
-
`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
|
|
526
|
-
ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
|
|
527
|
-
RETURNING sequence_number`,
|
|
528
|
-
[nodeId]
|
|
529
|
-
);
|
|
530
|
-
const seq = rows[0]?.sequence_number;
|
|
531
|
-
if (seq === void 0) {
|
|
532
|
-
throw new Error(`Failed to allocate sequence number for node "${nodeId}" in transaction`);
|
|
533
|
-
}
|
|
534
|
-
return seq;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// src/mutations/resolve-causal-deps.ts
|
|
538
|
-
function resolveCausalDeps(ctx) {
|
|
539
|
-
const merged = [];
|
|
540
|
-
for (const id of ctx.extraCausalDeps ?? []) {
|
|
541
|
-
if (!merged.includes(id)) {
|
|
542
|
-
merged.push(id);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
for (const id of ctx.causalTracker?.nextCausalDeps(ctx.collection, ctx.inTransaction) ?? []) {
|
|
546
|
-
if (!merged.includes(id)) {
|
|
547
|
-
merged.push(id);
|
|
735
|
+
function stampFieldVersions(currentRaw, fields, version) {
|
|
736
|
+
const versions = parseFieldVersions(currentRaw);
|
|
737
|
+
for (const field of fields) {
|
|
738
|
+
const existing = versions[field];
|
|
739
|
+
if (existing === void 0 || existing < version) {
|
|
740
|
+
versions[field] = version;
|
|
548
741
|
}
|
|
549
742
|
}
|
|
550
|
-
return
|
|
743
|
+
return serializeFieldVersions(versions);
|
|
551
744
|
}
|
|
552
745
|
|
|
553
|
-
// src/mutations/execute-insert.ts
|
|
554
|
-
async function executeInsert(ctx, data) {
|
|
555
|
-
const validated = (0, import_core6.validateRecord)(ctx.collection, ctx.definition, data, "insert");
|
|
556
|
-
const recordId = (0, import_core6.generateUUIDv7)();
|
|
557
|
-
for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
|
|
558
|
-
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
559
|
-
validated[fieldName] = Date.now();
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const writeData = await (0, import_core6.transformSecretFieldsForWrite)(
|
|
563
|
-
validated,
|
|
564
|
-
ctx.definition,
|
|
565
|
-
ctx.secretKeyProvider
|
|
566
|
-
);
|
|
567
|
-
const causalDeps = resolveCausalDeps(ctx);
|
|
568
|
-
let operation;
|
|
569
|
-
let record;
|
|
570
|
-
await ctx.adapter.transaction(async (tx) => {
|
|
571
|
-
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
572
|
-
operation = await (0, import_core6.createOperation)(
|
|
573
|
-
{
|
|
574
|
-
nodeId: ctx.nodeId,
|
|
575
|
-
type: "insert",
|
|
576
|
-
collection: ctx.collection,
|
|
577
|
-
recordId,
|
|
578
|
-
// Binary richtext values are tagged as canonical JSON BEFORE the
|
|
579
|
-
// operation is content-hashed, so the hash input, persisted JSON,
|
|
580
|
-
// and wire payload are the identical value.
|
|
581
|
-
data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
|
|
582
|
-
previousData: null,
|
|
583
|
-
sequenceNumber,
|
|
584
|
-
causalDeps,
|
|
585
|
-
schemaVersion: ctx.schema.version
|
|
586
|
-
},
|
|
587
|
-
ctx.clock
|
|
588
|
-
);
|
|
589
|
-
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
590
|
-
const serializedData = serializeRecord(writeData, ctx.definition.fields);
|
|
591
|
-
const version = serializeRowVersion(operation.timestamp);
|
|
592
|
-
record = {
|
|
593
|
-
id: recordId,
|
|
594
|
-
...serializedData,
|
|
595
|
-
_created_at: operation.timestamp.wallTime,
|
|
596
|
-
_updated_at: operation.timestamp.wallTime,
|
|
597
|
-
_version: version,
|
|
598
|
-
// Stamp every inserted field with this operation's version so later
|
|
599
|
-
// per-field LWW compares against a real writer, not the fallback.
|
|
600
|
-
_field_versions: serializeFieldVersions(
|
|
601
|
-
fieldVersionsForFields(Object.keys(serializedData), version)
|
|
602
|
-
)
|
|
603
|
-
};
|
|
604
|
-
const builtInsert = buildInsertQuery(ctx.collection, record);
|
|
605
|
-
const opInsert = buildInsertQuery(
|
|
606
|
-
`_kora_ops_${ctx.collection}`,
|
|
607
|
-
serializeOperation(operation)
|
|
608
|
-
);
|
|
609
|
-
await tx.execute(builtInsert.sql, builtInsert.params);
|
|
610
|
-
await tx.execute(opInsert.sql, opInsert.params);
|
|
611
|
-
});
|
|
612
|
-
ctx.onMutation(ctx.collection, operation);
|
|
613
|
-
return {
|
|
614
|
-
id: recordId,
|
|
615
|
-
...writeData,
|
|
616
|
-
createdAt: operation.timestamp.wallTime,
|
|
617
|
-
updatedAt: operation.timestamp.wallTime
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// src/mutations/execute-update.ts
|
|
622
|
-
var import_core8 = require("@korajs/core");
|
|
623
|
-
|
|
624
746
|
// src/state-machine/state-validator.ts
|
|
625
|
-
var
|
|
626
|
-
var InvalidStateTransitionError = class extends
|
|
747
|
+
var import_core8 = require("@korajs/core");
|
|
748
|
+
var InvalidStateTransitionError = class extends import_core8.KoraError {
|
|
627
749
|
constructor(collection, recordId, field, fromState, toState, allowedStates) {
|
|
628
750
|
super(
|
|
629
751
|
`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)"}`,
|
|
@@ -657,7 +779,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
|
|
|
657
779
|
collection: collectionName,
|
|
658
780
|
transitions: stateMachine.transitions
|
|
659
781
|
};
|
|
660
|
-
const transitionResult = (0,
|
|
782
|
+
const transitionResult = (0, import_core8.validateTransition)(constraint, currentState, newState);
|
|
661
783
|
if (transitionResult.valid) {
|
|
662
784
|
return { valid: true, allowedStates: transitionResult.allowedTargets };
|
|
663
785
|
}
|
|
@@ -675,10 +797,25 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
|
|
|
675
797
|
return { valid: false, allowedStates };
|
|
676
798
|
}
|
|
677
799
|
function validateUpdateStateMachine(collectionName, recordId, collectionDef, currentRecord, updateData) {
|
|
800
|
+
let result = updateData;
|
|
678
801
|
const stateMachine = collectionDef.stateMachine;
|
|
679
|
-
if (stateMachine
|
|
680
|
-
|
|
802
|
+
if (stateMachine !== void 0) {
|
|
803
|
+
result = applyStateMachine(collectionName, recordId, stateMachine, currentRecord, result);
|
|
804
|
+
}
|
|
805
|
+
for (const [field, descriptor] of Object.entries(collectionDef.fields)) {
|
|
806
|
+
if (descriptor.kind !== "enum" || !descriptor.transitions) continue;
|
|
807
|
+
if (stateMachine?.field === field) continue;
|
|
808
|
+
result = applyStateMachine(
|
|
809
|
+
collectionName,
|
|
810
|
+
recordId,
|
|
811
|
+
{ field, transitions: descriptor.transitions, onInvalidTransition: "reject" },
|
|
812
|
+
currentRecord,
|
|
813
|
+
result
|
|
814
|
+
);
|
|
681
815
|
}
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
function applyStateMachine(collectionName, recordId, stateMachine, currentRecord, updateData) {
|
|
682
819
|
const stateField = stateMachine.field;
|
|
683
820
|
if (!(stateField in updateData)) {
|
|
684
821
|
return updateData;
|
|
@@ -703,161 +840,340 @@ function validateUpdateStateMachine(collectionName, recordId, collectionDef, cur
|
|
|
703
840
|
return filtered;
|
|
704
841
|
}
|
|
705
842
|
|
|
706
|
-
// src/mutations/
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
843
|
+
// src/mutations/secret-write.ts
|
|
844
|
+
var import_core9 = require("@korajs/core");
|
|
845
|
+
function toAtRestWriteData(data, definition, secretKeyProvider) {
|
|
846
|
+
return (0, import_core9.transformSecretFieldsForWrite)(data, definition, secretKeyProvider);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/mutations/write-ops.ts
|
|
850
|
+
async function prepareInsert(env, collection, data) {
|
|
851
|
+
const definition = requireDefinition(env, collection);
|
|
852
|
+
const validated = (0, import_core10.validateRecord)(collection, definition, data, "insert");
|
|
853
|
+
const now = Date.now();
|
|
854
|
+
for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
|
|
855
|
+
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
856
|
+
validated[fieldName] = now;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const writeData = await toAtRestWriteData(validated, definition, env.secretKeyProvider);
|
|
860
|
+
return { collection, recordId: (0, import_core10.generateUUIDv7)(), data: writeData };
|
|
861
|
+
}
|
|
862
|
+
async function writeInsertInTx(env, scope, insert, extraCausalDeps = []) {
|
|
863
|
+
const definition = requireDefinition(env, insert.collection);
|
|
864
|
+
const operation = await buildLocalOperation(env, scope, {
|
|
865
|
+
type: "insert",
|
|
866
|
+
collection: insert.collection,
|
|
867
|
+
recordId: insert.recordId,
|
|
868
|
+
// Binary richtext values are tagged as canonical JSON BEFORE hashing, so the
|
|
869
|
+
// hash input, persisted JSON and wire payload are the identical value.
|
|
870
|
+
data: encodeRichtextFieldsForOpData(insert.data, definition.fields),
|
|
871
|
+
previousData: null,
|
|
872
|
+
extraCausalDeps
|
|
873
|
+
});
|
|
874
|
+
if (env.fold) {
|
|
875
|
+
await appendOperationRow(scope, operation);
|
|
876
|
+
await env.fold.applyInTx(scope.tx, operation, "local");
|
|
877
|
+
} else {
|
|
878
|
+
const serialized = serializeRecord(insert.data, definition.fields);
|
|
879
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
880
|
+
const row = {
|
|
881
|
+
id: insert.recordId,
|
|
882
|
+
...serialized,
|
|
883
|
+
_created_at: operation.timestamp.wallTime,
|
|
884
|
+
_updated_at: operation.timestamp.wallTime,
|
|
885
|
+
_version: version,
|
|
886
|
+
// Every inserted field is stamped with this operation's version, so later
|
|
887
|
+
// per-field LWW compares against a real writer, not the row fallback.
|
|
888
|
+
_field_versions: stampFieldVersions(null, Object.keys(serialized), version)
|
|
889
|
+
};
|
|
890
|
+
const rowInsert = buildInsertQuery(insert.collection, row);
|
|
891
|
+
await scope.tx.execute(rowInsert.sql, rowInsert.params);
|
|
892
|
+
await appendOperationRow(scope, operation);
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
operation,
|
|
896
|
+
record: {
|
|
897
|
+
id: insert.recordId,
|
|
898
|
+
// A richtext value written as a string returns as the bytes reads return.
|
|
899
|
+
...toRichtextReadShape(insert.data, definition.fields),
|
|
900
|
+
createdAt: operation.timestamp.wallTime,
|
|
901
|
+
updatedAt: operation.timestamp.wallTime
|
|
902
|
+
},
|
|
903
|
+
sideEffects: []
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
async function writeUpdateInTx(env, scope, collection, id, data, extraCausalDeps = []) {
|
|
907
|
+
const definition = requireDefinition(env, collection);
|
|
908
|
+
const currentRow = await readLiveRow(scope, collection, id);
|
|
713
909
|
if (!currentRow) {
|
|
714
|
-
throw new RecordNotFoundError(
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
id,
|
|
721
|
-
ctx.definition,
|
|
722
|
-
currentRecord,
|
|
723
|
-
validated
|
|
724
|
-
);
|
|
725
|
-
if (Object.keys(validated).length === 0) {
|
|
726
|
-
return { ...currentRecord };
|
|
910
|
+
throw new RecordNotFoundError(collection, id);
|
|
911
|
+
}
|
|
912
|
+
const currentRecord = deserializeRecord(currentRow, definition.fields);
|
|
913
|
+
const allowed = validateUpdateStateMachine(collection, id, definition, currentRecord, data);
|
|
914
|
+
if (Object.keys(allowed).length === 0) {
|
|
915
|
+
return { operation: null, record: currentRecord, sideEffects: [] };
|
|
727
916
|
}
|
|
728
917
|
const previousData = {};
|
|
729
|
-
const
|
|
918
|
+
const resolved = {};
|
|
730
919
|
const atomicOps = {};
|
|
731
|
-
for (const key of Object.
|
|
732
|
-
const value = validated[key];
|
|
920
|
+
for (const [key, value] of Object.entries(allowed)) {
|
|
733
921
|
previousData[key] = currentRecord[key];
|
|
734
|
-
if ((0,
|
|
735
|
-
|
|
736
|
-
|
|
922
|
+
if ((0, import_core10.isAtomicOp)(value)) {
|
|
923
|
+
resolved[key] = (0, import_core10.resolveAtomicOp)(currentRecord[key], value);
|
|
924
|
+
const descriptor = definition.fields[key];
|
|
925
|
+
if (descriptor) (0, import_core10.assertResolvedFieldValue)(collection, key, descriptor, resolved[key]);
|
|
926
|
+
atomicOps[key] = (0, import_core10.toAtomicOp)(value);
|
|
737
927
|
} else {
|
|
738
|
-
|
|
928
|
+
resolved[key] = value;
|
|
739
929
|
}
|
|
740
930
|
}
|
|
741
|
-
const writeData = await (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
recordId: id,
|
|
757
|
-
// Binary richtext values (new and previous) are tagged as canonical
|
|
758
|
-
// JSON BEFORE the operation is content-hashed, so the hash input,
|
|
759
|
-
// persisted JSON, and wire payload are the identical value.
|
|
760
|
-
data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
|
|
761
|
-
previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
|
|
762
|
-
sequenceNumber,
|
|
763
|
-
causalDeps,
|
|
764
|
-
schemaVersion: ctx.schema.version,
|
|
765
|
-
...hasAtomicOps ? { atomicOps } : {}
|
|
766
|
-
},
|
|
767
|
-
ctx.clock
|
|
768
|
-
);
|
|
769
|
-
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
770
|
-
const serializedChanges = serializeRecord(writeData, ctx.definition.fields);
|
|
931
|
+
const writeData = await toAtRestWriteData(resolved, definition, env.secretKeyProvider);
|
|
932
|
+
const operation = await buildLocalOperation(env, scope, {
|
|
933
|
+
type: "update",
|
|
934
|
+
collection,
|
|
935
|
+
recordId: id,
|
|
936
|
+
data: encodeRichtextFieldsForOpData(writeData, definition.fields),
|
|
937
|
+
previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
|
|
938
|
+
extraCausalDeps,
|
|
939
|
+
...Object.keys(atomicOps).length > 0 ? { atomicOps } : {}
|
|
940
|
+
});
|
|
941
|
+
if (env.fold) {
|
|
942
|
+
await appendOperationRow(scope, operation);
|
|
943
|
+
await env.fold.applyInTx(scope.tx, operation, "local");
|
|
944
|
+
} else {
|
|
945
|
+
const serializedChanges = serializeRecord(writeData, definition.fields);
|
|
771
946
|
const version = serializeRowVersion(operation.timestamp);
|
|
772
|
-
const
|
|
773
|
-
for (const changedField of Object.keys(serializedChanges)) {
|
|
774
|
-
mergedFieldVersions[changedField] = version;
|
|
775
|
-
}
|
|
776
|
-
const updateQuery = buildUpdateQuery(ctx.collection, id, {
|
|
947
|
+
const rowUpdate = buildUpdateQuery(collection, id, {
|
|
777
948
|
...serializedChanges,
|
|
778
949
|
_updated_at: operation.timestamp.wallTime,
|
|
779
950
|
_version: version,
|
|
780
|
-
_field_versions:
|
|
951
|
+
_field_versions: stampFieldVersions(
|
|
952
|
+
currentRow._field_versions,
|
|
953
|
+
Object.keys(serializedChanges),
|
|
954
|
+
version
|
|
955
|
+
)
|
|
781
956
|
});
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
957
|
+
await scope.tx.execute(rowUpdate.sql, rowUpdate.params);
|
|
958
|
+
await appendOperationRow(scope, operation);
|
|
959
|
+
}
|
|
960
|
+
const updatedRow = await readLiveRow(scope, collection, id);
|
|
961
|
+
if (!updatedRow) {
|
|
962
|
+
throw new RecordNotFoundError(collection, id);
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
operation,
|
|
966
|
+
record: deserializeRecord(updatedRow, definition.fields),
|
|
967
|
+
sideEffects: []
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async function writeDeleteInTx(env, scope, collection, id, options = {}) {
|
|
971
|
+
const definition = requireDefinition(env, collection);
|
|
972
|
+
const currentRow = await readLiveRow(scope, collection, id);
|
|
973
|
+
if (!currentRow) {
|
|
974
|
+
throw new RecordNotFoundError(collection, id);
|
|
975
|
+
}
|
|
976
|
+
const operation = await buildLocalOperation(env, scope, {
|
|
977
|
+
type: "delete",
|
|
978
|
+
collection,
|
|
979
|
+
recordId: id,
|
|
980
|
+
data: null,
|
|
981
|
+
previousData: null,
|
|
982
|
+
extraCausalDeps: options.extraCausalDeps ?? []
|
|
788
983
|
});
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
984
|
+
if (!options.skipReferentialEnforcement && env.beforeLocalDelete) {
|
|
985
|
+
await env.beforeLocalDelete(operation, scope.tx);
|
|
986
|
+
}
|
|
987
|
+
if (env.fold) {
|
|
988
|
+
await appendOperationRow(scope, operation);
|
|
989
|
+
await env.fold.applyInTx(scope.tx, operation, "local");
|
|
990
|
+
} else {
|
|
991
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
992
|
+
const softDelete = buildSoftDeleteQuery(collection, id, operation.timestamp.wallTime, version);
|
|
993
|
+
await scope.tx.execute(softDelete.sql, softDelete.params);
|
|
994
|
+
await appendOperationRow(scope, operation);
|
|
995
|
+
}
|
|
996
|
+
const sideEffects = [];
|
|
997
|
+
if (!options.skipReferentialEnforcement && env.relationEnforcer) {
|
|
998
|
+
const result = await env.relationEnforcer.enforceDelete(
|
|
999
|
+
collection,
|
|
1000
|
+
id,
|
|
1001
|
+
scope,
|
|
1002
|
+
env,
|
|
1003
|
+
operation.id
|
|
1004
|
+
);
|
|
1005
|
+
sideEffects.push(...result.operations);
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
operation,
|
|
1009
|
+
record: deserializeRecord(currentRow, definition.fields),
|
|
1010
|
+
sideEffects
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
async function buildLocalOperation(env, scope, input) {
|
|
1014
|
+
const sequenceNumber = await scope.sequence.take();
|
|
1015
|
+
const causalDeps = scope.causal.depsFor(input.collection, input.extraCausalDeps);
|
|
1016
|
+
const operation = await (0, import_core10.createOperation)(
|
|
1017
|
+
{
|
|
1018
|
+
nodeId: env.nodeId,
|
|
1019
|
+
type: input.type,
|
|
1020
|
+
collection: input.collection,
|
|
1021
|
+
recordId: input.recordId,
|
|
1022
|
+
data: input.data,
|
|
1023
|
+
previousData: input.previousData,
|
|
1024
|
+
sequenceNumber,
|
|
1025
|
+
causalDeps,
|
|
1026
|
+
schemaVersion: env.schema.version,
|
|
1027
|
+
...input.atomicOps !== void 0 ? { atomicOps: input.atomicOps } : {},
|
|
1028
|
+
...env.transactionId !== void 0 ? { transactionId: env.transactionId } : {},
|
|
1029
|
+
...env.mutationName !== void 0 ? { mutationName: env.mutationName } : {}
|
|
1030
|
+
},
|
|
1031
|
+
env.clock
|
|
1032
|
+
);
|
|
1033
|
+
const maxBytes = env.maxOperationBytes ?? import_core10.DEFAULT_MAX_OPERATION_BYTES;
|
|
1034
|
+
const bytes = (0, import_core10.measureOperationBytes)(operation);
|
|
1035
|
+
if (bytes > maxBytes) {
|
|
1036
|
+
throw new import_core10.OperationTooLargeError(input.collection, input.recordId, bytes, maxBytes);
|
|
1037
|
+
}
|
|
1038
|
+
scope.causal.record(input.collection, operation.id);
|
|
1039
|
+
return operation;
|
|
1040
|
+
}
|
|
1041
|
+
async function appendOperationRow(scope, operation) {
|
|
1042
|
+
const opInsert = buildInsertQuery(
|
|
1043
|
+
`_kora_ops_${operation.collection}`,
|
|
1044
|
+
serializeOperation(operation)
|
|
1045
|
+
);
|
|
1046
|
+
await scope.tx.execute(opInsert.sql, opInsert.params);
|
|
1047
|
+
}
|
|
1048
|
+
async function readLiveRow(scope, collection, id) {
|
|
1049
|
+
const rows = await scope.tx.query(
|
|
1050
|
+
`SELECT * FROM ${(0, import_core10.quoteIdent)(collection)} WHERE id = ? AND _deleted = 0`,
|
|
792
1051
|
[id]
|
|
793
1052
|
);
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1053
|
+
return rows[0];
|
|
1054
|
+
}
|
|
1055
|
+
function requireDefinition(env, collection) {
|
|
1056
|
+
const definition = env.schema.collections[collection];
|
|
1057
|
+
if (!definition) {
|
|
1058
|
+
throw new import_core10.KoraError(
|
|
1059
|
+
`Unknown collection "${collection}". Available: ${Object.keys(env.schema.collections).join(", ")}`,
|
|
1060
|
+
"UNKNOWN_COLLECTION",
|
|
1061
|
+
{ collection }
|
|
1062
|
+
);
|
|
797
1063
|
}
|
|
798
|
-
return
|
|
1064
|
+
return definition;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/mutations/execute-insert.ts
|
|
1068
|
+
async function executeInsert(ctx, data) {
|
|
1069
|
+
const prepared = await prepareInsert(ctx, ctx.collection, data);
|
|
1070
|
+
const result = await runLocalWrite(
|
|
1071
|
+
ctx,
|
|
1072
|
+
(scope) => writeInsertInTx(ctx, scope, prepared, ctx.extraCausalDeps ?? [])
|
|
1073
|
+
);
|
|
1074
|
+
return result.record;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/mutations/execute-update.ts
|
|
1078
|
+
var import_core11 = require("@korajs/core");
|
|
1079
|
+
async function executeUpdate(ctx, id, data) {
|
|
1080
|
+
const validated = (0, import_core11.validateRecord)(ctx.collection, ctx.definition, data, "update");
|
|
1081
|
+
const result = await runLocalWrite(
|
|
1082
|
+
ctx,
|
|
1083
|
+
(scope) => writeUpdateInTx(ctx, scope, ctx.collection, id, validated, ctx.extraCausalDeps ?? [])
|
|
1084
|
+
);
|
|
1085
|
+
return result.record;
|
|
799
1086
|
}
|
|
800
1087
|
|
|
801
1088
|
// src/mutations/execute-delete.ts
|
|
802
|
-
var import_core9 = require("@korajs/core");
|
|
803
1089
|
async function executeDelete(ctx, id, options) {
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
1090
|
+
const result = await runLocalWrite(
|
|
1091
|
+
ctx,
|
|
1092
|
+
(scope) => writeDeleteInTx(ctx, scope, ctx.collection, id, {
|
|
1093
|
+
skipReferentialEnforcement: options?.skipReferentialEnforcement ?? false,
|
|
1094
|
+
extraCausalDeps: ctx.extraCausalDeps ?? []
|
|
1095
|
+
})
|
|
807
1096
|
);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1097
|
+
return result.operation ? [result.operation, ...result.sideEffects] : result.sideEffects;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/sync/sync-state.ts
|
|
1101
|
+
var import_core12 = require("@korajs/core");
|
|
1102
|
+
|
|
1103
|
+
// src/sync/sync-durability.ts
|
|
1104
|
+
var OWN_ACKED_THROUGH_META_KEY = "own_acked_through";
|
|
1105
|
+
|
|
1106
|
+
// src/sync/local-sync-records.ts
|
|
1107
|
+
var TERMINAL_REJECTIONS_TABLE = "_kora_terminal_rejections";
|
|
1108
|
+
var LOCAL_NODES_TABLE = "_kora_local_nodes";
|
|
1109
|
+
var TERMINAL_REJECTIONS_DDL = `CREATE TABLE IF NOT EXISTS ${TERMINAL_REJECTIONS_TABLE} (
|
|
1110
|
+
operation_id TEXT PRIMARY KEY NOT NULL,
|
|
1111
|
+
node_id TEXT,
|
|
1112
|
+
sequence_number INTEGER,
|
|
1113
|
+
code TEXT NOT NULL,
|
|
1114
|
+
rejected_at INTEGER NOT NULL
|
|
1115
|
+
)`;
|
|
1116
|
+
var LOCAL_NODES_DDL = `CREATE TABLE IF NOT EXISTS ${LOCAL_NODES_TABLE} (
|
|
1117
|
+
node_id TEXT PRIMARY KEY NOT NULL,
|
|
1118
|
+
created_at INTEGER NOT NULL,
|
|
1119
|
+
accepted INTEGER NOT NULL DEFAULT 0,
|
|
1120
|
+
held INTEGER NOT NULL DEFAULT 0,
|
|
1121
|
+
refused_cycle INTEGER,
|
|
1122
|
+
principal TEXT,
|
|
1123
|
+
binding TEXT,
|
|
1124
|
+
refused_principals TEXT
|
|
1125
|
+
)`;
|
|
1126
|
+
async function hasUnsyncedOwnOperations(db) {
|
|
1127
|
+
const tables = new Set(
|
|
1128
|
+
(await db.query("SELECT name FROM sqlite_master WHERE type = 'table'")).map(
|
|
1129
|
+
(row) => row.name
|
|
1130
|
+
)
|
|
1131
|
+
);
|
|
1132
|
+
const opsTables = [...tables].filter((name) => name.startsWith("_kora_ops_"));
|
|
1133
|
+
if (opsTables.length === 0 || !tables.has("_kora_meta")) return false;
|
|
1134
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
1135
|
+
if (tables.has(LOCAL_NODES_TABLE)) {
|
|
1136
|
+
for (const row of await db.query(
|
|
1137
|
+
`SELECT node_id FROM ${LOCAL_NODES_TABLE}`
|
|
1138
|
+
)) {
|
|
1139
|
+
nodes.add(row.node_id);
|
|
832
1140
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1141
|
+
}
|
|
1142
|
+
const meta = await db.query(
|
|
1143
|
+
"SELECT key, value FROM _kora_meta WHERE key = 'node_id' OR key = ? OR key LIKE ?",
|
|
1144
|
+
[OWN_ACKED_THROUGH_META_KEY, `${OWN_ACKED_THROUGH_META_KEY}:%`]
|
|
1145
|
+
);
|
|
1146
|
+
const prefixes = /* @__PURE__ */ new Map();
|
|
1147
|
+
for (const row of meta) {
|
|
1148
|
+
if (row.key === "node_id") {
|
|
1149
|
+
nodes.add(row.value);
|
|
1150
|
+
} else if (row.key === OWN_ACKED_THROUGH_META_KEY) {
|
|
1151
|
+
try {
|
|
1152
|
+
const parsed = JSON.parse(row.value);
|
|
1153
|
+
if (typeof parsed.nodeId === "string") {
|
|
1154
|
+
nodes.add(parsed.nodeId);
|
|
1155
|
+
if (typeof parsed.sequence === "number") prefixes.set(parsed.nodeId, parsed.sequence);
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
1159
|
+
} else {
|
|
1160
|
+
const nodeId = row.key.slice(OWN_ACKED_THROUGH_META_KEY.length + 1);
|
|
1161
|
+
const sequence = Number(row.value);
|
|
1162
|
+
if (!prefixes.has(nodeId) && Number.isInteger(sequence)) prefixes.set(nodeId, sequence);
|
|
838
1163
|
}
|
|
839
|
-
const version = serializeRowVersion(operation.timestamp);
|
|
840
|
-
const deleteQuery = buildSoftDeleteQuery(
|
|
841
|
-
ctx.collection,
|
|
842
|
-
id,
|
|
843
|
-
operation.timestamp.wallTime,
|
|
844
|
-
version
|
|
845
|
-
);
|
|
846
|
-
const opInsert = buildInsertQuery(
|
|
847
|
-
`_kora_ops_${ctx.collection}`,
|
|
848
|
-
serializeOperation(operation)
|
|
849
|
-
);
|
|
850
|
-
await tx.execute(deleteQuery.sql, deleteQuery.params);
|
|
851
|
-
await tx.execute(opInsert.sql, opInsert.params);
|
|
852
|
-
});
|
|
853
|
-
if (!operation) {
|
|
854
|
-
throw new Error("Delete operation was not created");
|
|
855
1164
|
}
|
|
856
|
-
|
|
857
|
-
for (const
|
|
858
|
-
|
|
1165
|
+
const exclude = tables.has(TERMINAL_REJECTIONS_TABLE) ? ` AND id NOT IN (SELECT operation_id FROM ${TERMINAL_REJECTIONS_TABLE})` : "";
|
|
1166
|
+
for (const nodeId of nodes) {
|
|
1167
|
+
const prefix = prefixes.get(nodeId) ?? 0;
|
|
1168
|
+
for (const table of opsTables) {
|
|
1169
|
+
const rows = await db.query(
|
|
1170
|
+
`SELECT COUNT(*) AS n FROM "${table.replace(/"/g, '""')}" WHERE node_id = ? AND sequence_number > ?${exclude}`,
|
|
1171
|
+
[nodeId, prefix]
|
|
1172
|
+
);
|
|
1173
|
+
if (Number(rows[0]?.n ?? 0) > 0) return true;
|
|
1174
|
+
}
|
|
859
1175
|
}
|
|
860
|
-
return
|
|
1176
|
+
return false;
|
|
861
1177
|
}
|
|
862
1178
|
// Annotate the CommonJS export names for ESM import in node:
|
|
863
1179
|
0 && (module.exports = {
|
|
@@ -875,8 +1191,8 @@ async function executeDelete(ctx, id, options) {
|
|
|
875
1191
|
executeDelete,
|
|
876
1192
|
executeInsert,
|
|
877
1193
|
executeUpdate,
|
|
1194
|
+
hasUnsyncedOwnOperations,
|
|
878
1195
|
isIncomingNewerThanRow,
|
|
879
|
-
resolveCausalDeps,
|
|
880
1196
|
rowVersionFromRecord,
|
|
881
1197
|
serializeOperation,
|
|
882
1198
|
serializeRecord,
|