@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.
Files changed (69) hide show
  1. package/dist/adapters/better-sqlite3.d.cts +1 -1
  2. package/dist/adapters/better-sqlite3.d.ts +1 -1
  3. package/dist/adapters/indexeddb.cjs +410 -17
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +23 -5
  6. package/dist/adapters/indexeddb.d.ts +23 -5
  7. package/dist/adapters/indexeddb.js +163 -18
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
  10. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
  11. package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
  12. package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
  13. package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
  14. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
  15. package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
  16. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  17. package/dist/adapters/sqlite-wasm-worker.js +34 -5
  18. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  19. package/dist/adapters/sqlite-wasm.cjs +249 -2
  20. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  21. package/dist/adapters/sqlite-wasm.d.cts +15 -3
  22. package/dist/adapters/sqlite-wasm.d.ts +15 -3
  23. package/dist/adapters/sqlite-wasm.js +2 -2
  24. package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
  25. package/dist/backup-JN3LB4YT.js +14 -0
  26. package/dist/backup-JN3LB4YT.js.map +1 -0
  27. package/dist/chunk-56LL4XP6.js +205 -0
  28. package/dist/chunk-56LL4XP6.js.map +1 -0
  29. package/dist/chunk-6AHYGQQT.js +425 -0
  30. package/dist/chunk-6AHYGQQT.js.map +1 -0
  31. package/dist/chunk-7BPETY5Y.js +570 -0
  32. package/dist/chunk-7BPETY5Y.js.map +1 -0
  33. package/dist/chunk-AGN3CIPB.js +188 -0
  34. package/dist/chunk-AGN3CIPB.js.map +1 -0
  35. package/dist/chunk-EZSTD2XC.js +380 -0
  36. package/dist/chunk-EZSTD2XC.js.map +1 -0
  37. package/dist/chunk-MYSJN3DF.js +117 -0
  38. package/dist/chunk-MYSJN3DF.js.map +1 -0
  39. package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
  40. package/dist/chunk-XO4LMPAH.js.map +1 -0
  41. package/dist/export-audit-5GV6VRDZ.js +15 -0
  42. package/dist/export-audit-5GV6VRDZ.js.map +1 -0
  43. package/dist/index.cjs +2183 -587
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +423 -187
  46. package/dist/index.d.ts +423 -187
  47. package/dist/index.js +886 -726
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal.cjs +789 -0
  50. package/dist/internal.cjs.map +1 -0
  51. package/dist/internal.d.cts +154 -0
  52. package/dist/internal.d.ts +154 -0
  53. package/dist/internal.js +48 -0
  54. package/dist/internal.js.map +1 -0
  55. package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
  56. package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
  57. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
  58. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
  59. package/dist/types-BMyHmwvn.d.ts +163 -0
  60. package/dist/types-CTr00g_d.d.cts +163 -0
  61. package/dist/types-DepnRDk_.d.cts +212 -0
  62. package/dist/types-DepnRDk_.d.ts +212 -0
  63. package/package.json +23 -2
  64. package/dist/chunk-DXKLAQ6P.js.map +0 -1
  65. package/dist/chunk-ZP5AXQ3Z.js +0 -179
  66. package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
  67. package/dist/types-DF-KDSK1.d.cts +0 -106
  68. package/dist/types-DF-KDSK1.d.ts +0 -106
  69. /package/dist/{sqlite-wasm-channel-46AOWNPM.js.map → audit-trace-store-AM4EWQNZ.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,3 +1,31 @@
1
+ import {
2
+ decodeAuditExport,
3
+ readAuditExportManifest,
4
+ verifyAuditExportChecksum
5
+ } from "./chunk-AGN3CIPB.js";
6
+ import {
7
+ persistedAuditTraceFromEvent
8
+ } from "./chunk-MYSJN3DF.js";
9
+ import {
10
+ InvalidStateTransitionError,
11
+ allocateNextSequenceInTransaction,
12
+ allocateNextSequenceNumber,
13
+ buildCountQuery,
14
+ buildInsertQuery,
15
+ buildLwwSoftDeleteQuery,
16
+ buildLwwUpdateQuery,
17
+ buildSelectQuery,
18
+ buildSoftDeleteQuery,
19
+ buildUpdateQuery,
20
+ executeDelete,
21
+ executeInsert,
22
+ executeUpdate,
23
+ isIncomingNewerThanRow,
24
+ readSequenceNumber,
25
+ serializeRowVersion,
26
+ validateStateTransition,
27
+ validateUpdateStateMachine
28
+ } from "./chunk-7BPETY5Y.js";
1
29
  import {
2
30
  AdapterError,
3
31
  PersistenceError,
@@ -7,452 +35,186 @@ import {
7
35
  WorkerInitError,
8
36
  WorkerTimeoutError
9
37
  } from "./chunk-LAWV6CFH.js";
10
-
11
- // src/store/store.ts
12
38
  import {
13
- HybridLogicalClock as HybridLogicalClock2,
14
- createVersionVector,
15
- generateUUIDv7 as generateUUIDv73,
16
- migrationStepsToSQL
17
- } from "@korajs/core";
18
-
19
- // src/collection/collection.ts
39
+ exportBackup,
40
+ readBackupManifest,
41
+ restoreBackup,
42
+ verifyBackupChecksum
43
+ } from "./chunk-EZSTD2XC.js";
20
44
  import {
21
- createOperation,
22
- generateUUIDv7,
23
- isAtomicOp,
24
- resolveAtomicOp,
25
- toAtomicOp,
26
- validateRecord
27
- } from "@korajs/core";
45
+ decodeRichtext,
46
+ deserializeOperationWithCollection,
47
+ deserializeRecord,
48
+ encodeRichtext,
49
+ richtextStatesEqual,
50
+ richtextToPlainText,
51
+ serializeOperation,
52
+ serializeRecord
53
+ } from "./chunk-56LL4XP6.js";
28
54
 
