korajs 0.3.5 → 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.
@@ -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