korajs 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-WALHLVVF.js +683 -0
- package/dist/chunk-WALHLVVF.js.map +1 -0
- package/dist/index.cjs +1090 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -7
- package/dist/index.d.ts +105 -7
- package/dist/index.js +450 -149
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +700 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +111 -0
- package/dist/testing.d.ts +111 -0
- package/dist/testing.js +13 -0
- package/dist/testing.js.map +1 -0
- package/package.json +19 -7
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
ApplyPipeline: () => ApplyPipeline,
|
|
24
|
+
MergeAwareSyncStore: () => MergeAwareSyncStore,
|
|
25
|
+
StoreQueueStorage: () => StoreQueueStorage,
|
|
26
|
+
StoreSyncStatePersistence: () => StoreSyncStatePersistence
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(testing_exports);
|
|
29
|
+
|
|
30
|
+
// src/apply-pipeline.ts
|
|
31
|
+
var import_core2 = require("@korajs/core");
|
|
32
|
+
var import_internal2 = require("@korajs/core/internal");
|
|
33
|
+
var import_merge = require("@korajs/merge");
|
|
34
|
+
var import_store = require("@korajs/store");
|
|
35
|
+
var import_internal3 = require("@korajs/store/internal");
|
|
36
|
+
|
|
37
|
+
// src/build-side-effect-entry.ts
|
|
38
|
+
var import_core = require("@korajs/core");
|
|
39
|
+
var import_internal = require("@korajs/store/internal");
|
|
40
|
+
async function buildSideEffectEntry(ctx, effect, parentOpId, transactionId, mutationName) {
|
|
41
|
+
const causalDeps = [parentOpId];
|
|
42
|
+
const baseInput = {
|
|
43
|
+
nodeId: ctx.nodeId,
|
|
44
|
+
collection: effect.collection,
|
|
45
|
+
recordId: effect.recordId,
|
|
46
|
+
sequenceNumber: await ctx.allocateSequenceNumber(),
|
|
47
|
+
causalDeps,
|
|
48
|
+
schemaVersion: ctx.schema.version,
|
|
49
|
+
...transactionId !== void 0 ? { transactionId } : {},
|
|
50
|
+
...mutationName !== void 0 ? { mutationName } : {}
|
|
51
|
+
};
|
|
52
|
+
if (effect.type === "delete") {
|
|
53
|
+
const operation2 = await (0, import_core.createOperation)(
|
|
54
|
+
{
|
|
55
|
+
...baseInput,
|
|
56
|
+
type: "delete",
|
|
57
|
+
data: null,
|
|
58
|
+
previousData: effect.previousData
|
|
59
|
+
},
|
|
60
|
+
ctx.clock
|
|
61
|
+
);
|
|
62
|
+
ctx.causalTracker?.afterOperation(effect.collection, operation2.id, ctx.inTransaction);
|
|
63
|
+
const version2 = (0, import_internal.serializeRowVersion)(operation2.timestamp);
|
|
64
|
+
const deleteQuery = (0, import_internal.buildSoftDeleteQuery)(
|
|
65
|
+
effect.collection,
|
|
66
|
+
effect.recordId,
|
|
67
|
+
operation2.timestamp.wallTime,
|
|
68
|
+
version2
|
|
69
|
+
);
|
|
70
|
+
const opInsert2 = (0, import_internal.buildInsertQuery)(
|
|
71
|
+
`_kora_ops_${effect.collection}`,
|
|
72
|
+
(0, import_internal.serializeOperation)(operation2)
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
operation: operation2,
|
|
76
|
+
collection: effect.collection,
|
|
77
|
+
commands: [
|
|
78
|
+
{ sql: deleteQuery.sql, params: deleteQuery.params },
|
|
79
|
+
{ sql: opInsert2.sql, params: opInsert2.params },
|
|
80
|
+
{
|
|
81
|
+
sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
82
|
+
params: [ctx.nodeId, operation2.sequenceNumber]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const operation = await (0, import_core.createOperation)(
|
|
88
|
+
{
|
|
89
|
+
...baseInput,
|
|
90
|
+
type: "update",
|
|
91
|
+
data: effect.data,
|
|
92
|
+
previousData: effect.previousData
|
|
93
|
+
},
|
|
94
|
+
ctx.clock
|
|
95
|
+
);
|
|
96
|
+
ctx.causalTracker?.afterOperation(effect.collection, operation.id, ctx.inTransaction);
|
|
97
|
+
const definition = ctx.schema.collections[effect.collection];
|
|
98
|
+
const serializedChanges = effect.data && definition ? (0, import_internal.serializeRecord)(effect.data, definition.fields) : effect.data ?? {};
|
|
99
|
+
const version = (0, import_internal.serializeRowVersion)(operation.timestamp);
|
|
100
|
+
const updateQuery = (0, import_internal.buildUpdateQuery)(effect.collection, effect.recordId, {
|
|
101
|
+
...serializedChanges,
|
|
102
|
+
_updated_at: operation.timestamp.wallTime,
|
|
103
|
+
_version: version
|
|
104
|
+
});
|
|
105
|
+
const opInsert = (0, import_internal.buildInsertQuery)(
|
|
106
|
+
`_kora_ops_${effect.collection}`,
|
|
107
|
+
(0, import_internal.serializeOperation)(operation)
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
operation,
|
|
111
|
+
collection: effect.collection,
|
|
112
|
+
commands: [
|
|
113
|
+
{ sql: updateQuery.sql, params: updateQuery.params },
|
|
114
|
+
{ sql: opInsert.sql, params: opInsert.params },
|
|
115
|
+
{
|
|
116
|
+
sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
117
|
+
params: [ctx.nodeId, operation.sequenceNumber]
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/apply-pipeline.ts
|
|
124
|
+
var ApplyPipeline = class {
|
|
125
|
+
constructor(deps) {
|
|
126
|
+
this.deps = deps;
|
|
127
|
+
this.relationLookupMap = (0, import_merge.buildMergeRelationLookup)(deps.store.getSchema());
|
|
128
|
+
}
|
|
129
|
+
deps;
|
|
130
|
+
relationLookupMap;
|
|
131
|
+
/** Local insert — same persistence path as remote apply side effects. */
|
|
132
|
+
async insert(collection, data) {
|
|
133
|
+
return (0, import_internal3.executeInsert)(this.deps.store.createMutationContext(collection), data);
|
|
134
|
+
}
|
|
135
|
+
/** Local update. */
|
|
136
|
+
async update(collection, id, data) {
|
|
137
|
+
return (0, import_internal3.executeUpdate)(this.deps.store.createMutationContext(collection), id, data);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Local delete with merge-package referential integrity (same rules as remote delete).
|
|
141
|
+
*/
|
|
142
|
+
async delete(collection, id) {
|
|
143
|
+
const ctx = this.deps.store.createMutationContext(collection);
|
|
144
|
+
const causalDeps = (0, import_internal3.resolveCausalDeps)(ctx);
|
|
145
|
+
const operation = await (0, import_core2.createOperation)(
|
|
146
|
+
{
|
|
147
|
+
nodeId: ctx.nodeId,
|
|
148
|
+
type: "delete",
|
|
149
|
+
collection,
|
|
150
|
+
recordId: id,
|
|
151
|
+
data: null,
|
|
152
|
+
previousData: null,
|
|
153
|
+
sequenceNumber: await ctx.allocateSequenceNumber(),
|
|
154
|
+
causalDeps,
|
|
155
|
+
schemaVersion: ctx.schema.version
|
|
156
|
+
},
|
|
157
|
+
ctx.clock
|
|
158
|
+
);
|
|
159
|
+
ctx.causalTracker?.afterOperation(collection, operation.id, ctx.inTransaction);
|
|
160
|
+
const refCtx = createReferentialMergeContext(this.deps.store);
|
|
161
|
+
const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
|
|
162
|
+
operation,
|
|
163
|
+
this.deps.store.getSchema(),
|
|
164
|
+
refCtx,
|
|
165
|
+
this.relationLookupMap
|
|
166
|
+
);
|
|
167
|
+
for (const trace of check.traces) {
|
|
168
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
169
|
+
}
|
|
170
|
+
if (!check.allowed) {
|
|
171
|
+
throw new import_core2.KoraError(
|
|
172
|
+
`Cannot delete record "${id}" from "${collection}": referential restrict policy violated`,
|
|
173
|
+
"REFERENTIAL_INTEGRITY",
|
|
174
|
+
{ collection, recordId: id }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
await (0, import_internal3.executeDelete)(ctx, id, {
|
|
178
|
+
skipReferentialEnforcement: true,
|
|
179
|
+
operation
|
|
180
|
+
});
|
|
181
|
+
if (check.sideEffectOps.length > 0) {
|
|
182
|
+
await applySideEffectOps(this.deps.store, check.sideEffectOps, operation.id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Commit a buffered transaction: merge-package delete enforcement, causal ordering, single DB txn.
|
|
187
|
+
*/
|
|
188
|
+
async commitTransaction(batch) {
|
|
189
|
+
const store = this.deps.store;
|
|
190
|
+
const supplemental = [];
|
|
191
|
+
for (const entry of batch.entries) {
|
|
192
|
+
if (entry.operation.type !== "delete") {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const refCtx = createReferentialMergeContext(store);
|
|
196
|
+
const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
|
|
197
|
+
entry.operation,
|
|
198
|
+
store.getSchema(),
|
|
199
|
+
refCtx,
|
|
200
|
+
this.relationLookupMap
|
|
201
|
+
);
|
|
202
|
+
for (const trace of check.traces) {
|
|
203
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
204
|
+
}
|
|
205
|
+
if (!check.allowed) {
|
|
206
|
+
throw new import_core2.KoraError(
|
|
207
|
+
`Cannot delete record "${entry.operation.recordId}" from "${entry.collection}": referential restrict policy violated`,
|
|
208
|
+
"REFERENTIAL_INTEGRITY",
|
|
209
|
+
{ collection: entry.collection, recordId: entry.operation.recordId }
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
for (const effect of check.sideEffectOps) {
|
|
213
|
+
const ctx2 = store.createMutationContext(effect.collection, { inTransaction: true });
|
|
214
|
+
supplemental.push(
|
|
215
|
+
await buildSideEffectEntry(
|
|
216
|
+
ctx2,
|
|
217
|
+
effect,
|
|
218
|
+
entry.operation.id,
|
|
219
|
+
batch.transactionId,
|
|
220
|
+
batch.mutationName
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const allEntries = [...batch.entries, ...supplemental];
|
|
226
|
+
const sortedOps = (0, import_internal2.topologicalSort)(allEntries.map((e) => e.operation));
|
|
227
|
+
const commandsByOpId = new Map(allEntries.map((e) => [e.operation.id, e.commands]));
|
|
228
|
+
const ctx = store.createMutationContext(
|
|
229
|
+
batch.entries[0]?.collection ?? Object.keys(store.getSchema().collections)[0] ?? "todos",
|
|
230
|
+
{ inTransaction: true }
|
|
231
|
+
);
|
|
232
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
233
|
+
for (const op of sortedOps) {
|
|
234
|
+
const commands = commandsByOpId.get(op.id);
|
|
235
|
+
if (!commands) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
for (const cmd of commands) {
|
|
239
|
+
await tx.execute(cmd.sql, cmd.params);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
const affectedCollections = /* @__PURE__ */ new Set();
|
|
244
|
+
for (const entry of allEntries) {
|
|
245
|
+
affectedCollections.add(entry.collection);
|
|
246
|
+
}
|
|
247
|
+
return { operations: sortedOps, affectedCollections };
|
|
248
|
+
}
|
|
249
|
+
async applyRemote(op) {
|
|
250
|
+
return this.apply(op, { mode: "remote", schema: this.deps.store.getSchema() });
|
|
251
|
+
}
|
|
252
|
+
async apply(op, context) {
|
|
253
|
+
if (context.mode === "local") {
|
|
254
|
+
return this.deps.store.applyRemoteOperation(op);
|
|
255
|
+
}
|
|
256
|
+
if (op.type === "delete") {
|
|
257
|
+
return this.applyRemoteDelete(op);
|
|
258
|
+
}
|
|
259
|
+
if (op.type === "insert" && op.data) {
|
|
260
|
+
return this.applyRemoteInsert(op);
|
|
261
|
+
}
|
|
262
|
+
if (op.type === "update" && op.data && op.previousData) {
|
|
263
|
+
return this.applyRemoteUpdate(op);
|
|
264
|
+
}
|
|
265
|
+
return this.deps.store.applyRemoteOperation(op);
|
|
266
|
+
}
|
|
267
|
+
async applyRemoteDelete(op) {
|
|
268
|
+
const blocked = await this.resolveRemoteDeleteVsLocalUpdate(op);
|
|
269
|
+
if (blocked) {
|
|
270
|
+
return "skipped";
|
|
271
|
+
}
|
|
272
|
+
const refCtx = createReferentialMergeContext(this.deps.store);
|
|
273
|
+
const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
|
|
274
|
+
op,
|
|
275
|
+
this.deps.store.getSchema(),
|
|
276
|
+
refCtx,
|
|
277
|
+
this.relationLookupMap
|
|
278
|
+
);
|
|
279
|
+
for (const trace of check.traces) {
|
|
280
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
281
|
+
}
|
|
282
|
+
if (!check.allowed) {
|
|
283
|
+
return "rejected";
|
|
284
|
+
}
|
|
285
|
+
const result = await this.deps.store.applyRemoteOperation(op);
|
|
286
|
+
if (result !== "applied") {
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
if (check.sideEffectOps.length > 0) {
|
|
290
|
+
await applySideEffectOps(this.deps.store, check.sideEffectOps, op.id);
|
|
291
|
+
}
|
|
292
|
+
return "applied";
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* When a local update is newer than a remote delete, keep the record alive.
|
|
296
|
+
*/
|
|
297
|
+
async resolveRemoteDeleteVsLocalUpdate(op) {
|
|
298
|
+
const collectionDef = this.deps.store.getSchema().collections[op.collection];
|
|
299
|
+
if (!collectionDef) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const localOp = await this.deps.store.getLatestLocalOperationForRecord(
|
|
303
|
+
op.collection,
|
|
304
|
+
op.recordId
|
|
305
|
+
);
|
|
306
|
+
if (!localOp || localOp.type !== "update") {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
const baseState = localOp.previousData ?? {};
|
|
310
|
+
const mergeResult = await this.deps.mergeEngine.merge({
|
|
311
|
+
local: localOp,
|
|
312
|
+
remote: op,
|
|
313
|
+
baseState,
|
|
314
|
+
collectionDef
|
|
315
|
+
});
|
|
316
|
+
this.emitMergeLifecycle(op, localOp, mergeResult);
|
|
317
|
+
return mergeResult.appliedOperation === "local";
|
|
318
|
+
}
|
|
319
|
+
async applyRemoteUpdate(op) {
|
|
320
|
+
const schema = this.deps.store.getSchema();
|
|
321
|
+
const collectionDef = schema.collections[op.collection];
|
|
322
|
+
if (!collectionDef) {
|
|
323
|
+
return this.deps.store.applyRemoteOperation(op);
|
|
324
|
+
}
|
|
325
|
+
const accessor = this.deps.store.collection(op.collection);
|
|
326
|
+
const currentRecord = await accessor.findById(op.recordId);
|
|
327
|
+
if (!currentRecord) {
|
|
328
|
+
const tombstoneResult = await this.applyRemoteUpdateOnDeletedRow(op, collectionDef);
|
|
329
|
+
if (tombstoneResult !== null) {
|
|
330
|
+
return tombstoneResult;
|
|
331
|
+
}
|
|
332
|
+
return this.deps.store.applyRemoteOperation(op);
|
|
333
|
+
}
|
|
334
|
+
let hasConflict = false;
|
|
335
|
+
for (const field of Object.keys(op.data ?? {})) {
|
|
336
|
+
const fieldDef = collectionDef.fields[field];
|
|
337
|
+
const expectedBase = op.previousData?.[field];
|
|
338
|
+
const currentLocal = currentRecord[field];
|
|
339
|
+
if (fieldDef?.kind === "richtext") {
|
|
340
|
+
if (!(0, import_store.richtextStatesEqual)(currentLocal, expectedBase)) {
|
|
341
|
+
hasConflict = true;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!deepEqual(expectedBase, currentLocal)) {
|
|
346
|
+
hasConflict = true;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (!hasConflict) {
|
|
351
|
+
return this.deps.store.applyRemoteOperation(op);
|
|
352
|
+
}
|
|
353
|
+
return this.applyMergedUpdate(op, collectionDef, currentRecord, op.previousData ?? {});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Remote update vs local soft-delete: merge before materializing to avoid zombie rows.
|
|
357
|
+
*/
|
|
358
|
+
async applyRemoteUpdateOnDeletedRow(op, collectionDef) {
|
|
359
|
+
const snapshot = await this.deps.store.findMaterializedRow(op.collection, op.recordId);
|
|
360
|
+
if (!snapshot?.deleted) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const localOp = await this.deps.store.getLatestLocalOperationForRecord(
|
|
364
|
+
op.collection,
|
|
365
|
+
op.recordId
|
|
366
|
+
);
|
|
367
|
+
if (!localOp || localOp.type !== "delete") {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const mergeResult = await this.deps.mergeEngine.merge({
|
|
371
|
+
local: localOp,
|
|
372
|
+
remote: op,
|
|
373
|
+
baseState: op.previousData ?? {},
|
|
374
|
+
collectionDef
|
|
375
|
+
});
|
|
376
|
+
this.emitMergeLifecycle(op, localOp, mergeResult);
|
|
377
|
+
if (mergeResult.appliedOperation === "local") {
|
|
378
|
+
return "skipped";
|
|
379
|
+
}
|
|
380
|
+
const mergedOp = {
|
|
381
|
+
...op,
|
|
382
|
+
data: { ...mergeResult.mergedData },
|
|
383
|
+
timestamp: maxTimestamp(op.timestamp, localOp.timestamp)
|
|
384
|
+
};
|
|
385
|
+
const applyResult = await this.deps.store.applyRemoteOperation(mergedOp, {
|
|
386
|
+
reactivateIfDeleted: true
|
|
387
|
+
});
|
|
388
|
+
if (applyResult === "applied" && mergeResult.sideEffects.length > 0) {
|
|
389
|
+
await applySideEffectOps(this.deps.store, mergeResult.sideEffects, op.id);
|
|
390
|
+
}
|
|
391
|
+
return applyResult;
|
|
392
|
+
}
|
|
393
|
+
async applyMergedUpdate(op, collectionDef, currentRecord, baseState, applyOptions) {
|
|
394
|
+
const localTimestamp = await resolveLocalTimestamp(
|
|
395
|
+
this.deps.store,
|
|
396
|
+
op.collection,
|
|
397
|
+
op.recordId,
|
|
398
|
+
currentRecord,
|
|
399
|
+
this.deps.store.getNodeId()
|
|
400
|
+
);
|
|
401
|
+
const localOp = {
|
|
402
|
+
...op,
|
|
403
|
+
data: buildLocalDiff(baseState, currentRecord, Object.keys(op.data ?? {})),
|
|
404
|
+
previousData: op.previousData,
|
|
405
|
+
nodeId: this.deps.store.getNodeId(),
|
|
406
|
+
timestamp: localTimestamp
|
|
407
|
+
};
|
|
408
|
+
const constraintContext = createConstraintContext(this.deps.store);
|
|
409
|
+
const mergeResult = await this.deps.mergeEngine.merge(
|
|
410
|
+
{
|
|
411
|
+
local: localOp,
|
|
412
|
+
remote: op,
|
|
413
|
+
baseState,
|
|
414
|
+
collectionDef
|
|
415
|
+
},
|
|
416
|
+
constraintContext
|
|
417
|
+
);
|
|
418
|
+
this.emitMergeLifecycle(op, localOp, mergeResult);
|
|
419
|
+
const mergedOp = {
|
|
420
|
+
...op,
|
|
421
|
+
data: mergeResult.mergedData,
|
|
422
|
+
timestamp: maxTimestamp(op.timestamp, localTimestamp)
|
|
423
|
+
};
|
|
424
|
+
const applyResult = await this.deps.store.applyRemoteOperation(mergedOp, applyOptions);
|
|
425
|
+
if (applyResult === "applied" && mergeResult.sideEffects.length > 0) {
|
|
426
|
+
await applySideEffectOps(this.deps.store, mergeResult.sideEffects, op.id);
|
|
427
|
+
}
|
|
428
|
+
return applyResult;
|
|
429
|
+
}
|
|
430
|
+
async applyRemoteInsert(op) {
|
|
431
|
+
const collectionDef = this.deps.store.getSchema().collections[op.collection];
|
|
432
|
+
if (!collectionDef || !op.data) {
|
|
433
|
+
return this.deps.store.applyRemoteOperation(op);
|
|
434
|
+
}
|
|
435
|
+
const snapshot = await this.deps.store.findMaterializedRow(op.collection, op.recordId);
|
|
436
|
+
if (!snapshot || snapshot.deleted) {
|
|
437
|
+
return this.deps.store.applyRemoteOperation(op);
|
|
438
|
+
}
|
|
439
|
+
const localOp = await resolveLocalOpForRecord(
|
|
440
|
+
this.deps.store,
|
|
441
|
+
op.collection,
|
|
442
|
+
op.recordId,
|
|
443
|
+
snapshot.record
|
|
444
|
+
);
|
|
445
|
+
const mergeResult = await this.deps.mergeEngine.merge({
|
|
446
|
+
local: localOp,
|
|
447
|
+
remote: op,
|
|
448
|
+
baseState: {},
|
|
449
|
+
collectionDef
|
|
450
|
+
});
|
|
451
|
+
this.emitMergeLifecycle(op, localOp, mergeResult);
|
|
452
|
+
const mergedOp = {
|
|
453
|
+
...op,
|
|
454
|
+
data: mergeResult.mergedData,
|
|
455
|
+
timestamp: maxTimestamp(op.timestamp, localOp.timestamp)
|
|
456
|
+
};
|
|
457
|
+
return this.deps.store.applyRemoteOperation(mergedOp);
|
|
458
|
+
}
|
|
459
|
+
emitMergeLifecycle(remote, local, mergeResult) {
|
|
460
|
+
this.deps.emitter?.emit({
|
|
461
|
+
type: "merge:started",
|
|
462
|
+
operationA: remote,
|
|
463
|
+
operationB: local
|
|
464
|
+
});
|
|
465
|
+
const hadMergeConflict = mergeResult.traces.some(
|
|
466
|
+
(t) => t.strategy !== "no-conflict-local" && t.strategy !== "no-conflict-remote" && t.strategy !== "no-conflict-unchanged"
|
|
467
|
+
);
|
|
468
|
+
if (hadMergeConflict) {
|
|
469
|
+
this.deps.onMergeConflict?.();
|
|
470
|
+
}
|
|
471
|
+
for (const trace of mergeResult.traces) {
|
|
472
|
+
if (trace.strategy !== "no-conflict-local" && trace.strategy !== "no-conflict-remote" && trace.strategy !== "no-conflict-unchanged") {
|
|
473
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const firstTrace = mergeResult.traces[0];
|
|
477
|
+
if (firstTrace) {
|
|
478
|
+
this.deps.emitter?.emit({ type: "merge:completed", trace: firstTrace });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
async function resolveLocalOpForRecord(store, collection, recordId, record) {
|
|
483
|
+
const localLatest = await store.getLatestLocalOperationForRecord(collection, recordId);
|
|
484
|
+
if (localLatest) {
|
|
485
|
+
return localLatest;
|
|
486
|
+
}
|
|
487
|
+
const anyLatest = await store.getLatestOperationForRecord(collection, recordId);
|
|
488
|
+
if (anyLatest) {
|
|
489
|
+
return anyLatest;
|
|
490
|
+
}
|
|
491
|
+
return syntheticInsertFromSnapshot(
|
|
492
|
+
record,
|
|
493
|
+
collection,
|
|
494
|
+
store.getNodeId(),
|
|
495
|
+
store.getSchema().version
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
function syntheticInsertFromSnapshot(record, collection, nodeId, schemaVersion) {
|
|
499
|
+
const { id, createdAt, updatedAt, ...fields } = record;
|
|
500
|
+
return {
|
|
501
|
+
id: `synthetic-local-${id}`,
|
|
502
|
+
nodeId,
|
|
503
|
+
type: "insert",
|
|
504
|
+
collection,
|
|
505
|
+
recordId: id,
|
|
506
|
+
data: fields,
|
|
507
|
+
previousData: null,
|
|
508
|
+
timestamp: { wallTime: updatedAt, logical: 0, nodeId },
|
|
509
|
+
sequenceNumber: 0,
|
|
510
|
+
causalDeps: [],
|
|
511
|
+
schemaVersion
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function createConstraintContext(store) {
|
|
515
|
+
return {
|
|
516
|
+
async queryRecords(collection, where) {
|
|
517
|
+
const rows = await store.collection(collection).where(where).exec();
|
|
518
|
+
return rows;
|
|
519
|
+
},
|
|
520
|
+
async countRecords(collection, where) {
|
|
521
|
+
return store.collection(collection).where(where).count();
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function createReferentialMergeContext(store) {
|
|
526
|
+
return {
|
|
527
|
+
async queryRecords(collection, where) {
|
|
528
|
+
const rows = await store.collection(collection).where(where).exec();
|
|
529
|
+
return rows;
|
|
530
|
+
},
|
|
531
|
+
async recordExists(collection, recordId) {
|
|
532
|
+
const row = await store.collection(collection).findById(recordId);
|
|
533
|
+
return row !== null;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
async function applySideEffectOps(store, sideEffects, parentOpId) {
|
|
538
|
+
for (const effect of sideEffects) {
|
|
539
|
+
const ctx = store.createMutationContext(effect.collection, {
|
|
540
|
+
extraCausalDeps: [parentOpId]
|
|
541
|
+
});
|
|
542
|
+
if (effect.type === "delete") {
|
|
543
|
+
await (0, import_internal3.executeDelete)(ctx, effect.recordId, { skipReferentialEnforcement: true });
|
|
544
|
+
} else if (effect.type === "update" && effect.data) {
|
|
545
|
+
await (0, import_internal3.executeUpdate)(ctx, effect.recordId, effect.data);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function buildLocalDiff(baseState, currentRecord, fields) {
|
|
550
|
+
const diff = {};
|
|
551
|
+
for (const field of fields) {
|
|
552
|
+
diff[field] = currentRecord[field];
|
|
553
|
+
}
|
|
554
|
+
return diff;
|
|
555
|
+
}
|
|
556
|
+
async function resolveLocalTimestamp(store, collection, recordId, currentRecord, nodeId) {
|
|
557
|
+
const latestLocal = await store.getLatestLocalOperationForRecord(collection, recordId);
|
|
558
|
+
if (latestLocal) {
|
|
559
|
+
return latestLocal.timestamp;
|
|
560
|
+
}
|
|
561
|
+
const updatedAt = currentRecord.updatedAt;
|
|
562
|
+
if (typeof updatedAt === "number") {
|
|
563
|
+
return { wallTime: updatedAt, logical: 0, nodeId };
|
|
564
|
+
}
|
|
565
|
+
return { wallTime: Date.now(), logical: 0, nodeId };
|
|
566
|
+
}
|
|
567
|
+
function maxTimestamp(a, b) {
|
|
568
|
+
return import_core2.HybridLogicalClock.compare(a, b) >= 0 ? a : b;
|
|
569
|
+
}
|
|
570
|
+
function deepEqual(a, b) {
|
|
571
|
+
if (a === b) return true;
|
|
572
|
+
if (a === null || b === null) return false;
|
|
573
|
+
if (a === void 0 || b === void 0) return false;
|
|
574
|
+
if (typeof a !== typeof b) return false;
|
|
575
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
576
|
+
if (a.length !== b.length) return false;
|
|
577
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
578
|
+
}
|
|
579
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
580
|
+
const keysA = Object.keys(a);
|
|
581
|
+
const keysB = Object.keys(b);
|
|
582
|
+
if (keysA.length !== keysB.length) return false;
|
|
583
|
+
return keysA.every(
|
|
584
|
+
(key) => deepEqual(a[key], b[key])
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/merge-aware-sync-store.ts
|
|
591
|
+
var MergeAwareSyncStore = class {
|
|
592
|
+
constructor(store, mergeEngine, emitter, options) {
|
|
593
|
+
this.store = store;
|
|
594
|
+
this.pipeline = new ApplyPipeline({
|
|
595
|
+
store,
|
|
596
|
+
mergeEngine,
|
|
597
|
+
emitter,
|
|
598
|
+
onMergeConflict: options?.onMergeConflict
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
store;
|
|
602
|
+
pipeline;
|
|
603
|
+
getVersionVector() {
|
|
604
|
+
return this.store.getVersionVector();
|
|
605
|
+
}
|
|
606
|
+
getNodeId() {
|
|
607
|
+
return this.store.getNodeId();
|
|
608
|
+
}
|
|
609
|
+
async getOperationRange(nodeId, fromSeq, toSeq) {
|
|
610
|
+
return this.store.getOperationRange(nodeId, fromSeq, toSeq);
|
|
611
|
+
}
|
|
612
|
+
async applyRemoteOperation(op) {
|
|
613
|
+
return this.pipeline.applyRemote(op);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// src/store-queue-storage.ts
|
|
618
|
+
var import_internal4 = require("@korajs/store/internal");
|
|
619
|
+
var StoreQueueStorage = class {
|
|
620
|
+
constructor(adapter) {
|
|
621
|
+
this.adapter = adapter;
|
|
622
|
+
}
|
|
623
|
+
adapter;
|
|
624
|
+
async load() {
|
|
625
|
+
const rows = await this.adapter.query(
|
|
626
|
+
"SELECT id, payload FROM _kora_sync_queue ORDER BY rowid ASC"
|
|
627
|
+
);
|
|
628
|
+
return rows.map((row) => operationFromQueuePayload(row.payload));
|
|
629
|
+
}
|
|
630
|
+
async enqueue(op) {
|
|
631
|
+
const row = (0, import_internal4.serializeOperation)(op);
|
|
632
|
+
const payload = { ...row, _collection: op.collection };
|
|
633
|
+
await this.adapter.execute(
|
|
634
|
+
"INSERT OR REPLACE INTO _kora_sync_queue (id, payload) VALUES (?, ?)",
|
|
635
|
+
[op.id, JSON.stringify(payload)]
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
async dequeue(ids) {
|
|
639
|
+
if (ids.length === 0) return;
|
|
640
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
641
|
+
await this.adapter.execute(`DELETE FROM _kora_sync_queue WHERE id IN (${placeholders})`, ids);
|
|
642
|
+
}
|
|
643
|
+
async count() {
|
|
644
|
+
const rows = await this.adapter.query(
|
|
645
|
+
"SELECT COUNT(*) as cnt FROM _kora_sync_queue"
|
|
646
|
+
);
|
|
647
|
+
return rows[0]?.cnt ?? 0;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
function operationFromQueuePayload(payload) {
|
|
651
|
+
const parsed = JSON.parse(payload);
|
|
652
|
+
const op = (0, import_internal4.deserializeOperation)(parsed);
|
|
653
|
+
return {
|
|
654
|
+
...op,
|
|
655
|
+
collection: parsed._collection
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/store-sync-state.ts
|
|
660
|
+
var import_store2 = require("@korajs/store");
|
|
661
|
+
var import_sync = require("@korajs/sync");
|
|
662
|
+
var StoreSyncStatePersistence = class {
|
|
663
|
+
constructor(store, scope) {
|
|
664
|
+
this.store = store;
|
|
665
|
+
this.scope = scope;
|
|
666
|
+
}
|
|
667
|
+
store;
|
|
668
|
+
scope;
|
|
669
|
+
loadLastAckedServerVector() {
|
|
670
|
+
return this.store.loadLastAckedServerVector();
|
|
671
|
+
}
|
|
672
|
+
saveLastAckedServerVector(vector) {
|
|
673
|
+
return this.store.saveLastAckedServerVector(vector);
|
|
674
|
+
}
|
|
675
|
+
mergeServerVectors(a, b) {
|
|
676
|
+
return this.store.mergeServerVectors(a, b);
|
|
677
|
+
}
|
|
678
|
+
async countUnsyncedOperations(serverVector) {
|
|
679
|
+
const ops = await this.getUnsyncedOperations(serverVector);
|
|
680
|
+
return ops.length;
|
|
681
|
+
}
|
|
682
|
+
async getUnsyncedOperations(serverVector) {
|
|
683
|
+
const ops = await this.store.getUnsyncedOperations(serverVector);
|
|
684
|
+
return ops.filter((op) => (0, import_sync.operationMatchesScope)(op, this.scope));
|
|
685
|
+
}
|
|
686
|
+
loadDeltaCursor() {
|
|
687
|
+
return this.store.loadDeltaCursor().then((encoded) => (0, import_sync.decodeDeltaCursor)(encoded));
|
|
688
|
+
}
|
|
689
|
+
async saveDeltaCursor(cursor) {
|
|
690
|
+
await this.store.saveDeltaCursor(cursor ? (0, import_sync.encodeDeltaCursor)(cursor) : null);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
694
|
+
0 && (module.exports = {
|
|
695
|
+
ApplyPipeline,
|
|
696
|
+
MergeAwareSyncStore,
|
|
697
|
+
StoreQueueStorage,
|
|
698
|
+
StoreSyncStatePersistence
|
|
699
|
+
});
|
|
700
|
+
//# sourceMappingURL=testing.cjs.map
|