29
- // src/query/sql-builder.ts
30
- function buildSelectQuery(descriptor, fields) {
31
- const params = [];
32
- const parts = [`SELECT * FROM ${descriptor.collection}`];
33
- const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
34
- const deletedFilter = "_deleted = 0";
35
- if (whereClause) {
36
- parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
37
- } else {
38
- parts.push(`WHERE ${deletedFilter}`);
39
- }
40
- if (descriptor.orderBy.length > 0) {
41
- const orderParts = descriptor.orderBy.map((o) => {
42
- validateFieldName(o.field, fields);
43
- return `${o.field} ${o.direction.toUpperCase()}`;
44
- });
45
- parts.push(`ORDER BY ${orderParts.join(", ")}`);
46
- }
47
- if (descriptor.limit !== void 0) {
48
- parts.push(`LIMIT ${descriptor.limit}`);
49
- }
50
- if (descriptor.offset !== void 0) {
51
- parts.push(`OFFSET ${descriptor.offset}`);
52
- }
53
- return { sql: parts.join(" "), params };
54
- }
55
- function buildCountQuery(descriptor, fields) {
56
- const params = [];
57
- const parts = [`SELECT COUNT(*) as count FROM ${descriptor.collection}`];
58
- const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
59
- const deletedFilter = "_deleted = 0";
60
- if (whereClause) {
61
- parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
62
- } else {
63
- parts.push(`WHERE ${deletedFilter}`);
64
- }
65
- return { sql: parts.join(" "), params };
66
- }
67
- function buildInsertQuery(collection, record) {
68
- const columns = Object.keys(record);
69
- const placeholders = columns.map(() => "?");
70
- const params = Object.values(record);
71
- const sql = `INSERT INTO ${collection} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
72
- return { sql, params };
73
- }
74
- function buildUpdateQuery(collection, id, changes) {
75
- const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
76
- const params = [...Object.values(changes), id];
77
- const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
78
- return { sql, params };
79
- }
80
- function buildSoftDeleteQuery(collection, id, updatedAt) {
81
- return {
82
- sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
83
- params: [updatedAt, id]
84
- };
85
- }
86
- var VALID_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"]);
87
- function buildWhereClauseParts(where, fields, params) {
88
- const conditions = [];
89
- for (const [fieldName, value] of Object.entries(where)) {
90
- validateFieldName(fieldName, fields);
91
- const descriptor = fields[fieldName];
92
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
93
- const ops = value;
94
- for (const [op, opValue] of Object.entries(ops)) {
95
- if (!VALID_OPERATORS.has(op)) {
96
- throw new QueryError(`Unknown operator "${op}" on field "${fieldName}"`, {
97
- field: fieldName,
98
- operator: op,
99
- validOperators: [...VALID_OPERATORS]
100
- });
101
- }
102
- conditions.push(buildOperatorCondition(fieldName, op, opValue, descriptor, params));
103
- }
104
- } else {
105
- conditions.push(buildOperatorCondition(fieldName, "$eq", value, descriptor, params));
55
+ // src/compaction/types.ts
56
+ var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
57
+
58
+ // src/compaction/compact-operation-log.ts
59
+ import { HybridLogicalClock, createVersionVector } from "@korajs/core";
60
+ function computeAckCompactionWatermark(serverVector) {
61
+ const watermark = createVersionVector();
62
+ for (const [nodeId, seq] of serverVector) {
63
+ if (seq > 0) {
64
+ watermark.set(nodeId, seq);
106
65
  }
107
66
  }
108
- if (conditions.length === 0) return null;
109
- return conditions.join(" AND ");
67
+ return watermark;
110
68
  }
111
- function buildOperatorCondition(fieldName, operator, value, descriptor, params) {
112
- const sqlValue = descriptor?.kind === "boolean" && typeof value === "boolean" ? value ? 1 : 0 : value;
113
- switch (operator) {
114
- case "$eq":
115
- if (sqlValue === null) {
116
- return `${fieldName} IS NULL`;
117
- }
118
- params.push(sqlValue);
119
- return `${fieldName} = ?`;
120
- case "$ne":
121
- if (sqlValue === null) {
122
- return `${fieldName} IS NOT NULL`;
69
+ async function compactOperationLog(adapter, schema, strategy, serverVector) {
70
+ if (strategy.mode === "never") {
71
+ return { deletedCount: 0, watermark: createVersionVector() };
72
+ }
73
+ const watermark = computeAckCompactionWatermark(serverVector);
74
+ if (watermark.size === 0) {
75
+ return { deletedCount: 0, watermark };
76
+ }
77
+ const ageCutoffWall = strategy.mode === "after-days" ? Date.now() - strategy.days * 24 * 60 * 60 * 1e3 : null;
78
+ const ageCutoffSerialized = ageCutoffWall !== null ? HybridLogicalClock.serialize({
79
+ wallTime: ageCutoffWall,
80
+ logical: 0,
81
+ nodeId: ""
82
+ }).slice(0, 15) : null;
83
+ let deletedCount = 0;
84
+ const collectionNames = Object.keys(schema.collections);
85
+ await adapter.transaction(async (tx) => {
86
+ for (const collectionName of collectionNames) {
87
+ const table = `_kora_ops_${collectionName}`;
88
+ for (const [nodeId, maxSeq] of watermark) {
89
+ if (maxSeq <= 0) continue;
90
+ const countSql = buildCountSql(table, ageCutoffSerialized !== null);
91
+ const countParams = ageCutoffSerialized !== null ? [nodeId, maxSeq, ageCutoffSerialized] : [nodeId, maxSeq];
92
+ const countRows = await tx.query(countSql, countParams);
93
+ deletedCount += countRows[0]?.count ?? 0;
94
+ const deleteSql = buildDeleteSql(table, ageCutoffSerialized !== null);
95
+ await tx.execute(deleteSql, countParams);
123
96
  }
124
- params.push(sqlValue);
125
- return `${fieldName} != ?`;
126
- case "$gt":
127
- params.push(sqlValue);
128
- return `${fieldName} > ?`;
129
- case "$gte":
130
- params.push(sqlValue);
131
- return `${fieldName} >= ?`;
132
- case "$lt":
133
- params.push(sqlValue);
134
- return `${fieldName} < ?`;
135
- case "$lte":
136
- params.push(sqlValue);
137
- return `${fieldName} <= ?`;
138
- case "$in": {
139
- if (!Array.isArray(sqlValue)) {
140
- throw new QueryError(`$in operator requires an array value for field "${fieldName}"`, {
141
- field: fieldName,
142
- received: typeof sqlValue
143
- });
144
- }
145
- const placeholders = sqlValue.map(() => "?");
146
- for (const item of sqlValue) {
147
- params.push(
148
- descriptor?.kind === "boolean" && typeof item === "boolean" ? item ? 1 : 0 : item
149
- );
150
- }
151
- return `${fieldName} IN (${placeholders.join(", ")})`;
152
97
  }
153
- default:
154
- throw new QueryError(`Unknown operator "${operator}"`, { operator });
155
- }
156
- }
157
- function validateFieldName(fieldName, fields) {
158
- const allowedFields = /* @__PURE__ */ new Set([
159
- ...Object.keys(fields),
160
- "id",
161
- "createdAt",
162
- "updatedAt",
163
- "_created_at",
164
- "_updated_at"
165
- ]);
166
- if (!allowedFields.has(fieldName)) {
167
- throw new QueryError(
168
- `Unknown field "${fieldName}" in query. Available fields: ${[...allowedFields].join(", ")}`,
169
- { field: fieldName }
170
- );
171
- }
98
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
99
+ COMPACTION_BASELINE_META_KEY,
100
+ String(Date.now())
101
+ ]);
102
+ });
103
+ return { deletedCount, watermark };
172
104
  }
173
-
174
- // src/serialization/serializer.ts
175
- import { HybridLogicalClock } from "@korajs/core";
176
-
177
- // src/serialization/richtext-serializer.ts
178
- import * as Y from "yjs";
179
- var TEXT_KEY = "content";
180
- function encodeRichtext(value) {
181
- if (value === null || value === void 0) {
182
- return null;
183
- }
184
- if (typeof value === "string") {
185
- const doc = new Y.Doc();
186
- doc.getText(TEXT_KEY).insert(0, value);
187
- return Y.encodeStateAsUpdate(doc);
188
- }
189
- if (value instanceof Uint8Array) {
190
- return value;
105
+ function buildCountSql(table, withAge) {
106
+ if (withAge) {
107
+ return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
191
108
  }
192
- if (value instanceof ArrayBuffer) {
193
- return new Uint8Array(value);
194
- }
195
- throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
109
+ return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
196
110
  }
197
- function decodeRichtext(value) {
198
- if (value === null || value === void 0) {
199
- return null;
200
- }
201
- if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
202
- return new Uint8Array(value);
203
- }
204
- if (value instanceof Uint8Array) {
205
- return value;
111
+ function buildDeleteSql(table, withAge) {
112
+ if (withAge) {
113
+ return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
206
114
  }
207
- if (value instanceof ArrayBuffer) {
208
- return new Uint8Array(value);
209
- }
210
- throw new Error(
211
- "Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined."
212
- );
213
- }
214
- function richtextToPlainText(value) {
215
- const encoded = encodeRichtext(value);
216
- if (!encoded) return "";
217
- const doc = new Y.Doc();
218
- Y.applyUpdate(doc, encoded);
219
- return doc.getText(TEXT_KEY).toString();
115
+ return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
220
116
  }
221
117
 
222
- // src/serialization/serializer.ts
223
- function serializeRecord(data, fields) {
224
- const result = {};
225
- for (const [key, value] of Object.entries(data)) {
226
- const descriptor = fields[key];
227
- if (!descriptor) {
228
- result[key] = value;
229
- continue;
230
- }
231
- result[key] = serializeValue(value, descriptor);
232
- }
233
- return result;
118
+ // src/sync/sync-state.ts
119
+ import { createVersionVector as createVersionVector2 } from "@korajs/core";
120
+ var LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
121
+ var DELTA_CURSOR_META_KEY = "delta_cursor";
122
+ function serializeVersionVectorToMeta(vector) {
123
+ const record = {};
124
+ for (const [nodeId, seq] of vector) {
125
+ record[nodeId] = seq;
126
+ }
127
+ return JSON.stringify(record);
234
128
  }
235
- function deserializeRecord(row, fields) {
236
- const result = {
237
- id: row.id,
238
- createdAt: row._created_at,
239
- updatedAt: row._updated_at
240
- };
241
- for (const [key, descriptor] of Object.entries(fields)) {
242
- const rawValue = row[key];
243
- if (rawValue === void 0 || rawValue === null) {
244
- result[key] = rawValue ?? null;
245
- continue;
129
+ function deserializeVersionVectorFromMeta(value) {
130
+ const parsed = JSON.parse(value);
131
+ const vector = createVersionVector2();
132
+ for (const [nodeId, seq] of Object.entries(parsed)) {
133
+ if (typeof seq === "number" && seq >= 0) {
134
+ vector.set(nodeId, seq);
246
135
  }
247
- result[key] = deserializeValue(rawValue, descriptor);
248
136
  }
249
- return result;
137
+ return vector;
250
138
  }
251
- var ATOMIC_OPS_KEY = "__kora_atomic_ops__";
252
- var TX_ID_KEY = "__kora_tx_id__";
253
- var MUTATION_NAME_KEY = "__kora_mutation__";
254
- function serializeOperation(op) {
255
- const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
256
- let dataPayload = null;
257
- if (op.data) {
258
- dataPayload = { ...op.data };
259
- if (op.atomicOps !== void 0 && Object.keys(op.atomicOps).length > 0) {
260
- dataPayload[ATOMIC_OPS_KEY] = op.atomicOps;
261
- }
262
- if (op.transactionId !== void 0) {
263
- dataPayload[TX_ID_KEY] = op.transactionId;
264
- }
265
- if (op.mutationName !== void 0) {
266
- dataPayload[MUTATION_NAME_KEY] = op.mutationName;
267
- }
268
- } else if (hasMetadata) {
269
- dataPayload = {};
270
- if (op.transactionId !== void 0) {
271
- dataPayload[TX_ID_KEY] = op.transactionId;
272
- }
273
- if (op.mutationName !== void 0) {
274
- dataPayload[MUTATION_NAME_KEY] = op.mutationName;
275
- }
139
+ function mergeVersionVectors(a, b) {
140
+ const merged = new Map(a);
141
+ for (const [nodeId, seq] of b) {
142
+ merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, seq));
276
143
  }
277
- return {
278
- id: op.id,
279
- node_id: op.nodeId,
280
- type: op.type,
281
- record_id: op.recordId,
282
- data: dataPayload ? JSON.stringify(dataPayload) : null,
283
- previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
284
- timestamp: HybridLogicalClock.serialize(op.timestamp),
285
- sequence_number: op.sequenceNumber,
286
- causal_deps: JSON.stringify(op.causalDeps),
287
- schema_version: op.schemaVersion
288
- };
144
+ return merged;
289
145
  }
290
- function deserializeOperation(row) {
291
- let data = null;
292
- let atomicOps;
293
- let transactionId;
294
- let mutationName;
295
- if (row.data) {
296
- const parsed = JSON.parse(row.data);
297
- if (ATOMIC_OPS_KEY in parsed) {
298
- atomicOps = parsed[ATOMIC_OPS_KEY];
299
- }
300
- if (TX_ID_KEY in parsed) {
301
- transactionId = parsed[TX_ID_KEY];
146
+ async function collectOperationsAheadOfServer(localVector, serverVector, fetchRange) {
147
+ const missing = [];
148
+ for (const [nodeId, localSeq] of localVector) {
149
+ const serverSeq = serverVector.get(nodeId) ?? 0;
150
+ if (localSeq > serverSeq) {
151
+ const ops = await fetchRange(nodeId, serverSeq + 1, localSeq);
152
+ missing.push(...ops);
302
153
  }
303
- if (MUTATION_NAME_KEY in parsed) {
304
- mutationName = parsed[MUTATION_NAME_KEY];
305
- }
306
- const { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed;
307
- data = Object.keys(rest).length > 0 ? rest : null;
308
154
  }
309
- return {
310
- id: row.id,
311
- nodeId: row.node_id,
312
- type: row.type,
313
- collection: "",
314
- // Collection name is derived from the table name by the caller
315
- recordId: row.record_id,
316
- data,
317
- previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
318
- timestamp: HybridLogicalClock.deserialize(row.timestamp),
319
- sequenceNumber: row.sequence_number,
320
- causalDeps: JSON.parse(row.causal_deps),
321
- schemaVersion: row.schema_version,
322
- ...atomicOps !== void 0 ? { atomicOps } : {},
323
- ...transactionId !== void 0 ? { transactionId } : {},
324
- ...mutationName !== void 0 ? { mutationName } : {}
325
- };
326
- }
327
- function deserializeOperationWithCollection(row, collection) {
328
- const op = deserializeOperation(row);
329
- return { ...op, collection };
155
+ missing.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
156
+ return missing;
330
157
  }
331
- function serializeValue(value, descriptor) {
332
- if (value === null || value === void 0) {
333
- return null;
158
+ async function loadLastAckedServerVector(adapter) {
159
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
160
+ LAST_ACKED_SERVER_VECTOR_META_KEY
161
+ ]);
162
+ const row = rows[0];
163
+ if (!row?.value) {
164
+ return createVersionVector2();
334
165
  }
335
- switch (descriptor.kind) {
336
- case "boolean":
337
- return value ? 1 : 0;
338
- case "array":
339
- return JSON.stringify(value);
340
- case "richtext":
341
- return encodeRichtext(value);
342
- default:
343
- return value;
166
+ try {
167
+ return deserializeVersionVectorFromMeta(row.value);
168
+ } catch {
169
+ return createVersionVector2();
344
170
  }
345
171
  }
346
- function deserializeValue(value, descriptor) {
347
- switch (descriptor.kind) {
348
- case "boolean":
349
- return value === 1 || value === true;
350
- case "array":
351
- if (typeof value === "string") {
352
- return JSON.parse(value);
353
- }
354
- return value;
355
- case "richtext":
356
- return decodeRichtext(value);
357
- default:
358
- return value;
359
- }
172
+ async function saveLastAckedServerVector(adapter, vector) {
173
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
174
+ LAST_ACKED_SERVER_VECTOR_META_KEY,
175
+ serializeVersionVectorToMeta(vector)
176
+ ]);
360
177
  }
361
-
362
- // src/state-machine/state-validator.ts
363
- import { KoraError, validateTransition } from "@korajs/core";
364
- var InvalidStateTransitionError = class extends KoraError {
365
- constructor(collection, recordId, field, fromState, toState, allowedStates) {
366
- super(
367
- `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)"}`,
368
- "INVALID_STATE_TRANSITION",
369
- { collection, recordId, field, fromState, toState, allowedStates }
370
- );
371
- this.collection = collection;
372
- this.recordId = recordId;
373
- this.field = field;
374
- this.fromState = fromState;
375
- this.toState = toState;
376
- this.allowedStates = allowedStates;
377
- this.name = "InvalidStateTransitionError";
378
- }
379
- collection;
380
- recordId;
381
- field;
382
- fromState;
383
- toState;
384
- allowedStates;
385
- };
386
- function validateStateTransition(collectionName, recordId, stateMachine, currentState, newState) {
387
- if (currentState === null) {
388
- return { valid: true, allowedStates: [] };
389
- }
390
- if (currentState === newState) {
391
- return { valid: true, allowedStates: stateMachine.transitions[currentState] ?? [] };
392
- }
393
- const constraint = {
394
- field: stateMachine.field,
395
- collection: collectionName,
396
- transitions: stateMachine.transitions
397
- };
398
- const transitionResult = validateTransition(constraint, currentState, newState);
399
- if (transitionResult.valid) {
400
- return { valid: true, allowedStates: transitionResult.allowedTargets };
401
- }
402
- const allowedStates = transitionResult.allowedTargets;
403
- if (stateMachine.onInvalidTransition === "reject") {
404
- throw new InvalidStateTransitionError(
405
- collectionName,
406
- recordId,
407
- stateMachine.field,
408
- currentState,
409
- newState,
410
- allowedStates
411
- );
412
- }
413
- return { valid: false, allowedStates };
178
+ async function loadDeltaCursor(adapter) {
179
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
180
+ DELTA_CURSOR_META_KEY
181
+ ]);
182
+ return rows[0]?.value ?? null;
414
183
  }
