lakesync 0.1.5 → 0.1.8
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/adapter.d.ts +199 -19
- package/dist/adapter.js +19 -3
- package/dist/analyst.js +2 -2
- package/dist/{base-poller-CBvhdvcj.d.ts → base-poller-Bj9kX9dv.d.ts} +76 -19
- package/dist/catalogue.d.ts +1 -1
- package/dist/catalogue.js +3 -3
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-PWGQ3PXE.js → chunk-JI4C4R5H.js} +280 -140
- package/dist/chunk-JI4C4R5H.js.map +1 -0
- package/dist/{chunk-L4ZL5JA7.js → chunk-KVSWLIJR.js} +2 -2
- package/dist/{chunk-7UBS6MFH.js → chunk-LDFFCG2K.js} +377 -247
- package/dist/chunk-LDFFCG2K.js.map +1 -0
- package/dist/{chunk-Z7FGLEQU.js → chunk-LPWXOYNS.js} +376 -287
- package/dist/chunk-LPWXOYNS.js.map +1 -0
- package/dist/{chunk-SZSGSTVZ.js → chunk-PYRS74YP.js} +15 -4
- package/dist/{chunk-SZSGSTVZ.js.map → chunk-PYRS74YP.js.map} +1 -1
- package/dist/{chunk-TVLTXHW6.js → chunk-QNITY4F6.js} +30 -7
- package/dist/{chunk-TVLTXHW6.js.map → chunk-QNITY4F6.js.map} +1 -1
- package/dist/{chunk-46CKACNC.js → chunk-SSICS5KI.js} +2 -2
- package/dist/{chunk-B3QEUG6E.js → chunk-TMLG32QV.js} +2 -2
- package/dist/client.d.ts +164 -13
- package/dist/client.js +310 -163
- package/dist/client.js.map +1 -1
- package/dist/compactor.d.ts +1 -1
- package/dist/compactor.js +4 -4
- package/dist/connector-jira.d.ts +2 -2
- package/dist/connector-jira.js +3 -3
- package/dist/connector-salesforce.d.ts +2 -2
- package/dist/connector-salesforce.js +3 -3
- package/dist/{coordinator-DN8D8C7W.d.ts → coordinator-NXy6tA0h.d.ts} +23 -16
- package/dist/{db-types-B6_JKQWK.d.ts → db-types-CfLMUBfW.d.ts} +1 -1
- package/dist/gateway-server.d.ts +158 -64
- package/dist/gateway-server.js +482 -4003
- package/dist/gateway-server.js.map +1 -1
- package/dist/gateway.d.ts +61 -104
- package/dist/gateway.js +12 -6
- package/dist/index.d.ts +45 -10
- package/dist/index.js +14 -2
- package/dist/parquet.d.ts +1 -1
- package/dist/parquet.js +3 -3
- package/dist/proto.d.ts +1 -1
- package/dist/proto.js +3 -3
- package/dist/react.d.ts +47 -10
- package/dist/react.js +88 -40
- package/dist/react.js.map +1 -1
- package/dist/{registry-BN_9spxE.d.ts → registry-BcspAtZI.d.ts} +19 -4
- package/dist/{gateway-CvO7Xy3T.d.ts → request-handler-pUvL7ozF.d.ts} +139 -10
- package/dist/{resolver-BZURzdlL.d.ts → resolver-CXxmC0jR.d.ts} +1 -1
- package/dist/{src-RR7I76OL.js → src-B6NLV3FP.js} +4 -4
- package/dist/{src-SLVE5567.js → src-ROW4XLO7.js} +15 -3
- package/dist/{src-V2CTPR7V.js → src-ZRHKG42A.js} +4 -4
- package/dist/{types-GGBfZBKQ.d.ts → types-BdGBv2ba.d.ts} +23 -2
- package/dist/{types-D-E0VrfS.d.ts → types-BrcD1oJg.d.ts} +26 -19
- package/package.json +1 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-7UBS6MFH.js.map +0 -1
- package/dist/chunk-PWGQ3PXE.js.map +0 -1
- package/dist/chunk-Z7FGLEQU.js.map +0 -1
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-L4ZL5JA7.js.map → chunk-KVSWLIJR.js.map} +0 -0
- /package/dist/{chunk-46CKACNC.js.map → chunk-SSICS5KI.js.map} +0 -0
- /package/dist/{chunk-B3QEUG6E.js.map → chunk-TMLG32QV.js.map} +0 -0
- /package/dist/{src-RR7I76OL.js.map → src-B6NLV3FP.js.map} +0 -0
- /package/dist/{src-SLVE5567.js.map → src-ROW4XLO7.js.map} +0 -0
- /package/dist/{src-V2CTPR7V.js.map → src-ZRHKG42A.js.map} +0 -0
package/dist/client.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
decodeSyncResponse,
|
|
7
7
|
encodeSyncPull,
|
|
8
8
|
encodeSyncPush
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-KVSWLIJR.js";
|
|
10
10
|
import {
|
|
11
11
|
Err,
|
|
12
12
|
HLC,
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
quoteIdentifier,
|
|
23
23
|
toError,
|
|
24
24
|
unwrapOrThrow
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
import "./chunk-
|
|
25
|
+
} from "./chunk-LDFFCG2K.js";
|
|
26
|
+
import "./chunk-DGUM43GV.js";
|
|
27
27
|
|
|
28
28
|
// ../client/src/db/local-db.ts
|
|
29
29
|
import initSqlJs from "sql.js";
|
|
@@ -368,15 +368,19 @@ async function migrateSchema(db, oldSchema, newSchema) {
|
|
|
368
368
|
});
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
// ../client/src/queue/memory-
|
|
372
|
-
var
|
|
371
|
+
// ../client/src/queue/memory-outbox.ts
|
|
372
|
+
var MemoryOutbox = class {
|
|
373
373
|
entries = /* @__PURE__ */ new Map();
|
|
374
374
|
counter = 0;
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
prefix;
|
|
376
|
+
constructor(prefix = "mem") {
|
|
377
|
+
this.prefix = prefix;
|
|
378
|
+
}
|
|
379
|
+
/** Add an item to the queue. */
|
|
380
|
+
async push(item) {
|
|
377
381
|
const entry = {
|
|
378
|
-
id:
|
|
379
|
-
|
|
382
|
+
id: `${this.prefix}-${++this.counter}`,
|
|
383
|
+
item,
|
|
380
384
|
status: "pending",
|
|
381
385
|
createdAt: Date.now(),
|
|
382
386
|
retryCount: 0
|
|
@@ -384,13 +388,13 @@ var MemoryQueue = class {
|
|
|
384
388
|
this.entries.set(entry.id, entry);
|
|
385
389
|
return Ok(entry);
|
|
386
390
|
}
|
|
387
|
-
/** Peek at pending entries (ordered by createdAt), skipping entries with future retryAfter */
|
|
391
|
+
/** Peek at pending entries (ordered by createdAt), skipping entries with future retryAfter. */
|
|
388
392
|
async peek(limit) {
|
|
389
393
|
const now = Date.now();
|
|
390
394
|
const pending = [...this.entries.values()].filter((e) => e.status === "pending" && (e.retryAfter === void 0 || e.retryAfter <= now)).sort((a, b) => a.createdAt - b.createdAt).slice(0, limit);
|
|
391
395
|
return Ok(pending);
|
|
392
396
|
}
|
|
393
|
-
/** Mark entries as currently being sent */
|
|
397
|
+
/** Mark entries as currently being sent. */
|
|
394
398
|
async markSending(ids) {
|
|
395
399
|
for (const id of ids) {
|
|
396
400
|
const entry = this.entries.get(id);
|
|
@@ -400,14 +404,14 @@ var MemoryQueue = class {
|
|
|
400
404
|
}
|
|
401
405
|
return Ok(void 0);
|
|
402
406
|
}
|
|
403
|
-
/** Acknowledge successful delivery (removes entries) */
|
|
407
|
+
/** Acknowledge successful delivery (removes entries). */
|
|
404
408
|
async ack(ids) {
|
|
405
409
|
for (const id of ids) {
|
|
406
410
|
this.entries.delete(id);
|
|
407
411
|
}
|
|
408
412
|
return Ok(void 0);
|
|
409
413
|
}
|
|
410
|
-
/** Negative acknowledge — reset to pending with incremented retryCount and exponential backoff */
|
|
414
|
+
/** Negative acknowledge — reset to pending with incremented retryCount and exponential backoff. */
|
|
411
415
|
async nack(ids) {
|
|
412
416
|
for (const id of ids) {
|
|
413
417
|
const entry = this.entries.get(id);
|
|
@@ -420,18 +424,68 @@ var MemoryQueue = class {
|
|
|
420
424
|
}
|
|
421
425
|
return Ok(void 0);
|
|
422
426
|
}
|
|
423
|
-
/** Get the number of pending + sending entries */
|
|
427
|
+
/** Get the number of pending + sending entries. */
|
|
424
428
|
async depth() {
|
|
425
|
-
const count = [...this.entries.values()].filter(
|
|
429
|
+
const count = [...this.entries.values()].filter(
|
|
430
|
+
(e) => e.status === "pending" || e.status === "sending"
|
|
431
|
+
).length;
|
|
426
432
|
return Ok(count);
|
|
427
433
|
}
|
|
428
|
-
/** Remove all entries */
|
|
434
|
+
/** Remove all entries. */
|
|
429
435
|
async clear() {
|
|
430
436
|
this.entries.clear();
|
|
431
437
|
return Ok(void 0);
|
|
432
438
|
}
|
|
433
439
|
};
|
|
434
440
|
|
|
441
|
+
// ../client/src/queue/memory-queue.ts
|
|
442
|
+
var MemoryQueue = class {
|
|
443
|
+
outbox = new MemoryOutbox("mem");
|
|
444
|
+
/** Add a delta to the queue */
|
|
445
|
+
async push(delta) {
|
|
446
|
+
const result = await this.outbox.push(delta);
|
|
447
|
+
if (!result.ok) return result;
|
|
448
|
+
return { ok: true, value: this.toQueueEntry(result.value) };
|
|
449
|
+
}
|
|
450
|
+
/** Peek at pending entries (ordered by createdAt), skipping entries with future retryAfter */
|
|
451
|
+
async peek(limit) {
|
|
452
|
+
const result = await this.outbox.peek(limit);
|
|
453
|
+
if (!result.ok) return result;
|
|
454
|
+
return { ok: true, value: result.value.map((e) => this.toQueueEntry(e)) };
|
|
455
|
+
}
|
|
456
|
+
/** Mark entries as currently being sent */
|
|
457
|
+
async markSending(ids) {
|
|
458
|
+
return this.outbox.markSending(ids);
|
|
459
|
+
}
|
|
460
|
+
/** Acknowledge successful delivery (removes entries) */
|
|
461
|
+
async ack(ids) {
|
|
462
|
+
return this.outbox.ack(ids);
|
|
463
|
+
}
|
|
464
|
+
/** Negative acknowledge — reset to pending with incremented retryCount and exponential backoff */
|
|
465
|
+
async nack(ids) {
|
|
466
|
+
return this.outbox.nack(ids);
|
|
467
|
+
}
|
|
468
|
+
/** Get the number of pending + sending entries */
|
|
469
|
+
async depth() {
|
|
470
|
+
return this.outbox.depth();
|
|
471
|
+
}
|
|
472
|
+
/** Remove all entries */
|
|
473
|
+
async clear() {
|
|
474
|
+
return this.outbox.clear();
|
|
475
|
+
}
|
|
476
|
+
/** Convert a generic OutboxEntry to the SyncQueue-specific QueueEntry shape. */
|
|
477
|
+
toQueueEntry(entry) {
|
|
478
|
+
return {
|
|
479
|
+
id: entry.id,
|
|
480
|
+
delta: entry.item,
|
|
481
|
+
status: entry.status,
|
|
482
|
+
createdAt: entry.createdAt,
|
|
483
|
+
retryCount: entry.retryCount,
|
|
484
|
+
retryAfter: entry.retryAfter
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
435
489
|
// ../client/src/queue/idb-queue.ts
|
|
436
490
|
import { openDB as openDB2 } from "idb";
|
|
437
491
|
var DB_NAME = "lakesync-queue";
|
|
@@ -574,6 +628,128 @@ var IDBQueue = class {
|
|
|
574
628
|
}
|
|
575
629
|
};
|
|
576
630
|
|
|
631
|
+
// ../client/src/sync/action-processor.ts
|
|
632
|
+
var ActionProcessor = class {
|
|
633
|
+
actionQueue;
|
|
634
|
+
transport;
|
|
635
|
+
clientId;
|
|
636
|
+
hlc;
|
|
637
|
+
maxRetries;
|
|
638
|
+
onComplete = null;
|
|
639
|
+
constructor(config) {
|
|
640
|
+
this.actionQueue = config.actionQueue;
|
|
641
|
+
this.transport = config.transport;
|
|
642
|
+
this.clientId = config.clientId;
|
|
643
|
+
this.hlc = config.hlc;
|
|
644
|
+
this.maxRetries = config.maxRetries;
|
|
645
|
+
}
|
|
646
|
+
/** Register a callback for action completion events. */
|
|
647
|
+
setOnComplete(cb) {
|
|
648
|
+
this.onComplete = cb;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Submit an action for execution.
|
|
652
|
+
*
|
|
653
|
+
* Pushes the action to the ActionQueue and triggers immediate processing.
|
|
654
|
+
*/
|
|
655
|
+
async enqueue(params) {
|
|
656
|
+
const hlc = this.hlc.now();
|
|
657
|
+
const { generateActionId } = await import("./src-ROW4XLO7.js");
|
|
658
|
+
const actionId = await generateActionId({
|
|
659
|
+
clientId: this.clientId,
|
|
660
|
+
hlc,
|
|
661
|
+
connector: params.connector,
|
|
662
|
+
actionType: params.actionType,
|
|
663
|
+
params: params.params
|
|
664
|
+
});
|
|
665
|
+
const action = {
|
|
666
|
+
actionId,
|
|
667
|
+
clientId: this.clientId,
|
|
668
|
+
hlc,
|
|
669
|
+
connector: params.connector,
|
|
670
|
+
actionType: params.actionType,
|
|
671
|
+
params: params.params,
|
|
672
|
+
idempotencyKey: params.idempotencyKey
|
|
673
|
+
};
|
|
674
|
+
await this.actionQueue.push(action);
|
|
675
|
+
void this.processQueue();
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Process pending actions from the action queue.
|
|
679
|
+
*
|
|
680
|
+
* Peeks at pending entries, sends them to the gateway via
|
|
681
|
+
* `transport.executeAction()`, and acks/nacks based on the result.
|
|
682
|
+
* Dead-letters entries after `maxRetries` failures.
|
|
683
|
+
*/
|
|
684
|
+
async processQueue() {
|
|
685
|
+
if (!this.transport.executeAction) return;
|
|
686
|
+
const peekResult = await this.actionQueue.peek(100);
|
|
687
|
+
if (!peekResult.ok || peekResult.value.length === 0) return;
|
|
688
|
+
const deadLettered = peekResult.value.filter((e) => e.retryCount >= this.maxRetries);
|
|
689
|
+
const entries = peekResult.value.filter((e) => e.retryCount < this.maxRetries);
|
|
690
|
+
if (deadLettered.length > 0) {
|
|
691
|
+
console.warn(
|
|
692
|
+
`[ActionProcessor] Dead-lettering ${deadLettered.length} actions after ${this.maxRetries} retries`
|
|
693
|
+
);
|
|
694
|
+
await this.actionQueue.ack(deadLettered.map((e) => e.id));
|
|
695
|
+
for (const entry of deadLettered) {
|
|
696
|
+
this.onComplete?.(entry.action.actionId, {
|
|
697
|
+
actionId: entry.action.actionId,
|
|
698
|
+
code: "DEAD_LETTERED",
|
|
699
|
+
message: `Action dead-lettered after ${this.maxRetries} retries`,
|
|
700
|
+
retryable: false
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (entries.length === 0) return;
|
|
705
|
+
const ids = entries.map((e) => e.id);
|
|
706
|
+
await this.actionQueue.markSending(ids);
|
|
707
|
+
const transportResult = await this.transport.executeAction({
|
|
708
|
+
clientId: this.clientId,
|
|
709
|
+
actions: entries.map((e) => e.action)
|
|
710
|
+
});
|
|
711
|
+
if (transportResult.ok) {
|
|
712
|
+
await this.actionQueue.ack(ids);
|
|
713
|
+
for (const result of transportResult.value.results) {
|
|
714
|
+
this.onComplete?.(result.actionId, result);
|
|
715
|
+
}
|
|
716
|
+
const retryableIds = [];
|
|
717
|
+
for (let i = 0; i < transportResult.value.results.length; i++) {
|
|
718
|
+
const result = transportResult.value.results[i];
|
|
719
|
+
if (isActionError(result) && result.retryable) {
|
|
720
|
+
retryableIds.push(ids[i]);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
await this.actionQueue.nack(ids);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Discover available connectors and their supported action types.
|
|
729
|
+
*
|
|
730
|
+
* Delegates to the transport's `describeActions()` method. Returns
|
|
731
|
+
* empty connectors when the transport does not support discovery.
|
|
732
|
+
*/
|
|
733
|
+
async describeActions() {
|
|
734
|
+
if (!this.transport.describeActions) {
|
|
735
|
+
return { ok: true, value: { connectors: {} } };
|
|
736
|
+
}
|
|
737
|
+
return this.transport.describeActions();
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* List available connector types and their configuration schemas.
|
|
741
|
+
*
|
|
742
|
+
* Delegates to the transport's `listConnectorTypes()` method. Returns
|
|
743
|
+
* an empty array when the transport does not support it.
|
|
744
|
+
*/
|
|
745
|
+
async listConnectorTypes() {
|
|
746
|
+
if (!this.transport.listConnectorTypes) {
|
|
747
|
+
return { ok: true, value: [] };
|
|
748
|
+
}
|
|
749
|
+
return this.transport.listConnectorTypes();
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
577
753
|
// ../client/src/sync/applier.ts
|
|
578
754
|
async function applyRemoteDeltas(db, deltas, resolver, pendingQueue) {
|
|
579
755
|
if (deltas.length === 0) {
|
|
@@ -777,6 +953,54 @@ async function applySqlDelta(db, delta) {
|
|
|
777
953
|
}
|
|
778
954
|
}
|
|
779
955
|
|
|
956
|
+
// ../client/src/sync/auto-sync.ts
|
|
957
|
+
var AutoSyncScheduler = class {
|
|
958
|
+
intervalId = null;
|
|
959
|
+
visibilityHandler = null;
|
|
960
|
+
syncFn;
|
|
961
|
+
intervalMs;
|
|
962
|
+
constructor(syncFn, intervalMs) {
|
|
963
|
+
this.syncFn = syncFn;
|
|
964
|
+
this.intervalMs = intervalMs;
|
|
965
|
+
}
|
|
966
|
+
/** Whether auto-sync is currently running. */
|
|
967
|
+
get isRunning() {
|
|
968
|
+
return this.intervalId !== null;
|
|
969
|
+
}
|
|
970
|
+
/** Start periodic syncing and visibility-change-triggered sync. */
|
|
971
|
+
start() {
|
|
972
|
+
if (this.intervalId !== null) return;
|
|
973
|
+
this.intervalId = setInterval(() => {
|
|
974
|
+
void this.syncFn();
|
|
975
|
+
}, this.intervalMs);
|
|
976
|
+
this.setupVisibilitySync();
|
|
977
|
+
}
|
|
978
|
+
/** Stop periodic syncing and remove the visibility listener. */
|
|
979
|
+
stop() {
|
|
980
|
+
if (this.intervalId !== null) {
|
|
981
|
+
clearInterval(this.intervalId);
|
|
982
|
+
this.intervalId = null;
|
|
983
|
+
}
|
|
984
|
+
if (this.visibilityHandler) {
|
|
985
|
+
if (typeof document !== "undefined") {
|
|
986
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
987
|
+
}
|
|
988
|
+
this.visibilityHandler = null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/** Register a visibility change listener to sync when the tab becomes visible. */
|
|
992
|
+
setupVisibilitySync() {
|
|
993
|
+
this.visibilityHandler = () => {
|
|
994
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
995
|
+
void this.syncFn();
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
if (typeof document !== "undefined") {
|
|
999
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
780
1004
|
// ../client/src/sync/tracker.ts
|
|
781
1005
|
function rowWithoutId(row) {
|
|
782
1006
|
const result = {};
|
|
@@ -977,17 +1201,14 @@ var SyncCoordinator = class {
|
|
|
977
1201
|
_clientId;
|
|
978
1202
|
maxRetries;
|
|
979
1203
|
syncMode;
|
|
980
|
-
autoSyncIntervalMs;
|
|
981
|
-
realtimeHeartbeatMs;
|
|
982
1204
|
lastSyncedHlc = HLC.encode(0, 0);
|
|
983
1205
|
_lastSyncTime = null;
|
|
984
|
-
syncIntervalId = null;
|
|
985
|
-
visibilityHandler = null;
|
|
986
1206
|
syncing = false;
|
|
987
|
-
|
|
988
|
-
|
|
1207
|
+
autoSyncScheduler;
|
|
1208
|
+
actionProcessor;
|
|
989
1209
|
listeners = {
|
|
990
1210
|
onChange: [],
|
|
1211
|
+
onSyncStart: [],
|
|
991
1212
|
onSyncComplete: [],
|
|
992
1213
|
onError: [],
|
|
993
1214
|
onActionComplete: []
|
|
@@ -1000,11 +1221,25 @@ var SyncCoordinator = class {
|
|
|
1000
1221
|
this._clientId = config?.clientId ?? `client-${crypto.randomUUID()}`;
|
|
1001
1222
|
this.maxRetries = config?.maxRetries ?? 10;
|
|
1002
1223
|
this.syncMode = config?.syncMode ?? "full";
|
|
1003
|
-
this.autoSyncIntervalMs = config?.autoSyncIntervalMs ?? AUTO_SYNC_INTERVAL_MS;
|
|
1004
|
-
this.realtimeHeartbeatMs = config?.realtimeHeartbeatMs ?? REALTIME_HEARTBEAT_MS;
|
|
1005
|
-
this.actionQueue = config?.actionQueue ?? null;
|
|
1006
|
-
this.maxActionRetries = config?.maxActionRetries ?? 5;
|
|
1007
1224
|
this.tracker = new SyncTracker(db, this.queue, this.hlc, this._clientId);
|
|
1225
|
+
const autoSyncIntervalMs = config?.autoSyncIntervalMs ?? AUTO_SYNC_INTERVAL_MS;
|
|
1226
|
+
const realtimeHeartbeatMs = config?.realtimeHeartbeatMs ?? REALTIME_HEARTBEAT_MS;
|
|
1227
|
+
const intervalMs = transport.supportsRealtime ? realtimeHeartbeatMs : autoSyncIntervalMs;
|
|
1228
|
+
this.autoSyncScheduler = new AutoSyncScheduler(() => this.syncOnce(), intervalMs);
|
|
1229
|
+
if (config?.actionQueue) {
|
|
1230
|
+
this.actionProcessor = new ActionProcessor({
|
|
1231
|
+
actionQueue: config.actionQueue,
|
|
1232
|
+
transport,
|
|
1233
|
+
clientId: this._clientId,
|
|
1234
|
+
hlc: this.hlc,
|
|
1235
|
+
maxRetries: config?.maxActionRetries ?? 5
|
|
1236
|
+
});
|
|
1237
|
+
this.actionProcessor.setOnComplete((actionId, result) => {
|
|
1238
|
+
this.emit("onActionComplete", actionId, result);
|
|
1239
|
+
});
|
|
1240
|
+
} else {
|
|
1241
|
+
this.actionProcessor = null;
|
|
1242
|
+
}
|
|
1008
1243
|
if (this.transport.onBroadcast) {
|
|
1009
1244
|
this.transport.onBroadcast((deltas, serverHlc) => {
|
|
1010
1245
|
void this.handleBroadcast(deltas, serverHlc);
|
|
@@ -1029,6 +1264,14 @@ var SyncCoordinator = class {
|
|
|
1029
1264
|
}
|
|
1030
1265
|
}
|
|
1031
1266
|
}
|
|
1267
|
+
/** Readable snapshot of the current sync state. */
|
|
1268
|
+
get state() {
|
|
1269
|
+
return {
|
|
1270
|
+
syncing: this.syncing,
|
|
1271
|
+
lastSyncTime: this._lastSyncTime,
|
|
1272
|
+
lastSyncedHlc: this.lastSyncedHlc
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1032
1275
|
/** Push pending deltas to the gateway via the transport */
|
|
1033
1276
|
async pushToGateway() {
|
|
1034
1277
|
const peekResult = await this.queue.peek(100);
|
|
@@ -1130,26 +1373,11 @@ var SyncCoordinator = class {
|
|
|
1130
1373
|
}
|
|
1131
1374
|
/**
|
|
1132
1375
|
* Start auto-sync: periodic interval + visibility change handler.
|
|
1133
|
-
* Synchronises (push + pull) on tab focus and every
|
|
1376
|
+
* Synchronises (push + pull) on tab focus and every N seconds.
|
|
1134
1377
|
*/
|
|
1135
1378
|
startAutoSync() {
|
|
1136
1379
|
this.transport.connect?.();
|
|
1137
|
-
|
|
1138
|
-
this.syncIntervalId = setInterval(() => {
|
|
1139
|
-
void this.syncOnce();
|
|
1140
|
-
}, intervalMs);
|
|
1141
|
-
this.setupVisibilitySync();
|
|
1142
|
-
}
|
|
1143
|
-
/** Register a visibility change listener to sync on tab focus. */
|
|
1144
|
-
setupVisibilitySync() {
|
|
1145
|
-
this.visibilityHandler = () => {
|
|
1146
|
-
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
1147
|
-
void this.syncOnce();
|
|
1148
|
-
}
|
|
1149
|
-
};
|
|
1150
|
-
if (typeof document !== "undefined") {
|
|
1151
|
-
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
1152
|
-
}
|
|
1380
|
+
this.autoSyncScheduler.start();
|
|
1153
1381
|
}
|
|
1154
1382
|
/**
|
|
1155
1383
|
* Perform initial sync via checkpoint download.
|
|
@@ -1175,6 +1403,7 @@ var SyncCoordinator = class {
|
|
|
1175
1403
|
async syncOnce() {
|
|
1176
1404
|
if (this.syncing) return;
|
|
1177
1405
|
this.syncing = true;
|
|
1406
|
+
this.emit("onSyncStart");
|
|
1178
1407
|
try {
|
|
1179
1408
|
if (this.syncMode !== "pushOnly") {
|
|
1180
1409
|
if (this.lastSyncedHlc === HLC.encode(0, 0)) {
|
|
@@ -1203,84 +1432,20 @@ var SyncCoordinator = class {
|
|
|
1203
1432
|
* @param params - Partial action (connector, actionType, params). ActionId and HLC are generated.
|
|
1204
1433
|
*/
|
|
1205
1434
|
async executeAction(params) {
|
|
1206
|
-
if (!this.
|
|
1435
|
+
if (!this.actionProcessor) {
|
|
1207
1436
|
this.emit("onError", new Error("No action queue configured"));
|
|
1208
1437
|
return;
|
|
1209
1438
|
}
|
|
1210
|
-
|
|
1211
|
-
const { generateActionId } = await import("./src-SLVE5567.js");
|
|
1212
|
-
const actionId = await generateActionId({
|
|
1213
|
-
clientId: this._clientId,
|
|
1214
|
-
hlc,
|
|
1215
|
-
connector: params.connector,
|
|
1216
|
-
actionType: params.actionType,
|
|
1217
|
-
params: params.params
|
|
1218
|
-
});
|
|
1219
|
-
const action = {
|
|
1220
|
-
actionId,
|
|
1221
|
-
clientId: this._clientId,
|
|
1222
|
-
hlc,
|
|
1223
|
-
connector: params.connector,
|
|
1224
|
-
actionType: params.actionType,
|
|
1225
|
-
params: params.params,
|
|
1226
|
-
idempotencyKey: params.idempotencyKey
|
|
1227
|
-
};
|
|
1228
|
-
await this.actionQueue.push(action);
|
|
1229
|
-
void this.processActionQueue();
|
|
1439
|
+
await this.actionProcessor.enqueue(params);
|
|
1230
1440
|
}
|
|
1231
1441
|
/**
|
|
1232
1442
|
* Process pending actions from the action queue.
|
|
1233
1443
|
*
|
|
1234
|
-
*
|
|
1235
|
-
* `transport.executeAction()`, and acks/nacks based on the result.
|
|
1236
|
-
* Dead-letters entries after `maxActionRetries` failures.
|
|
1237
|
-
* Triggers an immediate `syncOnce()` on success to pull fresh state.
|
|
1444
|
+
* Delegates to the ActionProcessor if one is configured.
|
|
1238
1445
|
*/
|
|
1239
1446
|
async processActionQueue() {
|
|
1240
|
-
if (!this.
|
|
1241
|
-
|
|
1242
|
-
if (!peekResult.ok || peekResult.value.length === 0) return;
|
|
1243
|
-
const deadLettered = peekResult.value.filter((e) => e.retryCount >= this.maxActionRetries);
|
|
1244
|
-
const entries = peekResult.value.filter((e) => e.retryCount < this.maxActionRetries);
|
|
1245
|
-
if (deadLettered.length > 0) {
|
|
1246
|
-
console.warn(
|
|
1247
|
-
`[SyncCoordinator] Dead-lettering ${deadLettered.length} actions after ${this.maxActionRetries} retries`
|
|
1248
|
-
);
|
|
1249
|
-
await this.actionQueue.ack(deadLettered.map((e) => e.id));
|
|
1250
|
-
for (const entry of deadLettered) {
|
|
1251
|
-
this.emit("onActionComplete", entry.action.actionId, {
|
|
1252
|
-
actionId: entry.action.actionId,
|
|
1253
|
-
code: "DEAD_LETTERED",
|
|
1254
|
-
message: `Action dead-lettered after ${this.maxActionRetries} retries`,
|
|
1255
|
-
retryable: false
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
if (entries.length === 0) return;
|
|
1260
|
-
const ids = entries.map((e) => e.id);
|
|
1261
|
-
await this.actionQueue.markSending(ids);
|
|
1262
|
-
const transportResult = await this.transport.executeAction({
|
|
1263
|
-
clientId: this._clientId,
|
|
1264
|
-
actions: entries.map((e) => e.action)
|
|
1265
|
-
});
|
|
1266
|
-
if (transportResult.ok) {
|
|
1267
|
-
await this.actionQueue.ack(ids);
|
|
1268
|
-
for (const result of transportResult.value.results) {
|
|
1269
|
-
this.emit("onActionComplete", result.actionId, result);
|
|
1270
|
-
}
|
|
1271
|
-
const retryableIds = [];
|
|
1272
|
-
const ackableIds = [];
|
|
1273
|
-
for (let i = 0; i < transportResult.value.results.length; i++) {
|
|
1274
|
-
const result = transportResult.value.results[i];
|
|
1275
|
-
if (isActionError(result) && result.retryable) {
|
|
1276
|
-
retryableIds.push(ids[i]);
|
|
1277
|
-
} else {
|
|
1278
|
-
ackableIds.push(ids[i]);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
} else {
|
|
1282
|
-
await this.actionQueue.nack(ids);
|
|
1283
|
-
}
|
|
1447
|
+
if (!this.actionProcessor) return;
|
|
1448
|
+
await this.actionProcessor.processQueue();
|
|
1284
1449
|
}
|
|
1285
1450
|
/**
|
|
1286
1451
|
* Discover available connectors and their supported action types.
|
|
@@ -1289,6 +1454,9 @@ var SyncCoordinator = class {
|
|
|
1289
1454
|
* empty connectors when the transport does not support discovery.
|
|
1290
1455
|
*/
|
|
1291
1456
|
async describeActions() {
|
|
1457
|
+
if (this.actionProcessor) {
|
|
1458
|
+
return this.actionProcessor.describeActions();
|
|
1459
|
+
}
|
|
1292
1460
|
if (!this.transport.describeActions) {
|
|
1293
1461
|
return { ok: true, value: { connectors: {} } };
|
|
1294
1462
|
}
|
|
@@ -1301,6 +1469,9 @@ var SyncCoordinator = class {
|
|
|
1301
1469
|
* an empty array when the transport does not support it.
|
|
1302
1470
|
*/
|
|
1303
1471
|
async listConnectorTypes() {
|
|
1472
|
+
if (this.actionProcessor) {
|
|
1473
|
+
return this.actionProcessor.listConnectorTypes();
|
|
1474
|
+
}
|
|
1304
1475
|
if (!this.transport.listConnectorTypes) {
|
|
1305
1476
|
return { ok: true, value: [] };
|
|
1306
1477
|
}
|
|
@@ -1308,16 +1479,7 @@ var SyncCoordinator = class {
|
|
|
1308
1479
|
}
|
|
1309
1480
|
/** Stop auto-sync and clean up listeners */
|
|
1310
1481
|
stopAutoSync() {
|
|
1311
|
-
|
|
1312
|
-
clearInterval(this.syncIntervalId);
|
|
1313
|
-
this.syncIntervalId = null;
|
|
1314
|
-
}
|
|
1315
|
-
if (this.visibilityHandler) {
|
|
1316
|
-
if (typeof document !== "undefined") {
|
|
1317
|
-
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
1318
|
-
}
|
|
1319
|
-
this.visibilityHandler = null;
|
|
1320
|
-
}
|
|
1482
|
+
this.autoSyncScheduler.stop();
|
|
1321
1483
|
this.transport.disconnect?.();
|
|
1322
1484
|
}
|
|
1323
1485
|
};
|
|
@@ -1728,67 +1890,49 @@ var IDBActionQueue = class {
|
|
|
1728
1890
|
|
|
1729
1891
|
// ../client/src/queue/memory-action-queue.ts
|
|
1730
1892
|
var MemoryActionQueue = class {
|
|
1731
|
-
|
|
1732
|
-
counter = 0;
|
|
1893
|
+
outbox = new MemoryOutbox("mem-action");
|
|
1733
1894
|
/** Add an action to the queue. */
|
|
1734
1895
|
async push(action) {
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
status: "pending",
|
|
1739
|
-
createdAt: Date.now(),
|
|
1740
|
-
retryCount: 0
|
|
1741
|
-
};
|
|
1742
|
-
this.entries.set(entry.id, entry);
|
|
1743
|
-
return Ok(entry);
|
|
1896
|
+
const result = await this.outbox.push(action);
|
|
1897
|
+
if (!result.ok) return result;
|
|
1898
|
+
return { ok: true, value: this.toActionEntry(result.value) };
|
|
1744
1899
|
}
|
|
1745
1900
|
/** Peek at pending entries (ordered by createdAt), skipping entries with future retryAfter. */
|
|
1746
1901
|
async peek(limit) {
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
return
|
|
1902
|
+
const result = await this.outbox.peek(limit);
|
|
1903
|
+
if (!result.ok) return result;
|
|
1904
|
+
return { ok: true, value: result.value.map((e) => this.toActionEntry(e)) };
|
|
1750
1905
|
}
|
|
1751
1906
|
/** Mark entries as currently being sent. */
|
|
1752
1907
|
async markSending(ids) {
|
|
1753
|
-
|
|
1754
|
-
const entry = this.entries.get(id);
|
|
1755
|
-
if (entry?.status === "pending") {
|
|
1756
|
-
entry.status = "sending";
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
return Ok(void 0);
|
|
1908
|
+
return this.outbox.markSending(ids);
|
|
1760
1909
|
}
|
|
1761
1910
|
/** Acknowledge successful delivery (removes entries). */
|
|
1762
1911
|
async ack(ids) {
|
|
1763
|
-
|
|
1764
|
-
this.entries.delete(id);
|
|
1765
|
-
}
|
|
1766
|
-
return Ok(void 0);
|
|
1912
|
+
return this.outbox.ack(ids);
|
|
1767
1913
|
}
|
|
1768
1914
|
/** Negative acknowledge — reset to pending with incremented retryCount and exponential backoff. */
|
|
1769
1915
|
async nack(ids) {
|
|
1770
|
-
|
|
1771
|
-
const entry = this.entries.get(id);
|
|
1772
|
-
if (entry) {
|
|
1773
|
-
entry.status = "pending";
|
|
1774
|
-
entry.retryCount++;
|
|
1775
|
-
const backoffMs = Math.min(1e3 * 2 ** entry.retryCount, 3e4);
|
|
1776
|
-
entry.retryAfter = Date.now() + backoffMs;
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
return Ok(void 0);
|
|
1916
|
+
return this.outbox.nack(ids);
|
|
1780
1917
|
}
|
|
1781
1918
|
/** Get the number of pending + sending entries. */
|
|
1782
1919
|
async depth() {
|
|
1783
|
-
|
|
1784
|
-
(e) => e.status === "pending" || e.status === "sending"
|
|
1785
|
-
).length;
|
|
1786
|
-
return Ok(count);
|
|
1920
|
+
return this.outbox.depth();
|
|
1787
1921
|
}
|
|
1788
1922
|
/** Remove all entries. */
|
|
1789
1923
|
async clear() {
|
|
1790
|
-
this.
|
|
1791
|
-
|
|
1924
|
+
return this.outbox.clear();
|
|
1925
|
+
}
|
|
1926
|
+
/** Convert a generic OutboxEntry to the ActionQueue-specific ActionQueueEntry shape. */
|
|
1927
|
+
toActionEntry(entry) {
|
|
1928
|
+
return {
|
|
1929
|
+
id: entry.id,
|
|
1930
|
+
action: entry.item,
|
|
1931
|
+
status: entry.status,
|
|
1932
|
+
createdAt: entry.createdAt,
|
|
1933
|
+
retryCount: entry.retryCount,
|
|
1934
|
+
retryAfter: entry.retryAfter
|
|
1935
|
+
};
|
|
1792
1936
|
}
|
|
1793
1937
|
};
|
|
1794
1938
|
|
|
@@ -2106,6 +2250,8 @@ var WebSocketTransport = class {
|
|
|
2106
2250
|
}
|
|
2107
2251
|
};
|
|
2108
2252
|
export {
|
|
2253
|
+
ActionProcessor,
|
|
2254
|
+
AutoSyncScheduler,
|
|
2109
2255
|
DbError,
|
|
2110
2256
|
HttpTransport,
|
|
2111
2257
|
IDBActionQueue,
|
|
@@ -2113,6 +2259,7 @@ export {
|
|
|
2113
2259
|
LocalDB,
|
|
2114
2260
|
LocalTransport,
|
|
2115
2261
|
MemoryActionQueue,
|
|
2262
|
+
MemoryOutbox,
|
|
2116
2263
|
MemoryQueue,
|
|
2117
2264
|
SchemaSynchroniser,
|
|
2118
2265
|
SyncCoordinator,
|