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