415
- function validateUpdateStateMachine(collectionName, recordId, collectionDef, currentRecord, updateData) {
416
- const stateMachine = collectionDef.stateMachine;
417
- if (stateMachine === void 0) {
418
- return updateData;
419
- }
420
- const stateField = stateMachine.field;
421
- if (!(stateField in updateData)) {
422
- return updateData;
423
- }
424
- const currentState = currentRecord[stateField];
425
- const newState = updateData[stateField];
426
- if (typeof currentState !== "string" || typeof newState !== "string") {
427
- return updateData;
428
- }
429
- const result = validateStateTransition(
430
- collectionName,
431
- recordId,
432
- stateMachine,
433
- currentState,
434
- newState
435
- );
436
- if (result.valid) {
437
- return updateData;
438
- }
439
- const filtered = { ...updateData };
440
- delete filtered[stateField];
441
- return filtered;
184
+ async function saveDeltaCursor(adapter, cursor) {
185
+ if (cursor === null) {
186
+ await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [DELTA_CURSOR_META_KEY]);
187
+ return;
188
+ }
189
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
190
+ DELTA_CURSOR_META_KEY,
191
+ cursor
192
+ ]);
442
193
  }
443
194
 
195
+ // src/store/store.ts
196
+ import {
197
+ CausalTracker,
198
+ HybridLogicalClock as HybridLogicalClock2,
199
+ createVersionVector as createVersionVector3,
200
+ generateUUIDv7 as generateUUIDv73,
201
+ migrationStepsToSQL
202
+ } from "@korajs/core";
203
+
444
204
  // src/collection/collection.ts
445
205
  var Collection = class {
446
- constructor(name, definition, schema, adapter, clock, nodeId, getSequenceNumber, onMutation, relationEnforcer) {
206
+ constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
447
207
  this.name = name;
448
208
  this.definition = definition;
449
209
  this.schema = schema;
450
210
  this.adapter = adapter;
451
211
  this.clock = clock;
452
212
  this.nodeId = nodeId;
453
- this.getSequenceNumber = getSequenceNumber;
213
+ this.allocateSequenceNumber = allocateSequenceNumber;
454
214
  this.onMutation = onMutation;
455
- this.relationEnforcer = relationEnforcer ?? null;
215
+ this.relationEnforcer = relationEnforcer;
216
+ this.mutationHandler = mutationHandler;
217
+ this.causalTracker = causalTracker;
456
218
  }
457
219
  name;
458
220
  definition;
@@ -460,72 +222,32 @@ var Collection = class {
460
222
  adapter;
461
223
  clock;
462
224
  nodeId;
463
- getSequenceNumber;
225
+ allocateSequenceNumber;
464
226
  onMutation;
465
227
  relationEnforcer;
466
- /**
467
- * Insert a new record into the collection.
468
- * Generates a UUID v7 for the id, validates data, and persists atomically.
469
- *
470
- * @param data - The record data (auto fields and defaults are applied automatically)
471
- * @returns The inserted record with id, createdAt, updatedAt
472
- */
473
- async insert(data) {
474
- const validated = validateRecord(this.name, this.definition, data, "insert");
475
- const recordId = generateUUIDv7();
476
- const now = Date.now();
477
- for (const [fieldName, descriptor] of Object.entries(this.definition.fields)) {
478
- if (descriptor.auto && descriptor.kind === "timestamp") {
479
- validated[fieldName] = now;
480
- }
481
- }
482
- const sequenceNumber = this.getSequenceNumber();
483
- const operation = await createOperation(
484
- {
485
- nodeId: this.nodeId,
486
- type: "insert",
487
- collection: this.name,
488
- recordId,
489
- data: { ...validated },
490
- previousData: null,
491
- sequenceNumber,
492
- causalDeps: [],
493
- schemaVersion: this.schema.version
494
- },
495
- this.clock
496
- );
497
- const serializedData = serializeRecord(validated, this.definition.fields);
498
- const record = {
499
- id: recordId,
500
- ...serializedData,
501
- _created_at: now,
502
- _updated_at: now
503
- };
504
- const insertQuery = buildInsertQuery(this.name, record);
505
- const opRow = serializeOperation(operation);
506
- const opInsert = buildInsertQuery(
507
- `_kora_ops_${this.name}`,
508
- opRow
509
- );
510
- await this.adapter.transaction(async (tx) => {
511
- await tx.execute(insertQuery.sql, insertQuery.params);
512
- await tx.execute(opInsert.sql, opInsert.params);
513
- await tx.execute(
514
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
515
- [this.nodeId, sequenceNumber]
516
- );
517
- });
518
- this.onMutation(this.name, operation);
228
+ mutationHandler;
229
+ causalTracker;
230
+ mutationContext() {
519
231
  return {
520
- id: recordId,
521
- ...validated,
522
- createdAt: now,
523
- updatedAt: now
232
+ collection: this.name,
233
+ definition: this.definition,
234
+ schema: this.schema,
235
+ adapter: this.adapter,
236
+ clock: this.clock,
237
+ nodeId: this.nodeId,
238
+ allocateSequenceNumber: this.allocateSequenceNumber,
239
+ onMutation: this.onMutation,
240
+ relationEnforcer: this.relationEnforcer,
241
+ causalTracker: this.causalTracker,
242
+ inTransaction: false
524
243
  };
525
244
  }
526
- /**
527
- * Find a record by its ID. Returns null if not found or soft-deleted.
528
- */
245
+ async insert(data) {
246
+ if (this.mutationHandler) {
247
+ return this.mutationHandler.insert(this.name, data);
248
+ }
249
+ return executeInsert(this.mutationContext(), data);
250
+ }
529
251
  async findById(id) {
530
252
  const rows = await this.adapter.query(
531
253
  `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
@@ -535,173 +257,29 @@ var Collection = class {
535
257
  if (!row) return null;
536
258
  return deserializeRecord(row, this.definition.fields);
537
259
  }
538
- /**
539
- * Update an existing record. Only the provided fields are changed.
540
- *
541
- * @param id - The record ID to update
542
- * @param data - Partial data with only the fields to change
543
- * @returns The updated record
544
- * @throws {RecordNotFoundError} If the record doesn't exist or is deleted
545
- */
546
260
  async update(id, data) {
547
- const currentRows = await this.adapter.query(
548
- `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
549
- [id]
550
- );
551
- const currentRow = currentRows[0];
552
- if (!currentRow) {
553
- throw new RecordNotFoundError(this.name, id);
554
- }
555
- let validated = validateRecord(this.name, this.definition, data, "update");
556
- const now = Date.now();
557
- const currentRecord = deserializeRecord(currentRow, this.definition.fields);
558
- validated = validateUpdateStateMachine(
559
- this.name,
560
- id,
561
- this.definition,
562
- currentRecord,
563
- validated
564
- );
565
- if (Object.keys(validated).length === 0) {
566
- const fullRecord = await this.findById(id);
567
- if (!fullRecord) {
568
- throw new RecordNotFoundError(this.name, id);
569
- }
570
- return fullRecord;
571
- }
572
- const previousData = {};
573
- const resolvedData = {};
574
- const atomicOps = {};
575
- for (const key of Object.keys(validated)) {
576
- const value = validated[key];
577
- previousData[key] = currentRecord[key];
578
- if (isAtomicOp(value)) {
579
- resolvedData[key] = resolveAtomicOp(currentRecord[key], value);
580
- atomicOps[key] = toAtomicOp(value);
581
- } else {
582
- resolvedData[key] = value;
583
- }
261
+ if (this.mutationHandler) {
262
+ return this.mutationHandler.update(this.name, id, data);
584
263
  }
585
- const hasAtomicOps = Object.keys(atomicOps).length > 0;
586
- const sequenceNumber = this.getSequenceNumber();
587
- const operation = await createOperation(
588
- {
589
- nodeId: this.nodeId,
590
- type: "update",
591
- collection: this.name,
592
- recordId: id,
593
- data: { ...resolvedData },
594
- previousData,
595
- sequenceNumber,
596
- causalDeps: [],
597
- schemaVersion: this.schema.version,
598
- ...hasAtomicOps ? { atomicOps } : {}
599
- },
600
- this.clock
601
- );
602
- const serializedChanges = serializeRecord(resolvedData, this.definition.fields);
603
- const updateQuery = buildUpdateQuery(this.name, id, {
604
- ...serializedChanges,
605
- _updated_at: now
606
- });
607
- const opRow = serializeOperation(operation);
608
- const opInsert = buildInsertQuery(
609
- `_kora_ops_${this.name}`,
610
- opRow
611
- );
612
- await this.adapter.transaction(async (tx) => {
613
- await tx.execute(updateQuery.sql, updateQuery.params);
614
- await tx.execute(opInsert.sql, opInsert.params);
615
- await tx.execute(
616
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
617
- [this.nodeId, sequenceNumber]
618
- );
619
- });
620
- this.onMutation(this.name, operation);
621
- const updatedRow = await this.findById(id);
622
- if (!updatedRow) {
623
- throw new RecordNotFoundError(this.name, id);
624
- }
625
- return updatedRow;
264
+ return executeUpdate(this.mutationContext(), id, data);
626
265
  }
627
- /**
628
- * Soft-delete a record by its ID.
629
- *
630
- * When a RelationEnforcer is configured, referential integrity policies
631
- * are enforced within the same transaction:
632
- * - **cascade**: Recursively deletes all referencing records
633
- * - **set-null**: Sets foreign keys to null on referencing records
634
- * - **restrict**: Throws ReferentialIntegrityError if references exist
635
- * - **no-action**: Does nothing (dangling references are left)
636
- *
637
- * All cascaded operations are created atomically within the same transaction
638
- * and share causal dependencies with the original delete.
639
- *
640
- * @param id - The record ID to delete
641
- * @throws {RecordNotFoundError} If the record doesn't exist or is already deleted
642
- * @throws {ReferentialIntegrityError} If a 'restrict' policy is violated
643
- */
644
266
  async delete(id) {
645
- const currentRows = await this.adapter.query(
646
- `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
647
- [id]
648
- );
649
- if (!currentRows[0]) {
650
- throw new RecordNotFoundError(this.name, id);
651
- }
652
- const now = Date.now();
653
- const sequenceNumber = this.getSequenceNumber();
654
- const operation = await createOperation(
655
- {
656
- nodeId: this.nodeId,
657
- type: "delete",
658
- collection: this.name,
659
- recordId: id,
660
- data: null,
661
- previousData: null,
662
- sequenceNumber,
663
- causalDeps: [],
664
- schemaVersion: this.schema.version
665
- },
666
- this.clock
667
- );
668
- const deleteQuery = buildSoftDeleteQuery(this.name, id, now);
669
- const opRow = serializeOperation(operation);
670
- const opInsert = buildInsertQuery(
671
- `_kora_ops_${this.name}`,
672
- opRow
673
- );
674
- const cascadedOps = [];
675
- await this.adapter.transaction(async (tx) => {
676
- if (this.relationEnforcer) {
677
- const enforcementResult = await this.relationEnforcer.enforceDelete(
678
- this.name,
679
- id,
680
- tx,
681
- [operation.id]
682
- );
683
- cascadedOps.push(...enforcementResult.operations);
684
- }
685
- await tx.execute(deleteQuery.sql, deleteQuery.params);
686
- await tx.execute(opInsert.sql, opInsert.params);
687
- await tx.execute(
688
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
689
- [this.nodeId, sequenceNumber]
690
- );
691
- });
692
- this.onMutation(this.name, operation);
693
- for (const cascadedOp of cascadedOps) {
694
- this.onMutation(cascadedOp.collection, cascadedOp);
267
+ if (this.mutationHandler) {
268
+ await this.mutationHandler.delete(this.name, id);
269
+ return;
695
270
  }
271
+ await executeDelete(this.mutationContext(), id);
696
272
  }
697
- /** Get the collection name */
698
273
  getName() {
699
274
  return this.name;
700
275
  }
701
- /** Get the collection definition */
702
276
  getDefinition() {
703
277
  return this.definition;
704
278
  }
279
+ /** Replace the mutation handler (e.g. after ApplyPipeline is wired in createApp). */
280
+ setMutationHandler(handler) {
281
+ this.mutationHandler = handler;
282
+ }
705
283
  };
706
284
 
707
285
  // src/query/pluralize.ts
@@ -998,7 +576,7 @@ var QueryError2 = class extends Error {
998
576
  };
999
577
 
1000
578
  // src/relations/relation-enforcer.ts
1001
- import { KoraError as KoraError2, createOperation as createOperation2 } from "@korajs/core";
579
+ import { KoraError, createOperation } from "@korajs/core";
1002
580
 
1003
581
  // src/relations/relation-lookup.ts
1004
582
  function buildRelationLookup(schema) {
@@ -1022,7 +600,7 @@ function getIncomingRelations(lookup, collection) {
1022
600
  }
1023
601
 
1024
602
  // src/relations/relation-enforcer.ts
1025
- var ReferentialIntegrityError = class extends KoraError2 {
603
+ var ReferentialIntegrityError = class extends KoraError {
1026
604
  constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
1027
605
  super(
1028
606
  `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.`,
@@ -1044,13 +622,11 @@ var RelationEnforcer = class {
1044
622
  adapter;
1045
623
  clock;
1046
624
  nodeId;
1047
- getSequenceNumber;
1048
625
  constructor(config) {
1049
626
  this.schema = config.schema;
1050
627
  this.adapter = config.adapter;
1051
628
  this.clock = config.clock;
1052
629
  this.nodeId = config.nodeId;
1053
- this.getSequenceNumber = config.getSequenceNumber;
1054
630
  this.lookup = buildRelationLookup(config.schema);
1055
631
  }
1056
632
  /**
@@ -1112,8 +688,8 @@ var RelationEnforcer = class {
1112
688
  const operations = [];
1113
689
  for (const row of referencingRows) {
1114
690
  const now = Date.now();
1115
- const sequenceNumber = this.getSequenceNumber();
1116
- const operation = await createOperation2(
691
+ const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
692
+ const operation = await createOperation(
1117
693
  {
1118
694
  nodeId: this.nodeId,
1119
695
  type: "delete",
@@ -1129,16 +705,11 @@ var RelationEnforcer = class {
1129
705
  );
1130
706
  const deleteQuery = buildSoftDeleteQuery(sourceCollection, row.id, now);
1131
707
  await tx.execute(deleteQuery.sql, deleteQuery.params);
1132
- const opRow = serializeOperation(operation);
1133
708
  const opInsert = buildInsertQuery(
1134
709
  `_kora_ops_${sourceCollection}`,
1135
- opRow
710
+ serializeOperation(operation)
1136
711
  );
1137
712
  await tx.execute(opInsert.sql, opInsert.params);
1138
- await tx.execute(
1139
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1140
- [this.nodeId, sequenceNumber]
1141
- );
1142
713
  operations.push(operation);
1143
714
  const cascadeResult = await this.enforceDelete(sourceCollection, row.id, tx, [operation.id]);
1144
715
  operations.push(...cascadeResult.operations);
@@ -1164,10 +735,10 @@ var RelationEnforcer = class {
1164
735
  const operations = [];
1165
736
  for (const row of referencingRows) {
1166
737
  const now = Date.now();
1167
- const sequenceNumber = this.getSequenceNumber();
738
+ const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
1168
739
  const updateData = { [foreignKeyField]: null };
1169
740
  const previousData = { [foreignKeyField]: deletedRecordId };
1170
- const operation = await createOperation2(
741
+ const operation = await createOperation(
1171
742
  {
1172
743
  nodeId: this.nodeId,
1173
744
  type: "update",
@@ -1193,10 +764,6 @@ var RelationEnforcer = class {
1193
764
  opRow
1194
765
  );
1195
766
  await tx.execute(opInsert.sql, opInsert.params);
1196
- await tx.execute(
1197
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1198
- [this.nodeId, sequenceNumber]
1199
- );
1200
767
  operations.push(operation);
1201
768
  }
1202
769
  return operations;
@@ -1231,6 +798,170 @@ var RelationEnforcer = class {
1231
798
  }
1232
799
  };
1233
800
 
801
+ // src/replay/replay-to.ts
802
+ import { OperationError } from "@korajs/core";
803
+ import { topologicalSort } from "@korajs/core/internal";
804
+ function collectCausalClosure(allOps, targetOperationId) {
805
+ const opMap = /* @__PURE__ */ new Map();
806
+ for (const op of allOps) {
807
+ opMap.set(op.id, op);
808
+ }
809
+ const target = opMap.get(targetOperationId);
810
+ if (!target) {
811
+ throw new OperationError(
812
+ `Operation "${targetOperationId}" not found in the local operation log`,
813
+ {
814
+ operationId: targetOperationId
815
+ }
816
+ );
817
+ }
818
+ const included = /* @__PURE__ */ new Set();
819
+ const stack = [targetOperationId];
820
+ while (stack.length > 0) {
821
+ const id = stack.pop();
822
+ if (id === void 0 || included.has(id)) {
823
+ continue;
824
+ }
825
+ included.add(id);
826
+ const op = opMap.get(id);
827
+ if (!op) {
828
+ continue;
829
+ }
830
+ for (const depId of op.causalDeps) {
831
+ if (opMap.has(depId)) {
832
+ stack.push(depId);
833
+ }
834
+ }
835
+ }
836
+ const subset = allOps.filter((op) => included.has(op.id));
837
+ return topologicalSort(subset);
838
+ }
839
+ function buildReplaySnapshot(schema, allOps, targetOperationId) {
840
+ const operationsApplied = collectCausalClosure(allOps, targetOperationId);
841
+ const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
842
+ if (!targetOperation) {
843
+ throw new OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
844
+ operationId: targetOperationId
845
+ });
846
+ }
847
+ const memory = /* @__PURE__ */ new Map();
848
+ for (const op of operationsApplied) {
849
+ applyOperationToMemory(memory, op, schema);
850
+ }
851
+ const collections = materializeCollections(schema, memory);
852
+ return {
853
+ targetOperation,
854
+ operationsApplied,
855
+ collections,
856
+ findRecord(collection, recordId) {
857
+ const colMap = memory.get(collection);
858
+ const record = colMap?.get(recordId);
859
+ if (!record || record.deleted) {
860
+ return null;
861
+ }
862
+ const definition = schema.collections[collection];
863
+ if (!definition) {
864
+ return null;
865
+ }
866
+ return toCollectionRecord(record, definition);
867
+ }
868
+ };
869
+ }
870
+ function applyOperationToMemory(state, op, schema) {
871
+ const definition = schema.collections[op.collection];
872
+ if (!definition) {
873
+ return;
874
+ }
875
+ let colMap = state.get(op.collection);
876
+ if (!colMap) {
877
+ colMap = /* @__PURE__ */ new Map();
878
+ state.set(op.collection, colMap);
879
+ }
880
+ const wallTime = op.timestamp.wallTime;
881
+ switch (op.type) {
882
+ case "insert": {
883
+ if (!op.data) {
884
+ return;
885
+ }
886
+ colMap.set(op.recordId, {
887
+ id: op.recordId,
888
+ fields: { ...op.data },
889
+ deleted: false,
890
+ createdAt: wallTime,
891
+ updatedAt: wallTime
892
+ });
893
+ break;
894
+ }
895
+ case "update": {
896
+ if (!op.data) {
897
+ return;
898
+ }
899
+ const existing = colMap.get(op.recordId);
900
+ if (existing && !existing.deleted) {
901
+ existing.fields = { ...existing.fields, ...op.data };
902
+ existing.updatedAt = wallTime;
903
+ break;
904
+ }
905
+ if (existing?.deleted) {
906
+ return;
907
+ }
908
+ colMap.set(op.recordId, {
909
+ id: op.recordId,
910
+ fields: { ...op.data },
911
+ deleted: false,
912
+ createdAt: wallTime,
913
+ updatedAt: wallTime
914
+ });
915
+ break;
916
+ }
917
+ case "delete": {
918
+ const existing = colMap.get(op.recordId);
919
+ if (existing) {
920
+ existing.deleted = true;
921
+ existing.updatedAt = wallTime;
922
+ return;
923
+ }
924
+ colMap.set(op.recordId, {
925
+ id: op.recordId,
926
+ fields: {},
927
+ deleted: true,
928
+ createdAt: wallTime,
929
+ updatedAt: wallTime
930
+ });
931
+ break;
932
+ }
933
+ }
934
+ }
935
+ function materializeCollections(schema, memory) {
936
+ const collections = {};
937
+ for (const [collectionName, definition] of Object.entries(schema.collections)) {
938
+ const colMap = memory.get(collectionName);
939
+ const records = [];
940
+ if (colMap) {
941
+ for (const record of colMap.values()) {
942
+ if (!record.deleted) {
943
+ records.push(toCollectionRecord(record, definition));
944
+ }
945
+ }
946
+ }
947
+ collections[collectionName] = records;
948
+ }
949
+ return collections;
950
+ }
951
+ function toCollectionRecord(record, definition) {
952
+ const result = {
953
+ id: record.id,
954
+ createdAt: record.createdAt,
955
+ updatedAt: record.updatedAt
956
+ };
957
+ for (const [fieldName] of Object.entries(definition.fields)) {
958
+ if (fieldName in record.fields) {
959
+ result[fieldName] = record.fields[fieldName];
960
+ }
961
+ }
962
+ return result;
963
+ }
964
+
1234
965
  // src/sequences/sequence-manager.ts
1235
966
  import { defaultSequenceFormat, formatSequenceValue } from "@korajs/core";
1236
967
  var SequenceManager = class {
@@ -1474,6 +1205,7 @@ var SubscriptionManager = class {
1474
1205
  subscriptions = /* @__PURE__ */ new Map();
1475
1206
  pendingCollections = /* @__PURE__ */ new Set();
1476
1207
  flushScheduled = false;
1208
+ onQuerySubscribed;
1477
1209
  // Bloom filter state
1478
1210
  bloomFilter = null;
1479
1211
  bloomDirty = false;
@@ -1490,6 +1222,7 @@ var SubscriptionManager = class {
1490
1222
  this.bloomThreshold = options?.bloomThreshold ?? DEFAULT_BLOOM_THRESHOLD;
1491
1223
  this.bloomExpectedItems = options?.bloomExpectedItems ?? DEFAULT_BLOOM_EXPECTED_ITEMS;
1492
1224
  this.bloomFalsePositiveRate = options?.bloomFalsePositiveRate ?? DEFAULT_BLOOM_FALSE_POSITIVE_RATE;
1225
+ this.onQuerySubscribed = options?.onQuerySubscribed;
1493
1226
  }
1494
1227
  /**
1495
1228
  * Register a new subscription.
@@ -1510,9 +1243,11 @@ var SubscriptionManager = class {
1510
1243
  };
1511
1244
  this.subscriptions.set(id, subscription);
1512
1245
  this.bloomDirty = true;
1246
+ const externalCleanup = this.onQuerySubscribed?.(descriptor);
1513
1247
  return () => {
1514
1248
  this.subscriptions.delete(id);
1515
1249
  this.bloomDirty = true;
1250
+ externalCleanup?.();
1516
1251
  };
1517
1252
  }
1518
1253
  /**
@@ -1533,6 +1268,7 @@ var SubscriptionManager = class {
1533
1268
  };
1534
1269
  this.subscriptions.set(id, subscription);
1535
1270
  this.bloomDirty = true;
1271
+ const externalCleanup = this.onQuerySubscribed?.(descriptor);
1536
1272
  executeFn().then((results) => {
1537
1273
  if (this.subscriptions.has(id)) {
1538
1274
  subscription.lastResults = results;
@@ -1542,6 +1278,7 @@ var SubscriptionManager = class {
1542
1278
  return () => {
1543
1279
  this.subscriptions.delete(id);
1544
1280
  this.bloomDirty = true;
1281
+ externalCleanup?.();
1545
1282
  };
1546
1283
  }
1547
1284
  /**
@@ -1677,10 +1414,7 @@ var SubscriptionManager = class {
1677
1414
  * any included collection dependencies.
1678
1415
  */
1679
1416
  rebuildBloomFilter() {
1680
- const filter = new SubscriptionBloomFilter(
1681
- this.bloomExpectedItems,
1682
- this.bloomFalsePositiveRate
1683
- );
1417
+ const filter = new SubscriptionBloomFilter(this.bloomExpectedItems, this.bloomFalsePositiveRate);
1684
1418
  for (const sub of this.subscriptions.values()) {
1685
1419
  filter.add(sub.descriptor.collection);
1686
1420
  if (sub.descriptor.includeCollections) {
@@ -1700,24 +1434,38 @@ var SubscriptionManager = class {
1700
1434
  });
1701
1435
  }
1702
1436
  /**
1703
- * Compare two result sets. Uses length check + JSON comparison as pragmatic approach.
1704
- * Sufficient for typical query results. Can be optimized to id-based diffing if profiling shows need.
1437
+ * Compare two result sets. Uses ID-based comparison first (same length, same IDs),
1438
+ * then deep comparison of remaining fields when IDs match.
1439
+ * Avoids JSON.stringify which is O(n) and fragile to field ordering differences.
1705
1440
  */
1706
1441
  resultsEqual(prev, next) {
1707
1442
  if (prev.length !== next.length) return false;
1708
1443
  if (prev.length === 0) return true;
1709
- return JSON.stringify(prev) === JSON.stringify(next);
1444
+ for (let i = 0; i < prev.length; i++) {
1445
+ if (prev[i]?.id !== next[i]?.id) return false;
1446
+ }
1447
+ for (let i = 0; i < prev.length; i++) {
1448
+ const a = prev[i];
1449
+ const b = next[i];
1450
+ const keysA = Object.keys(a);
1451
+ const keysB = Object.keys(b);
1452
+ if (keysA.length !== keysB.length) return false;
1453
+ for (const key of keysA) {
1454
+ if (a[key] !== b[key]) return false;
1455
+ }
1456
+ }
1457
+ return true;
1710
1458
  }
1711
1459
  };
1712
1460
 
1713
1461
  // src/transaction/transaction-context.ts
1714
1462
  import {
1715
- createOperation as createOperation3,
1716
- generateUUIDv7 as generateUUIDv72,
1717
- isAtomicOp as isAtomicOp2,
1718
- resolveAtomicOp as resolveAtomicOp2,
1719
- toAtomicOp as toAtomicOp2,
1720
- validateRecord as validateRecord2
1463
+ createOperation as createOperation2,
1464
+ generateUUIDv7,
1465
+ isAtomicOp,
1466
+ resolveAtomicOp,
1467
+ toAtomicOp,
1468
+ validateRecord
1721
1469
  } from "@korajs/core";
1722
1470
  var TransactionContext = class {
1723
1471
  transactionId;
@@ -1728,7 +1476,7 @@ var TransactionContext = class {
1728
1476
  config;
1729
1477
  constructor(config) {
1730
1478
  this.config = config;
1731
- this.transactionId = generateUUIDv72();
1479
+ this.transactionId = generateUUIDv7();
1732
1480
  }
1733
1481
  /**
1734
1482
  * Set a human-readable mutation name for this transaction.
@@ -1775,16 +1523,48 @@ var TransactionContext = class {
1775
1523
  if (this.buffer.length === 0) {
1776
1524
  return { operations: [], affectedCollections: /* @__PURE__ */ new Set() };
1777
1525
  }
1526
+ const handler = this.config.localMutationHandler;
1527
+ if (handler?.commitTransaction) {
1528
+ return handler.commitTransaction({
1529
+ entries: this.buffer.map((entry) => ({
1530
+ operation: entry.operation,
1531
+ commands: entry.commands,
1532
+ collection: entry.collection
1533
+ })),
1534
+ transactionId: this.transactionId,
1535
+ ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
1536
+ });
1537
+ }
1778
1538
  const operations = [];
1779
1539
  const affectedCollections = /* @__PURE__ */ new Set();
1780
1540
  await this.config.adapter.transaction(async (tx) => {
1781
1541
  for (const entry of this.buffer) {
1542
+ if (entry.operation.type === "delete" && this.config.relationEnforcer) {
1543
+ const cascadeResult = await this.config.relationEnforcer.enforceDelete(
1544
+ entry.collection,
1545
+ entry.operation.recordId,
1546
+ tx,
1547
+ [entry.operation.id]
1548
+ );
1549
+ for (const cascadedOp of cascadeResult.operations) {
1550
+ operations.push(cascadedOp);
1551
+ affectedCollections.add(cascadedOp.collection);
1552
+ this.config.causalTracker?.afterOperation(cascadedOp.collection, cascadedOp.id, true);
1553
+ }
1554
+ }
1782
1555
  for (const cmd of entry.commands) {
1783
1556
  await tx.execute(cmd.sql, cmd.params);
1784
1557
  }
1785
1558
  operations.push(entry.operation);
1786
1559
  affectedCollections.add(entry.collection);
1787
1560
  }
1561
+ const finalSeq = this.config.sequenceAllocator.getHighWaterMark();
1562
+ if (finalSeq > 0) {
1563
+ await tx.execute(
1564
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1565
+ [this.config.nodeId, finalSeq]
1566
+ );
1567
+ }
1788
1568
  });
1789
1569
  return { operations, affectedCollections };
1790
1570
  }
@@ -1811,16 +1591,17 @@ var TransactionContext = class {
1811
1591
  }
1812
1592
  async insert(collectionName, definition, data) {
1813
1593
  this.ensureActive();
1814
- const validated = validateRecord2(collectionName, definition, data, "insert");
1815
- const recordId = generateUUIDv72();
1594
+ const validated = validateRecord(collectionName, definition, data, "insert");
1595
+ const recordId = generateUUIDv7();
1816
1596
  const now = Date.now();
1817
1597
  for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
1818
1598
  if (descriptor.auto && descriptor.kind === "timestamp") {
1819
1599
  validated[fieldName] = now;
1820
1600
  }
1821
1601
  }
1822
- const sequenceNumber = this.config.getSequenceNumber();
1823
- const operation = await createOperation3(
1602
+ const sequenceNumber = await this.config.sequenceAllocator.allocate();
1603
+ const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
1604
+ const operation = await createOperation2(
1824
1605
  {
1825
1606
  nodeId: this.config.nodeId,
1826
1607
  type: "insert",
@@ -1829,19 +1610,22 @@ var TransactionContext = class {
1829
1610
  data: { ...validated },
1830
1611
  previousData: null,
1831
1612
  sequenceNumber,
1832
- causalDeps: [],
1613
+ causalDeps,
1833
1614
  schemaVersion: this.config.schema.version,
1834
1615
  transactionId: this.transactionId,
1835
1616
  ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
1836
1617
  },
1837
1618
  this.config.clock
1838
1619
  );
1620
+ this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
1839
1621
  const serializedData = serializeRecord(validated, definition.fields);
1622
+ const version = serializeRowVersion(operation.timestamp);
1840
1623
  const record = {
1841
1624
  id: recordId,
1842
1625
  ...serializedData,
1843
- _created_at: now,
1844
- _updated_at: now
1626
+ _created_at: operation.timestamp.wallTime,
1627
+ _updated_at: operation.timestamp.wallTime,
1628
+ _version: version
1845
1629
  };
1846
1630
  const insertQuery = buildInsertQuery(collectionName, record);
1847
1631
  const opRow = serializeOperation(operation);
@@ -1854,11 +1638,7 @@ var TransactionContext = class {
1854
1638
  collection: collectionName,
1855
1639
  commands: [
1856
1640
  { sql: insertQuery.sql, params: insertQuery.params },
1857
- { sql: opInsert.sql, params: opInsert.params },
1858
- {
1859
- sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1860
- params: [this.config.nodeId, sequenceNumber]
1861
- }
1641
+ { sql: opInsert.sql, params: opInsert.params }
1862
1642
  ]
1863
1643
  });
1864
1644
  return {
@@ -1874,7 +1654,7 @@ var TransactionContext = class {
1874
1654
  if (!currentRecord) {
1875
1655
  throw new RecordNotFoundError(collectionName, id);
1876
1656
  }
1877
- const validated = validateRecord2(collectionName, definition, data, "update");
1657
+ const validated = validateRecord(collectionName, definition, data, "update");
1878
1658
  const now = Date.now();
1879
1659
  const previousData = {};
1880
1660
  const resolvedData = {};
@@ -1882,16 +1662,17 @@ var TransactionContext = class {
1882
1662
  for (const key of Object.keys(validated)) {
1883
1663
  const value = validated[key];
1884
1664
  previousData[key] = currentRecord[key];
1885
- if (isAtomicOp2(value)) {
1886
- resolvedData[key] = resolveAtomicOp2(currentRecord[key], value);
1887
- atomicOps[key] = toAtomicOp2(value);
1665
+ if (isAtomicOp(value)) {
1666
+ resolvedData[key] = resolveAtomicOp(currentRecord[key], value);
1667
+ atomicOps[key] = toAtomicOp(value);
1888
1668
  } else {
1889
1669
  resolvedData[key] = value;
1890
1670
  }
1891
1671
  }
1892
1672
  const hasAtomicOps = Object.keys(atomicOps).length > 0;
1893
- const sequenceNumber = this.config.getSequenceNumber();
1894
- const operation = await createOperation3(
1673
+ const sequenceNumber = await this.config.sequenceAllocator.allocate();
1674
+ const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
1675
+ const operation = await createOperation2(
1895
1676
  {
1896
1677
  nodeId: this.config.nodeId,
1897
1678
  type: "update",
@@ -1900,7 +1681,7 @@ var TransactionContext = class {
1900
1681
  data: { ...resolvedData },
1901
1682
  previousData,
1902
1683
  sequenceNumber,
1903
- causalDeps: [],
1684
+ causalDeps,
1904
1685
  schemaVersion: this.config.schema.version,
1905
1686
  transactionId: this.transactionId,
1906
1687
  ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {},
@@ -1908,10 +1689,13 @@ var TransactionContext = class {
1908
1689
  },
1909
1690
  this.config.clock
1910
1691
  );
1692
+ this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
1911
1693
  const serializedChanges = serializeRecord(resolvedData, definition.fields);
1694
+ const version = serializeRowVersion(operation.timestamp);
1912
1695
  const updateQuery = buildUpdateQuery(collectionName, id, {
1913
1696
  ...serializedChanges,
1914
- _updated_at: now
1697
+ _updated_at: operation.timestamp.wallTime,
1698
+ _version: version
1915
1699
  });
1916
1700
  const opRow = serializeOperation(operation);
1917
1701
  const opInsert = buildInsertQuery(
@@ -1923,11 +1707,7 @@ var TransactionContext = class {
1923
1707
  collection: collectionName,
1924
1708
  commands: [
1925
1709
  { sql: updateQuery.sql, params: updateQuery.params },
1926
- { sql: opInsert.sql, params: opInsert.params },
1927
- {
1928
- sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1929
- params: [this.config.nodeId, sequenceNumber]
1930
- }
1710
+ { sql: opInsert.sql, params: opInsert.params }
1931
1711
  ]
1932
1712
  });
1933
1713
  return {
@@ -1943,8 +1723,9 @@ var TransactionContext = class {
1943
1723
  throw new RecordNotFoundError(collectionName, id);
1944
1724
  }
1945
1725
  const now = Date.now();
1946
- const sequenceNumber = this.config.getSequenceNumber();
1947
- const operation = await createOperation3(
1726
+ const sequenceNumber = await this.config.sequenceAllocator.allocate();
1727
+ const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
1728
+ const operation = await createOperation2(
1948
1729
  {
1949
1730
  nodeId: this.config.nodeId,
1950
1731
  type: "delete",
@@ -1953,13 +1734,14 @@ var TransactionContext = class {
1953
1734
  data: null,
1954
1735
  previousData: null,
1955
1736
  sequenceNumber,
1956
- causalDeps: [],
1737
+ causalDeps,
1957
1738
  schemaVersion: this.config.schema.version,
1958
1739
  transactionId: this.transactionId,
1959
1740
  ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
1960
1741
  },
1961
1742
  this.config.clock
1962
1743
  );
1744
+ this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
1963
1745
  const deleteQuery = buildSoftDeleteQuery(collectionName, id, now);
1964
1746
  const opRow = serializeOperation(operation);
1965
1747
  const opInsert = buildInsertQuery(
@@ -1971,11 +1753,7 @@ var TransactionContext = class {
1971
1753
  collection: collectionName,
1972
1754
  commands: [
1973
1755
  { sql: deleteQuery.sql, params: deleteQuery.params },
1974
- { sql: opInsert.sql, params: opInsert.params },
1975
- {
1976
- sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1977
- params: [this.config.nodeId, sequenceNumber]
1978
- }
1756
+ { sql: opInsert.sql, params: opInsert.params }
1979
1757
  ]
1980
1758
  });
1981
1759
  }
@@ -2026,25 +1804,86 @@ var TransactionContext = class {
2026
1804
  }
2027
1805
  };
2028
1806
 
1807
+ // src/transaction/transaction-sequence.ts
1808
+ var TransactionSequenceAllocator = class {
1809
+ constructor(adapter, nodeId) {
1810
+ this.adapter = adapter;
1811
+ this.nodeId = nodeId;
1812
+ }
1813
+ adapter;
1814
+ nodeId;
1815
+ loaded = false;
1816
+ watermark = 0;
1817
+ async allocate() {
1818
+ if (!this.loaded) {
1819
+ this.watermark = await readSequenceNumber(this.adapter, this.nodeId);
1820
+ this.loaded = true;
1821
+ }
1822
+ this.watermark++;
1823
+ return this.watermark;
1824
+ }
1825
+ getHighWaterMark() {
1826
+ return this.watermark;
1827
+ }
1828
+ };
1829
+
1830
+ // src/store/tab-node-id.ts
1831
+ import { generateUUIDv7 as generateUUIDv72 } from "@korajs/core";
1832
+ function perTabStorageKey(dbName) {
1833
+ return `kora-tab-node-${dbName}`;
1834
+ }
1835
+ function loadPerTabNodeId(dbName) {
1836
+ if (typeof sessionStorage === "undefined") {
1837
+ return null;
1838
+ }
1839
+ return sessionStorage.getItem(perTabStorageKey(dbName));
1840
+ }
1841
+ function savePerTabNodeId(dbName, nodeId) {
1842
+ if (typeof sessionStorage === "undefined") {
1843
+ return;
1844
+ }
1845
+ sessionStorage.setItem(perTabStorageKey(dbName), nodeId);
1846
+ }
1847
+ function resolvePerTabNodeId(dbName) {
1848
+ const existing = loadPerTabNodeId(dbName);
1849
+ if (existing) {
1850
+ return existing;
1851
+ }
1852
+ const nodeId = generateUUIDv72();
1853
+ savePerTabNodeId(dbName, nodeId);
1854
+ return nodeId;
1855
+ }
1856
+
2029
1857
  // src/store/store.ts
2030
1858
  var Store = class {
2031
1859
  opened = false;
2032
1860
  nodeId = "";
2033
1861
  sequenceNumber = 0;
2034
- versionVector = createVersionVector();
1862
+ versionVector = createVersionVector3();
2035
1863
  clock = null;
2036
1864
  collections = /* @__PURE__ */ new Map();
2037
- subscriptionManager = new SubscriptionManager();
1865
+ subscriptionManager;
2038
1866
  sequenceManager = null;
2039
1867
  schema;
2040
1868
  adapter;
2041
1869
  configNodeId;
1870
+ dbName;
1871
+ isolation;
2042
1872
  emitter;
1873
+ localMutationHandler;
1874
+ relationEnforcer = null;
1875
+ causalTracker = null;
2043
1876
  constructor(config) {
2044
1877
  this.schema = config.schema;
2045
1878
  this.adapter = config.adapter;
2046
1879
  this.configNodeId = config.nodeId;
1880
+ this.dbName = config.dbName ?? "kora-db";
1881
+ this.isolation = config.isolation ?? "shared";
2047
1882
  this.emitter = config.emitter ?? null;
1883
+ this.localMutationHandler = config.localMutationHandler ?? null;
1884
+ this.subscriptionManager = new SubscriptionManager({
1885
+ onQuerySubscribed: config.onQuerySubscribed
1886
+ });
2048
1887
  }
2049
1888
  /**
2050
1889
  * Open the store: initialize the database, load or generate a node ID,
@@ -2055,17 +1894,17 @@ var Store = class {
2055
1894
  await this.runMigrationsIfNeeded();
2056
1895
  this.nodeId = await this.loadOrGenerateNodeId();
2057
1896
  this.clock = new HybridLogicalClock2(this.nodeId);
1897
+ this.causalTracker = new CausalTracker();
2058
1898
  this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
2059
1899
  this.sequenceNumber = await this.loadSequenceNumber();
2060
1900
  this.versionVector = await this.loadVersionVector();
2061
1901
  const hasRelations = Object.keys(this.schema.relations).length > 0;
2062
- const relationEnforcer = hasRelations ? new RelationEnforcer({
1902
+ this.relationEnforcer = hasRelations ? new RelationEnforcer({
2063
1903
  schema: this.schema,
2064
1904
  adapter: this.adapter,
2065
1905
  clock: this.clock,
2066
- nodeId: this.nodeId,
2067
- getSequenceNumber: () => this.nextSequenceNumber()
2068
- }) : void 0;
1906
+ nodeId: this.nodeId
1907
+ }) : null;
2069
1908
  for (const [name, definition] of Object.entries(this.schema.collections)) {
2070
1909
  const col = new Collection(
2071
1910
  name,
@@ -2074,14 +1913,17 @@ var Store = class {
2074
1913
  this.adapter,
2075
1914
  this.clock,
2076
1915
  this.nodeId,
2077
- () => this.nextSequenceNumber(),
1916
+ () => this.allocateSequenceNumber(),
2078
1917
  (collectionName, operation) => {
1918
+ this.recordOperationSequence(operation);
2079
1919
  this.subscriptionManager.notify(collectionName, operation);
2080
1920
  if (this.emitter) {
2081
1921
  this.emitter.emit({ type: "operation:created", operation });
2082
1922
  }
2083
1923
  },
2084
- relationEnforcer
1924
+ this.relationEnforcer,
1925
+ this.localMutationHandler,
1926
+ this.causalTracker
2085
1927
  );
2086
1928
  this.collections.set(name, col);
2087
1929
  }
@@ -2147,7 +1989,7 @@ var Store = class {
2147
1989
  * Checks for duplicates, applies to the data table, persists the operation,
2148
1990
  * and updates the version vector.
2149
1991
  */
2150
- async applyRemoteOperation(op) {
1992
+ async applyRemoteOperation(op, options) {
2151
1993
  this.ensureOpen();
2152
1994
  const collection = op.collection;
2153
1995
  const definition = this.schema.collections[collection];
@@ -2164,27 +2006,52 @@ var Store = class {
2164
2006
  if (this.clock) {
2165
2007
  this.clock.receive(op.timestamp);
2166
2008
  }
2009
+ const remoteVersion = serializeRowVersion(op.timestamp);
2010
+ const wallTime = op.timestamp.wallTime;
2167
2011
  await this.adapter.transaction(async (tx) => {
2168
2012
  if (op.type === "insert" && op.data) {
2169
- const serializedData = serializeRecord(op.data, definition.fields);
2170
- const now = op.timestamp.wallTime;
2171
- const record = {
2172
- id: op.recordId,
2173
- ...serializedData,
2174
- _created_at: now,
2175
- _updated_at: now
2176
- };
2177
- const insertQuery = buildInsertQuery(collection, record);
2178
- await tx.execute(insertQuery.sql, insertQuery.params);
2013
+ const existing2 = await tx.query(
2014
+ `SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
2015
+ [op.recordId]
2016
+ );
2017
+ const row = existing2[0];
2018
+ if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
2019
+ } else {
2020
+ const serializedData = serializeRecord(op.data, definition.fields);
2021
+ const record = {
2022
+ id: op.recordId,
2023
+ ...serializedData,
2024
+ _created_at: wallTime,
2025
+ _updated_at: wallTime,
2026
+ _version: remoteVersion
2027
+ };
2028
+ const insertQuery = buildInsertQuery(collection, record);
2029
+ await tx.execute(insertQuery.sql, insertQuery.params);
2030
+ }
2179
2031
  } else if (op.type === "update" && op.data) {
2180
2032
  const serializedChanges = serializeRecord(op.data, definition.fields);
2181
- const updateQuery = buildUpdateQuery(collection, op.recordId, {
2033
+ const updatePayload = {
2182
2034
  ...serializedChanges,
2183
- _updated_at: op.timestamp.wallTime
2184
- });
2035
+ _updated_at: wallTime,
2036
+ _version: remoteVersion
2037
+ };
2038
+ if (options?.reactivateIfDeleted) {
2039
+ updatePayload._deleted = 0;
2040
+ }
2041
+ const updateQuery = buildLwwUpdateQuery(
2042
+ collection,
2043
+ op.recordId,
2044
+ updatePayload,
2045
+ remoteVersion
2046
+ );
2185
2047
  await tx.execute(updateQuery.sql, updateQuery.params);
2186
2048
  } else if (op.type === "delete") {
2187
- const deleteQuery = buildSoftDeleteQuery(collection, op.recordId, op.timestamp.wallTime);
2049
+ const deleteQuery = buildLwwSoftDeleteQuery(
2050
+ collection,
2051
+ op.recordId,
2052
+ wallTime,
2053
+ remoteVersion
2054
+ );
2188
2055
  await tx.execute(deleteQuery.sql, deleteQuery.params);
2189
2056
  }
2190
2057
  const opRow = serializeOperation(op);
@@ -2209,11 +2076,11 @@ var Store = class {
2209
2076
  * Get operations from a node within a sequence number range.
2210
2077
  * Implements the OperationLog interface for computeDelta.
2211
2078
  */
2212
- getRange(nodeId, fromSeq, toSeq) {
2213
- return [];
2079
+ async getRange(nodeId, fromSeq, toSeq) {
2080
+ return this.getOperationRange(nodeId, fromSeq, toSeq);
2214
2081
  }
2215
2082
  /**
2216
- * Async version of getRange for use by the sync layer.
2083
+ * Get operations from a node within a sequence number range.
2217
2084
  */
2218
2085
  async getOperationRange(nodeId, fromSeq, toSeq) {
2219
2086
  this.ensureOpen();
@@ -2230,12 +2097,233 @@ var Store = class {
2230
2097
  allOps.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
2231
2098
  return allOps;
2232
2099
  }
2100
+ /**
2101
+ * Load every operation from the local append-only log across all collections.
2102
+ * Used by sync delta computation, backup export, and time-travel replay.
2103
+ */
2104
+ async getAllOperations() {
2105
+ this.ensureOpen();
2106
+ const allOps = [];
2107
+ for (const collectionName of Object.keys(this.schema.collections)) {
2108
+ const rows = await this.adapter.query(
2109
+ `SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
2110
+ );
2111
+ for (const row of rows) {
2112
+ allOps.push(deserializeOperationWithCollection(row, collectionName));
2113
+ }
2114
+ }
2115
+ return allOps;
2116
+ }
2117
+ /**
2118
+ * Rebuild an in-memory snapshot of materialized state at a causal cut in the op log.
2119
+ * Does not mutate the live store — intended for DevTools time-travel inspection.
2120
+ *
2121
+ * @param operationId - Content-addressed id of the operation to replay through (inclusive)
2122
+ * @throws {OperationError} When the operation id is not present in the local log
2123
+ */
2124
+ async replayTo(operationId) {
2125
+ this.ensureOpen();
2126
+ const start = Date.now();
2127
+ const allOps = await this.getAllOperations();
2128
+ const snapshot = buildReplaySnapshot(this.schema, allOps, operationId);
2129
+ if (this.emitter) {
2130
+ this.emitter.emit({
2131
+ type: "replay:completed",
2132
+ targetOperationId: operationId,
2133
+ operationsApplied: snapshot.operationsApplied.length,
2134
+ duration: Date.now() - start
2135
+ });
2136
+ }
2137
+ return snapshot;
2138
+ }
2139
+ /**
2140
+ * Persist a merge trace to the durable audit log.
2141
+ */
2142
+ async appendAuditTrace(trace) {
2143
+ this.ensureOpen();
2144
+ const { appendAuditTrace: append } = await import("./audit-trace-store-AM4EWQNZ.js");
2145
+ await append(this.adapter, trace);
2146
+ }
2147
+ /**
2148
+ * Read persisted audit traces with optional filters.
2149
+ */
2150
+ async getAuditTraces(query) {
2151
+ this.ensureOpen();
2152
+ const { readAuditTraces: readAuditTraces2 } = await import("./audit-trace-store-AM4EWQNZ.js");
2153
+ return readAuditTraces2(this.adapter, query);
2154
+ }
2155
+ /**
2156
+ * Export operations and merge traces as a portable audit bundle.
2157
+ */
2158
+ async exportAudit(options) {
2159
+ this.ensureOpen();
2160
+ const { exportAudit: doExport } = await import("./export-audit-5GV6VRDZ.js");
2161
+ return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2162
+ }
2233
2163
  /**
2234
2164
  * Get the schema definition.
2235
2165
  */
2236
2166
  getSchema() {
2237
2167
  return this.schema;
2238
2168
  }
2169
+ /**
2170
+ * Route local CRUD through the unified apply pipeline (korajs ApplyPipeline).
2171
+ */
2172
+ setLocalMutationHandler(handler) {
2173
+ this.localMutationHandler = handler;
2174
+ for (const col of this.collections.values()) {
2175
+ col.setMutationHandler(handler);
2176
+ }
2177
+ }
2178
+ /**
2179
+ * Build mutation context for a collection (used by ApplyPipeline side effects).
2180
+ */
2181
+ createMutationContext(collection, options) {
2182
+ this.ensureOpen();
2183
+ const definition = this.schema.collections[collection];
2184
+ if (!definition || !this.clock) {
2185
+ throw new StoreNotOpenError();
2186
+ }
2187
+ return {
2188
+ collection,
2189
+ definition,
2190
+ schema: this.schema,
2191
+ adapter: this.adapter,
2192
+ clock: this.clock,
2193
+ nodeId: this.nodeId,
2194
+ allocateSequenceNumber: () => this.allocateSequenceNumber(),
2195
+ onMutation: (collectionName, operation) => {
2196
+ this.recordOperationSequence(operation);
2197
+ this.subscriptionManager.notify(collectionName, operation);
2198
+ if (this.emitter) {
2199
+ this.emitter.emit({ type: "operation:created", operation });
2200
+ }
2201
+ },
2202
+ relationEnforcer: this.relationEnforcer,
2203
+ causalTracker: this.causalTracker,
2204
+ inTransaction: options?.inTransaction ?? false,
2205
+ extraCausalDeps: options?.extraCausalDeps
2206
+ };
2207
+ }
2208
+ /**
2209
+ * Load a materialized row by ID, including soft-deleted tombstones.
2210
+ */
2211
+ async findMaterializedRow(collection, recordId) {
2212
+ this.ensureOpen();
2213
+ const definition = this.schema.collections[collection];
2214
+ if (!definition) {
2215
+ return null;
2216
+ }
2217
+ const rows = await this.adapter.query(
2218
+ `SELECT * FROM ${collection} WHERE id = ?`,
2219
+ [recordId]
2220
+ );
2221
+ const row = rows[0];
2222
+ if (!row) {
2223
+ return null;
2224
+ }
2225
+ return {
2226
+ record: deserializeRecord(row, definition.fields),
2227
+ deleted: row._deleted === 1
2228
+ };
2229
+ }
2230
+ /**
2231
+ * Latest operation from this device for a record (used for delete-vs-update merge on sync).
2232
+ */
2233
+ /**
2234
+ * Load the last server version vector acknowledged by this client (persisted in `_kora_meta`).
2235
+ */
2236
+ async loadLastAckedServerVector() {
2237
+ this.ensureOpen();
2238
+ return loadLastAckedServerVector(this.adapter);
2239
+ }
2240
+ /**
2241
+ * Persist the last server version vector this client believes the server has applied.
2242
+ */
2243
+ async saveLastAckedServerVector(vector) {
2244
+ this.ensureOpen();
2245
+ await saveLastAckedServerVector(this.adapter, vector);
2246
+ }
2247
+ /**
2248
+ * Load persisted delta cursor for resuming paginated initial sync.
2249
+ */
2250
+ async loadDeltaCursor() {
2251
+ this.ensureOpen();
2252
+ return loadDeltaCursor(this.adapter);
2253
+ }
2254
+ /**
2255
+ * Persist or clear the delta cursor for paginated initial sync resume.
2256
+ */
2257
+ async saveDeltaCursor(cursor) {
2258
+ this.ensureOpen();
2259
+ await saveDeltaCursor(this.adapter, cursor);
2260
+ }
2261
+ /**
2262
+ * Local operations not yet reflected on the server version vector.
2263
+ */
2264
+ async getUnsyncedOperations(serverVector) {
2265
+ this.ensureOpen();
2266
+ return collectOperationsAheadOfServer(
2267
+ this.getVersionVector(),
2268
+ serverVector,
2269
+ (nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
2270
+ );
2271
+ }
2272
+ /**
2273
+ * Count of local operations ahead of the server version vector.
2274
+ */
2275
+ async countUnsyncedOperations(serverVector) {
2276
+ const ops = await this.getUnsyncedOperations(serverVector);
2277
+ return ops.length;
2278
+ }
2279
+ /**
2280
+ * Compact the local operation log using materialized rows as the baseline.
2281
+ * Only removes ops the server has acknowledged (per {@link CompactionStrategy}).
2282
+ */
2283
+ async compact(strategy) {
2284
+ this.ensureOpen();
2285
+ if (strategy.mode === "never") {
2286
+ return compactOperationLog(this.adapter, this.schema, strategy, createVersionVector3());
2287
+ }
2288
+ const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
2289
+ return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
2290
+ }
2291
+ /**
2292
+ * Merge session remote vector with persisted last-acked vector (max per node).
2293
+ */
2294
+ mergeServerVectors(sessionVector, persistedVector) {
2295
+ return mergeVersionVectors(persistedVector, sessionVector);
2296
+ }
2297
+ async getLatestLocalOperationForRecord(collection, recordId) {
2298
+ this.ensureOpen();
2299
+ const rows = await this.adapter.query(
2300
+ `SELECT * FROM _kora_ops_${collection} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
2301
+ [this.nodeId, recordId]
2302
+ );
2303
+ const row = rows[0];
2304
+ if (!row) {
2305
+ return null;
2306
+ }
2307
+ return deserializeOperationWithCollection(row, collection);
2308
+ }
2309
+ /**
2310
+ * Latest operation for a record from any node (for 3-way merge when local op log is empty).
2311
+ */
2312
+ async getLatestOperationForRecord(collection, recordId) {
2313
+ this.ensureOpen();
2314
+ const rows = await this.adapter.query(
2315
+ `SELECT * FROM _kora_ops_${collection} WHERE record_id = ?`,
2316
+ [recordId]
2317
+ );
2318
+ let latest = null;
2319
+ for (const row of rows) {
2320
+ const op = deserializeOperationWithCollection(row, collection);
2321
+ if (!latest || HybridLogicalClock2.compare(op.timestamp, latest.timestamp) > 0) {
2322
+ latest = op;
2323
+ }
2324
+ }
2325
+ return latest;
2326
+ }
2239
2327
  /** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
2240
2328
  getSubscriptionManager() {
2241
2329
  return this.subscriptionManager;
@@ -2268,7 +2356,10 @@ var Store = class {
2268
2356
  adapter: this.adapter,
2269
2357
  clock: this.clock,
2270
2358
  nodeId: this.nodeId,
2271
- getSequenceNumber: () => this.nextSequenceNumber()
2359
+ sequenceAllocator: new TransactionSequenceAllocator(this.adapter, this.nodeId),
2360
+ relationEnforcer: this.relationEnforcer,
2361
+ causalTracker: this.causalTracker,
2362
+ localMutationHandler: this.localMutationHandler
2272
2363
  });
2273
2364
  }
2274
2365
  /**
@@ -2283,10 +2374,13 @@ var Store = class {
2283
2374
  */
2284
2375
  async transaction(fn) {
2285
2376
  const tx = this.createTransaction();
2377
+ this.causalTracker?.beginTransaction();
2286
2378
  try {
2287
2379
  await fn(tx);
2288
- const { operations, affectedCollections } = await tx.commit();
2380
+ const { operations } = await tx.commit();
2381
+ this.causalTracker?.clearTransaction();
2289
2382
  for (const op of operations) {
2383
+ this.recordOperationSequence(op);
2290
2384
  this.subscriptionManager.notify(op.collection, op);
2291
2385
  if (this.emitter) {
2292
2386
  this.emitter.emit({ type: "operation:created", operation: op });
@@ -2295,13 +2389,57 @@ var Store = class {
2295
2389
  return operations;
2296
2390
  } catch (error) {
2297
2391
  tx.rollback();
2392
+ this.causalTracker?.clearTransaction();
2298
2393
  throw error;
2299
2394
  }
2300
2395
  }
2301
- nextSequenceNumber() {
2302
- this.sequenceNumber++;
2303
- this.versionVector.set(this.nodeId, this.sequenceNumber);
2304
- return this.sequenceNumber;
2396
+ /**
2397
+ * Export all data as a portable backup binary.
2398
+ * Includes operations, version vector, metadata, and optionally materialized records.
2399
+ *
2400
+ * @param options - Backup options (includeRecords, collections, onProgress)
2401
+ * @returns Backup as a Uint8Array
2402
+ */
2403
+ async exportBackup(options) {
2404
+ this.ensureOpen();
2405
+ const { exportBackup: doExport } = await import("./backup-JN3LB4YT.js");
2406
+ return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2407
+ }
2408
+ /**
2409
+ * Restore data from a backup binary.
2410
+ *
2411
+ * @param data - The backup data
2412
+ * @param options - Restore options (merge, collections, onProgress)
2413
+ * @returns Result of the restore operation
2414
+ */
2415
+ async importBackup(data, options) {
2416
+ this.ensureOpen();
2417
+ const { restoreBackup: doRestore } = await import("./backup-JN3LB4YT.js");
2418
+ return doRestore(this.adapter, this.schema, data, options);
2419
+ }
2420
+ /**
2421
+ * Read backup manifest without loading the entire backup.
2422
+ *
2423
+ * @param data - The raw backup data
2424
+ * @returns The backup manifest
2425
+ */
2426
+ static readBackupManifest(data) {
2427
+ return readBackupManifest(data);
2428
+ }
2429
+ recordOperationSequence(operation) {
2430
+ const prev = this.versionVector.get(operation.nodeId) ?? 0;
2431
+ if (operation.sequenceNumber > prev) {
2432
+ this.versionVector.set(operation.nodeId, operation.sequenceNumber);
2433
+ }
2434
+ if (operation.nodeId === this.nodeId) {
2435
+ this.sequenceNumber = Math.max(this.sequenceNumber, operation.sequenceNumber);
2436
+ }
2437
+ }
2438
+ async allocateSequenceNumber() {
2439
+ const seq = await allocateNextSequenceNumber(this.adapter, this.nodeId);
2440
+ this.sequenceNumber = seq;
2441
+ this.versionVector.set(this.nodeId, seq);
2442
+ return seq;
2305
2443
  }
2306
2444
  /**
2307
2445
  * Check the stored schema version and run any pending migrations.
@@ -2384,12 +2522,17 @@ var Store = class {
2384
2522
  }
2385
2523
  async loadOrGenerateNodeId() {
2386
2524
  if (this.configNodeId) {
2387
- await this.adapter.execute(
2388
- "INSERT OR REPLACE INTO _kora_meta (key, value) VALUES ('node_id', ?)",
2389
- [this.configNodeId]
2390
- );
2525
+ if (this.isolation !== "per-tab") {
2526
+ await this.adapter.execute(
2527
+ "INSERT OR REPLACE INTO _kora_meta (key, value) VALUES ('node_id', ?)",
2528
+ [this.configNodeId]
2529
+ );
2530
+ }
2391
2531
  return this.configNodeId;
2392
2532
  }
2533
+ if (this.isolation === "per-tab") {
2534
+ return resolvePerTabNodeId(this.dbName);
2535
+ }
2393
2536
  const rows = await this.adapter.query(
2394
2537
  "SELECT value FROM _kora_meta WHERE key = 'node_id'"
2395
2538
  );
@@ -2413,7 +2556,7 @@ var Store = class {
2413
2556
  const rows = await this.adapter.query(
2414
2557
  "SELECT node_id, sequence_number FROM _kora_version_vector"
2415
2558
  );
2416
- const vector = createVersionVector();
2559
+ const vector = createVersionVector3();
2417
2560
  for (const row of rows) {
2418
2561
  vector.set(row.node_id, row.sequence_number);
2419
2562
  }
@@ -2427,8 +2570,10 @@ var Store = class {
2427
2570
  };
2428
2571
  export {
2429
2572
  AdapterError,
2573
+ COMPACTION_BASELINE_META_KEY,
2430
2574
  Collection,
2431
2575
  InvalidStateTransitionError,
2576
+ LAST_ACKED_SERVER_VECTOR_META_KEY,
2432
2577
  PersistenceError,
2433
2578
  QueryBuilder,
2434
2579
  QueryError,
@@ -2441,12 +2586,27 @@ export {
2441
2586
  TransactionContext,
2442
2587
  WorkerInitError,
2443
2588
  WorkerTimeoutError,
2589
+ collectOperationsAheadOfServer,
2590
+ compactOperationLog,
2591
+ computeAckCompactionWatermark,
2592
+ decodeAuditExport,
2444
2593
  decodeRichtext,
2594
+ deserializeVersionVectorFromMeta,
2445
2595
  encodeRichtext,
2596
+ exportBackup,
2597
+ mergeVersionVectors,
2598
+ persistedAuditTraceFromEvent,
2446
2599
  pluralize,
2600
+ readAuditExportManifest,
2601
+ readBackupManifest,
2602
+ restoreBackup,
2603
+ richtextStatesEqual,
2447
2604
  richtextToPlainText,
2605
+ serializeVersionVectorToMeta,
2448
2606
  singularize,
2449
2607
  validateStateTransition,
2450
- validateUpdateStateMachine
2608
+ validateUpdateStateMachine,
2609
+ verifyAuditExportChecksum,
2610
+ verifyBackupChecksum
2451
2611
  };
2452
2612
  //# sourceMappingURL=index.js.map