lakesync 0.1.6 → 0.2.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/adapter-types-DwsQGQS4.d.ts +94 -0
- package/dist/adapter.d.ts +202 -63
- package/dist/adapter.js +20 -5
- package/dist/analyst.js +2 -2
- package/dist/{base-poller-BpUyuG2R.d.ts → base-poller-Y7ORYgUv.d.ts} +78 -19
- package/dist/catalogue.d.ts +1 -1
- package/dist/catalogue.js +3 -3
- package/dist/{chunk-P3FT7QCW.js → chunk-4SG66H5K.js} +395 -252
- package/dist/chunk-4SG66H5K.js.map +1 -0
- package/dist/{chunk-GUJWMK5P.js → chunk-C4KD6YKP.js} +419 -380
- package/dist/chunk-C4KD6YKP.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-IRJ4QRWV.js → chunk-FIIHPQMQ.js} +396 -209
- package/dist/chunk-FIIHPQMQ.js.map +1 -0
- package/dist/{chunk-UAUQGP3B.js → chunk-U2NV4DUX.js} +2 -2
- package/dist/{chunk-NCZYFZ3B.js → chunk-XVP5DJJ7.js} +44 -18
- package/dist/{chunk-NCZYFZ3B.js.map → chunk-XVP5DJJ7.js.map} +1 -1
- package/dist/{chunk-FHVTUKXL.js → chunk-YHYBLU6W.js} +2 -2
- package/dist/{chunk-QMS7TGFL.js → chunk-ZNY4DSFU.js} +29 -15
- package/dist/{chunk-QMS7TGFL.js.map → chunk-ZNY4DSFU.js.map} +1 -1
- package/dist/{chunk-SF7Y6ZUA.js → chunk-ZU7RC7CT.js} +2 -2
- package/dist/client.d.ts +186 -17
- package/dist/client.js +456 -188
- package/dist/client.js.map +1 -1
- package/dist/compactor.d.ts +2 -2
- package/dist/compactor.js +4 -4
- package/dist/connector-jira.d.ts +13 -3
- package/dist/connector-jira.js +7 -3
- package/dist/connector-salesforce.d.ts +13 -3
- package/dist/connector-salesforce.js +7 -3
- package/dist/{coordinator-D32a5rNk.d.ts → coordinator-eGmZMnJ_.d.ts} +120 -30
- package/dist/create-poller-Cc2MGfhh.d.ts +55 -0
- package/dist/factory-DFfR-030.d.ts +33 -0
- package/dist/gateway-server.d.ts +516 -119
- package/dist/gateway-server.js +1201 -4035
- package/dist/gateway-server.js.map +1 -1
- package/dist/gateway.d.ts +69 -106
- package/dist/gateway.js +13 -6
- package/dist/index.d.ts +65 -58
- package/dist/index.js +18 -4
- 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-CPTgO9jv.d.ts → registry-Dd8JuW8T.d.ts} +19 -4
- package/dist/{gateway-Bpvatd9n.d.ts → request-handler-B1I5xDOx.d.ts} +193 -20
- package/dist/{resolver-CbuXm3nB.d.ts → resolver-CXxmC0jR.d.ts} +1 -1
- package/dist/{src-RHKJFQKR.js → src-WU7IBVC4.js} +19 -5
- package/dist/{types-CLlD4XOy.d.ts → types-BdGBv2ba.d.ts} +17 -2
- package/dist/{types-D-E0VrfS.d.ts → types-D2C9jTbL.d.ts} +39 -22
- package/package.json +1 -1
- package/dist/auth-CAVutXzx.d.ts +0 -30
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-GUJWMK5P.js.map +0 -1
- package/dist/chunk-IRJ4QRWV.js.map +0 -1
- package/dist/chunk-P3FT7QCW.js.map +0 -1
- package/dist/db-types-BlN-4KbQ.d.ts +0 -29
- package/dist/src-CLCALYDT.js +0 -25
- package/dist/src-FPJQYQNA.js +0 -27
- package/dist/src-FPJQYQNA.js.map +0 -1
- package/dist/src-RHKJFQKR.js.map +0 -1
- package/dist/types-DSC_EiwR.d.ts +0 -45
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-UAUQGP3B.js.map → chunk-U2NV4DUX.js.map} +0 -0
- /package/dist/{chunk-FHVTUKXL.js.map → chunk-YHYBLU6W.js.map} +0 -0
- /package/dist/{chunk-SF7Y6ZUA.js.map → chunk-ZU7RC7CT.js.map} +0 -0
- /package/dist/{src-CLCALYDT.js.map → src-WU7IBVC4.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-YHYBLU6W.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-4SG66H5K.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-WU7IBVC4.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,84 @@ 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
|
+
|
|
1004
|
+
// ../client/src/sync/strategy.ts
|
|
1005
|
+
var PullFirstStrategy = class {
|
|
1006
|
+
async execute(ctx) {
|
|
1007
|
+
if (ctx.syncMode !== "pushOnly") {
|
|
1008
|
+
if (ctx.isFirstSync) {
|
|
1009
|
+
await ctx.initialSync();
|
|
1010
|
+
}
|
|
1011
|
+
await ctx.pull();
|
|
1012
|
+
}
|
|
1013
|
+
if (ctx.syncMode !== "pullOnly") {
|
|
1014
|
+
await ctx.push();
|
|
1015
|
+
}
|
|
1016
|
+
await ctx.processActions();
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
var PushFirstStrategy = class {
|
|
1020
|
+
async execute(ctx) {
|
|
1021
|
+
if (ctx.syncMode !== "pullOnly") {
|
|
1022
|
+
await ctx.push();
|
|
1023
|
+
}
|
|
1024
|
+
if (ctx.syncMode !== "pushOnly") {
|
|
1025
|
+
if (ctx.isFirstSync) {
|
|
1026
|
+
await ctx.initialSync();
|
|
1027
|
+
}
|
|
1028
|
+
await ctx.pull();
|
|
1029
|
+
}
|
|
1030
|
+
await ctx.processActions();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
780
1034
|
// ../client/src/sync/tracker.ts
|
|
781
1035
|
function rowWithoutId(row) {
|
|
782
1036
|
const result = {};
|
|
@@ -977,17 +1231,18 @@ var SyncCoordinator = class {
|
|
|
977
1231
|
_clientId;
|
|
978
1232
|
maxRetries;
|
|
979
1233
|
syncMode;
|
|
980
|
-
autoSyncIntervalMs;
|
|
981
|
-
realtimeHeartbeatMs;
|
|
982
1234
|
lastSyncedHlc = HLC.encode(0, 0);
|
|
983
1235
|
_lastSyncTime = null;
|
|
984
|
-
syncIntervalId = null;
|
|
985
|
-
visibilityHandler = null;
|
|
986
1236
|
syncing = false;
|
|
987
|
-
|
|
988
|
-
|
|
1237
|
+
_online = true;
|
|
1238
|
+
onlineHandler = null;
|
|
1239
|
+
offlineHandler = null;
|
|
1240
|
+
strategy;
|
|
1241
|
+
autoSyncScheduler;
|
|
1242
|
+
actionProcessor;
|
|
989
1243
|
listeners = {
|
|
990
1244
|
onChange: [],
|
|
1245
|
+
onSyncStart: [],
|
|
991
1246
|
onSyncComplete: [],
|
|
992
1247
|
onError: [],
|
|
993
1248
|
onActionComplete: []
|
|
@@ -1000,11 +1255,26 @@ var SyncCoordinator = class {
|
|
|
1000
1255
|
this._clientId = config?.clientId ?? `client-${crypto.randomUUID()}`;
|
|
1001
1256
|
this.maxRetries = config?.maxRetries ?? 10;
|
|
1002
1257
|
this.syncMode = config?.syncMode ?? "full";
|
|
1003
|
-
this.
|
|
1004
|
-
this.realtimeHeartbeatMs = config?.realtimeHeartbeatMs ?? REALTIME_HEARTBEAT_MS;
|
|
1005
|
-
this.actionQueue = config?.actionQueue ?? null;
|
|
1006
|
-
this.maxActionRetries = config?.maxActionRetries ?? 5;
|
|
1258
|
+
this.strategy = config?.strategy ?? new PullFirstStrategy();
|
|
1007
1259
|
this.tracker = new SyncTracker(db, this.queue, this.hlc, this._clientId);
|
|
1260
|
+
const autoSyncIntervalMs = config?.autoSyncIntervalMs ?? AUTO_SYNC_INTERVAL_MS;
|
|
1261
|
+
const realtimeHeartbeatMs = config?.realtimeHeartbeatMs ?? REALTIME_HEARTBEAT_MS;
|
|
1262
|
+
const intervalMs = transport.supportsRealtime ? realtimeHeartbeatMs : autoSyncIntervalMs;
|
|
1263
|
+
this.autoSyncScheduler = new AutoSyncScheduler(() => this.syncOnce(), intervalMs);
|
|
1264
|
+
if (config?.actionQueue) {
|
|
1265
|
+
this.actionProcessor = new ActionProcessor({
|
|
1266
|
+
actionQueue: config.actionQueue,
|
|
1267
|
+
transport,
|
|
1268
|
+
clientId: this._clientId,
|
|
1269
|
+
hlc: this.hlc,
|
|
1270
|
+
maxRetries: config?.maxActionRetries ?? 5
|
|
1271
|
+
});
|
|
1272
|
+
this.actionProcessor.setOnComplete((actionId, result) => {
|
|
1273
|
+
this.emit("onActionComplete", actionId, result);
|
|
1274
|
+
});
|
|
1275
|
+
} else {
|
|
1276
|
+
this.actionProcessor = null;
|
|
1277
|
+
}
|
|
1008
1278
|
if (this.transport.onBroadcast) {
|
|
1009
1279
|
this.transport.onBroadcast((deltas, serverHlc) => {
|
|
1010
1280
|
void this.handleBroadcast(deltas, serverHlc);
|
|
@@ -1017,9 +1287,8 @@ var SyncCoordinator = class {
|
|
|
1017
1287
|
}
|
|
1018
1288
|
/** Remove an event listener */
|
|
1019
1289
|
off(event, listener) {
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
if (idx !== -1) arr.splice(idx, 1);
|
|
1290
|
+
const listeners = this.listeners;
|
|
1291
|
+
listeners[event] = this.listeners[event].filter((fn) => fn !== listener);
|
|
1023
1292
|
}
|
|
1024
1293
|
emit(event, ...args) {
|
|
1025
1294
|
for (const fn of this.listeners[event]) {
|
|
@@ -1029,6 +1298,18 @@ var SyncCoordinator = class {
|
|
|
1029
1298
|
}
|
|
1030
1299
|
}
|
|
1031
1300
|
}
|
|
1301
|
+
/** Readable snapshot of the current sync state. */
|
|
1302
|
+
get state() {
|
|
1303
|
+
return {
|
|
1304
|
+
syncing: this.syncing,
|
|
1305
|
+
lastSyncTime: this._lastSyncTime,
|
|
1306
|
+
lastSyncedHlc: this.lastSyncedHlc
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
/** Whether the client believes it is online. */
|
|
1310
|
+
get isOnline() {
|
|
1311
|
+
return this._online;
|
|
1312
|
+
}
|
|
1032
1313
|
/** Push pending deltas to the gateway via the transport */
|
|
1033
1314
|
async pushToGateway() {
|
|
1034
1315
|
const peekResult = await this.queue.peek(100);
|
|
@@ -1130,26 +1411,14 @@ var SyncCoordinator = class {
|
|
|
1130
1411
|
}
|
|
1131
1412
|
/**
|
|
1132
1413
|
* Start auto-sync: periodic interval + visibility change handler.
|
|
1133
|
-
* Synchronises (push + pull) on tab focus and every
|
|
1414
|
+
* Synchronises (push + pull) on tab focus and every N seconds.
|
|
1415
|
+
* Registers online/offline listeners to skip sync when offline
|
|
1416
|
+
* and trigger an immediate sync on reconnect.
|
|
1134
1417
|
*/
|
|
1135
1418
|
startAutoSync() {
|
|
1136
1419
|
this.transport.connect?.();
|
|
1137
|
-
|
|
1138
|
-
this.
|
|
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
|
-
}
|
|
1420
|
+
this.autoSyncScheduler.start();
|
|
1421
|
+
this.setupOnlineListeners();
|
|
1153
1422
|
}
|
|
1154
1423
|
/**
|
|
1155
1424
|
* Perform initial sync via checkpoint download.
|
|
@@ -1171,21 +1440,25 @@ var SyncCoordinator = class {
|
|
|
1171
1440
|
this.lastSyncedHlc = snapshotHlc;
|
|
1172
1441
|
this._lastSyncTime = /* @__PURE__ */ new Date();
|
|
1173
1442
|
}
|
|
1443
|
+
/** Build a {@link SyncContext} exposing sync operations for the current cycle. */
|
|
1444
|
+
createSyncContext() {
|
|
1445
|
+
return {
|
|
1446
|
+
isFirstSync: this.lastSyncedHlc === HLC.encode(0, 0),
|
|
1447
|
+
syncMode: this.syncMode,
|
|
1448
|
+
initialSync: () => this.initialSync(),
|
|
1449
|
+
pull: () => this.pullFromGateway(),
|
|
1450
|
+
push: () => this.pushToGateway(),
|
|
1451
|
+
processActions: () => this.processActionQueue()
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1174
1454
|
/** Perform a single sync cycle (push + pull + actions, depending on syncMode). */
|
|
1175
1455
|
async syncOnce() {
|
|
1176
1456
|
if (this.syncing) return;
|
|
1457
|
+
if (!this._online) return;
|
|
1177
1458
|
this.syncing = true;
|
|
1459
|
+
this.emit("onSyncStart");
|
|
1178
1460
|
try {
|
|
1179
|
-
|
|
1180
|
-
if (this.lastSyncedHlc === HLC.encode(0, 0)) {
|
|
1181
|
-
await this.initialSync();
|
|
1182
|
-
}
|
|
1183
|
-
await this.pullFromGateway();
|
|
1184
|
-
}
|
|
1185
|
-
if (this.syncMode !== "pullOnly") {
|
|
1186
|
-
await this.pushToGateway();
|
|
1187
|
-
}
|
|
1188
|
-
await this.processActionQueue();
|
|
1461
|
+
await this.strategy.execute(this.createSyncContext());
|
|
1189
1462
|
this.emit("onSyncComplete");
|
|
1190
1463
|
} catch (err) {
|
|
1191
1464
|
this.emit("onError", err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1203,84 +1476,20 @@ var SyncCoordinator = class {
|
|
|
1203
1476
|
* @param params - Partial action (connector, actionType, params). ActionId and HLC are generated.
|
|
1204
1477
|
*/
|
|
1205
1478
|
async executeAction(params) {
|
|
1206
|
-
if (!this.
|
|
1479
|
+
if (!this.actionProcessor) {
|
|
1207
1480
|
this.emit("onError", new Error("No action queue configured"));
|
|
1208
1481
|
return;
|
|
1209
1482
|
}
|
|
1210
|
-
|
|
1211
|
-
const { generateActionId } = await import("./src-RHKJFQKR.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();
|
|
1483
|
+
await this.actionProcessor.enqueue(params);
|
|
1230
1484
|
}
|
|
1231
1485
|
/**
|
|
1232
1486
|
* Process pending actions from the action queue.
|
|
1233
1487
|
*
|
|
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.
|
|
1488
|
+
* Delegates to the ActionProcessor if one is configured.
|
|
1238
1489
|
*/
|
|
1239
1490
|
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
|
-
}
|
|
1491
|
+
if (!this.actionProcessor) return;
|
|
1492
|
+
await this.actionProcessor.processQueue();
|
|
1284
1493
|
}
|
|
1285
1494
|
/**
|
|
1286
1495
|
* Discover available connectors and their supported action types.
|
|
@@ -1289,6 +1498,9 @@ var SyncCoordinator = class {
|
|
|
1289
1498
|
* empty connectors when the transport does not support discovery.
|
|
1290
1499
|
*/
|
|
1291
1500
|
async describeActions() {
|
|
1501
|
+
if (this.actionProcessor) {
|
|
1502
|
+
return this.actionProcessor.describeActions();
|
|
1503
|
+
}
|
|
1292
1504
|
if (!this.transport.describeActions) {
|
|
1293
1505
|
return { ok: true, value: { connectors: {} } };
|
|
1294
1506
|
}
|
|
@@ -1301,6 +1513,9 @@ var SyncCoordinator = class {
|
|
|
1301
1513
|
* an empty array when the transport does not support it.
|
|
1302
1514
|
*/
|
|
1303
1515
|
async listConnectorTypes() {
|
|
1516
|
+
if (this.actionProcessor) {
|
|
1517
|
+
return this.actionProcessor.listConnectorTypes();
|
|
1518
|
+
}
|
|
1304
1519
|
if (!this.transport.listConnectorTypes) {
|
|
1305
1520
|
return { ok: true, value: [] };
|
|
1306
1521
|
}
|
|
@@ -1308,17 +1523,40 @@ var SyncCoordinator = class {
|
|
|
1308
1523
|
}
|
|
1309
1524
|
/** Stop auto-sync and clean up listeners */
|
|
1310
1525
|
stopAutoSync() {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1526
|
+
this.autoSyncScheduler.stop();
|
|
1527
|
+
this.teardownOnlineListeners();
|
|
1528
|
+
this.transport.disconnect?.();
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Register window online/offline event listeners.
|
|
1532
|
+
* Guards all browser API access with typeof checks for Node/SSR safety.
|
|
1533
|
+
*/
|
|
1534
|
+
setupOnlineListeners() {
|
|
1535
|
+
if (typeof window === "undefined") return;
|
|
1536
|
+
if (typeof navigator !== "undefined" && typeof navigator.onLine === "boolean") {
|
|
1537
|
+
this._online = navigator.onLine;
|
|
1314
1538
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1539
|
+
this.onlineHandler = () => {
|
|
1540
|
+
this._online = true;
|
|
1541
|
+
void this.syncOnce();
|
|
1542
|
+
};
|
|
1543
|
+
this.offlineHandler = () => {
|
|
1544
|
+
this._online = false;
|
|
1545
|
+
};
|
|
1546
|
+
window.addEventListener("online", this.onlineHandler);
|
|
1547
|
+
window.addEventListener("offline", this.offlineHandler);
|
|
1548
|
+
}
|
|
1549
|
+
/** Remove online/offline listeners. */
|
|
1550
|
+
teardownOnlineListeners() {
|
|
1551
|
+
if (typeof window === "undefined") return;
|
|
1552
|
+
if (this.onlineHandler) {
|
|
1553
|
+
window.removeEventListener("online", this.onlineHandler);
|
|
1554
|
+
this.onlineHandler = null;
|
|
1555
|
+
}
|
|
1556
|
+
if (this.offlineHandler) {
|
|
1557
|
+
window.removeEventListener("offline", this.offlineHandler);
|
|
1558
|
+
this.offlineHandler = null;
|
|
1320
1559
|
}
|
|
1321
|
-
this.transport.disconnect?.();
|
|
1322
1560
|
}
|
|
1323
1561
|
};
|
|
1324
1562
|
|
|
@@ -1327,29 +1565,52 @@ var HttpTransport = class {
|
|
|
1327
1565
|
baseUrl;
|
|
1328
1566
|
gatewayId;
|
|
1329
1567
|
token;
|
|
1568
|
+
getToken;
|
|
1330
1569
|
_fetch;
|
|
1331
1570
|
constructor(config) {
|
|
1332
1571
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1333
1572
|
this.gatewayId = config.gatewayId;
|
|
1334
1573
|
this.token = config.token;
|
|
1574
|
+
this.getToken = config.getToken;
|
|
1335
1575
|
this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1336
1576
|
}
|
|
1577
|
+
/** Resolve the current bearer token, preferring getToken callback over static token. */
|
|
1578
|
+
async resolveToken() {
|
|
1579
|
+
if (this.getToken) {
|
|
1580
|
+
return this.getToken();
|
|
1581
|
+
}
|
|
1582
|
+
return this.token;
|
|
1583
|
+
}
|
|
1337
1584
|
/**
|
|
1338
1585
|
* Push local deltas to the remote gateway.
|
|
1339
1586
|
*
|
|
1340
1587
|
* Sends a POST request with the push payload as BigInt-safe JSON.
|
|
1588
|
+
* On 401, if `getToken` is configured, refreshes the token and retries once.
|
|
1341
1589
|
*/
|
|
1342
1590
|
async push(msg) {
|
|
1343
1591
|
const url = `${this.baseUrl}/sync/${this.gatewayId}/push`;
|
|
1592
|
+
const body = JSON.stringify(msg, bigintReplacer);
|
|
1344
1593
|
try {
|
|
1345
|
-
|
|
1594
|
+
let token = await this.resolveToken();
|
|
1595
|
+
let response = await this._fetch(url, {
|
|
1346
1596
|
method: "POST",
|
|
1347
1597
|
headers: {
|
|
1348
1598
|
"Content-Type": "application/json",
|
|
1349
|
-
Authorization: `Bearer ${
|
|
1599
|
+
Authorization: `Bearer ${token}`
|
|
1350
1600
|
},
|
|
1351
|
-
body
|
|
1601
|
+
body
|
|
1352
1602
|
});
|
|
1603
|
+
if (response.status === 401 && this.getToken) {
|
|
1604
|
+
token = await this.getToken();
|
|
1605
|
+
response = await this._fetch(url, {
|
|
1606
|
+
method: "POST",
|
|
1607
|
+
headers: {
|
|
1608
|
+
"Content-Type": "application/json",
|
|
1609
|
+
Authorization: `Bearer ${token}`
|
|
1610
|
+
},
|
|
1611
|
+
body
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1353
1614
|
if (!response.ok) {
|
|
1354
1615
|
const text = await response.text().catch(() => "Unknown error");
|
|
1355
1616
|
return Err(new LakeSyncError(`Push failed (${response.status}): ${text}`, "TRANSPORT_ERROR"));
|
|
@@ -1366,6 +1627,7 @@ var HttpTransport = class {
|
|
|
1366
1627
|
* Pull remote deltas from the gateway.
|
|
1367
1628
|
*
|
|
1368
1629
|
* Sends a GET request with query parameters for the pull cursor.
|
|
1630
|
+
* On 401, if `getToken` is configured, refreshes the token and retries once.
|
|
1369
1631
|
*/
|
|
1370
1632
|
async pull(msg) {
|
|
1371
1633
|
const params = new URLSearchParams({
|
|
@@ -1378,12 +1640,22 @@ var HttpTransport = class {
|
|
|
1378
1640
|
}
|
|
1379
1641
|
const url = `${this.baseUrl}/sync/${this.gatewayId}/pull?${params}`;
|
|
1380
1642
|
try {
|
|
1381
|
-
|
|
1643
|
+
let token = await this.resolveToken();
|
|
1644
|
+
let response = await this._fetch(url, {
|
|
1382
1645
|
method: "GET",
|
|
1383
1646
|
headers: {
|
|
1384
|
-
Authorization: `Bearer ${
|
|
1647
|
+
Authorization: `Bearer ${token}`
|
|
1385
1648
|
}
|
|
1386
1649
|
});
|
|
1650
|
+
if (response.status === 401 && this.getToken) {
|
|
1651
|
+
token = await this.getToken();
|
|
1652
|
+
response = await this._fetch(url, {
|
|
1653
|
+
method: "GET",
|
|
1654
|
+
headers: {
|
|
1655
|
+
Authorization: `Bearer ${token}`
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1387
1659
|
if (!response.ok) {
|
|
1388
1660
|
const text = await response.text().catch(() => "Unknown error");
|
|
1389
1661
|
return Err(new LakeSyncError(`Pull failed (${response.status}): ${text}`, "TRANSPORT_ERROR"));
|
|
@@ -1403,14 +1675,16 @@ var HttpTransport = class {
|
|
|
1403
1675
|
*/
|
|
1404
1676
|
async executeAction(msg) {
|
|
1405
1677
|
const url = `${this.baseUrl}/sync/${this.gatewayId}/action`;
|
|
1678
|
+
const body = JSON.stringify(msg, bigintReplacer);
|
|
1406
1679
|
try {
|
|
1680
|
+
const token = await this.resolveToken();
|
|
1407
1681
|
const response = await this._fetch(url, {
|
|
1408
1682
|
method: "POST",
|
|
1409
1683
|
headers: {
|
|
1410
1684
|
"Content-Type": "application/json",
|
|
1411
|
-
Authorization: `Bearer ${
|
|
1685
|
+
Authorization: `Bearer ${token}`
|
|
1412
1686
|
},
|
|
1413
|
-
body
|
|
1687
|
+
body
|
|
1414
1688
|
});
|
|
1415
1689
|
if (!response.ok) {
|
|
1416
1690
|
const text = await response.text().catch(() => "Unknown error");
|
|
@@ -1432,10 +1706,11 @@ var HttpTransport = class {
|
|
|
1432
1706
|
async describeActions() {
|
|
1433
1707
|
const url = `${this.baseUrl}/sync/${this.gatewayId}/actions`;
|
|
1434
1708
|
try {
|
|
1709
|
+
const token = await this.resolveToken();
|
|
1435
1710
|
const response = await this._fetch(url, {
|
|
1436
1711
|
method: "GET",
|
|
1437
1712
|
headers: {
|
|
1438
|
-
Authorization: `Bearer ${
|
|
1713
|
+
Authorization: `Bearer ${token}`
|
|
1439
1714
|
}
|
|
1440
1715
|
});
|
|
1441
1716
|
if (!response.ok) {
|
|
@@ -1495,10 +1770,11 @@ var HttpTransport = class {
|
|
|
1495
1770
|
async checkpoint() {
|
|
1496
1771
|
const url = `${this.baseUrl}/sync/${this.gatewayId}/checkpoint`;
|
|
1497
1772
|
try {
|
|
1773
|
+
const token = await this.resolveToken();
|
|
1498
1774
|
const response = await this._fetch(url, {
|
|
1499
1775
|
method: "GET",
|
|
1500
1776
|
headers: {
|
|
1501
|
-
Authorization: `Bearer ${
|
|
1777
|
+
Authorization: `Bearer ${token}`,
|
|
1502
1778
|
Accept: "application/x-lakesync-checkpoint-stream"
|
|
1503
1779
|
}
|
|
1504
1780
|
});
|
|
@@ -1728,67 +2004,49 @@ var IDBActionQueue = class {
|
|
|
1728
2004
|
|
|
1729
2005
|
// ../client/src/queue/memory-action-queue.ts
|
|
1730
2006
|
var MemoryActionQueue = class {
|
|
1731
|
-
|
|
1732
|
-
counter = 0;
|
|
2007
|
+
outbox = new MemoryOutbox("mem-action");
|
|
1733
2008
|
/** Add an action to the queue. */
|
|
1734
2009
|
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);
|
|
2010
|
+
const result = await this.outbox.push(action);
|
|
2011
|
+
if (!result.ok) return result;
|
|
2012
|
+
return { ok: true, value: this.toActionEntry(result.value) };
|
|
1744
2013
|
}
|
|
1745
2014
|
/** Peek at pending entries (ordered by createdAt), skipping entries with future retryAfter. */
|
|
1746
2015
|
async peek(limit) {
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
return
|
|
2016
|
+
const result = await this.outbox.peek(limit);
|
|
2017
|
+
if (!result.ok) return result;
|
|
2018
|
+
return { ok: true, value: result.value.map((e) => this.toActionEntry(e)) };
|
|
1750
2019
|
}
|
|
1751
2020
|
/** Mark entries as currently being sent. */
|
|
1752
2021
|
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);
|
|
2022
|
+
return this.outbox.markSending(ids);
|
|
1760
2023
|
}
|
|
1761
2024
|
/** Acknowledge successful delivery (removes entries). */
|
|
1762
2025
|
async ack(ids) {
|
|
1763
|
-
|
|
1764
|
-
this.entries.delete(id);
|
|
1765
|
-
}
|
|
1766
|
-
return Ok(void 0);
|
|
2026
|
+
return this.outbox.ack(ids);
|
|
1767
2027
|
}
|
|
1768
2028
|
/** Negative acknowledge — reset to pending with incremented retryCount and exponential backoff. */
|
|
1769
2029
|
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);
|
|
2030
|
+
return this.outbox.nack(ids);
|
|
1780
2031
|
}
|
|
1781
2032
|
/** Get the number of pending + sending entries. */
|
|
1782
2033
|
async depth() {
|
|
1783
|
-
|
|
1784
|
-
(e) => e.status === "pending" || e.status === "sending"
|
|
1785
|
-
).length;
|
|
1786
|
-
return Ok(count);
|
|
2034
|
+
return this.outbox.depth();
|
|
1787
2035
|
}
|
|
1788
2036
|
/** Remove all entries. */
|
|
1789
2037
|
async clear() {
|
|
1790
|
-
this.
|
|
1791
|
-
|
|
2038
|
+
return this.outbox.clear();
|
|
2039
|
+
}
|
|
2040
|
+
/** Convert a generic OutboxEntry to the ActionQueue-specific ActionQueueEntry shape. */
|
|
2041
|
+
toActionEntry(entry) {
|
|
2042
|
+
return {
|
|
2043
|
+
id: entry.id,
|
|
2044
|
+
action: entry.item,
|
|
2045
|
+
status: entry.status,
|
|
2046
|
+
createdAt: entry.createdAt,
|
|
2047
|
+
retryCount: entry.retryCount,
|
|
2048
|
+
retryAfter: entry.retryAfter
|
|
2049
|
+
};
|
|
1792
2050
|
}
|
|
1793
2051
|
};
|
|
1794
2052
|
|
|
@@ -1902,6 +2160,10 @@ var LocalTransport = class {
|
|
|
1902
2160
|
}
|
|
1903
2161
|
return Ok(this.gateway.describeActions());
|
|
1904
2162
|
}
|
|
2163
|
+
/** List available connector types — not supported by local transport. */
|
|
2164
|
+
async listConnectorTypes() {
|
|
2165
|
+
return Ok([]);
|
|
2166
|
+
}
|
|
1905
2167
|
};
|
|
1906
2168
|
|
|
1907
2169
|
// ../client/src/sync/transport-ws.ts
|
|
@@ -1946,7 +2208,7 @@ var WebSocketTransport = class {
|
|
|
1946
2208
|
connect() {
|
|
1947
2209
|
if (this.ws) return;
|
|
1948
2210
|
this.intentionalClose = false;
|
|
1949
|
-
this.openWebSocket();
|
|
2211
|
+
void this.openWebSocket();
|
|
1950
2212
|
}
|
|
1951
2213
|
/** Close the WebSocket connection and stop reconnecting. */
|
|
1952
2214
|
disconnect() {
|
|
@@ -2017,8 +2279,9 @@ var WebSocketTransport = class {
|
|
|
2017
2279
|
// -----------------------------------------------------------------------
|
|
2018
2280
|
// Internal
|
|
2019
2281
|
// -----------------------------------------------------------------------
|
|
2020
|
-
openWebSocket() {
|
|
2021
|
-
const
|
|
2282
|
+
async openWebSocket() {
|
|
2283
|
+
const token = this.config.getToken ? await this.config.getToken() : this.config.token;
|
|
2284
|
+
const url = `${this.config.url}?token=${encodeURIComponent(token)}`;
|
|
2022
2285
|
this.ws = new WebSocket(url);
|
|
2023
2286
|
this.ws.binaryType = "arraybuffer";
|
|
2024
2287
|
this.ws.onopen = () => {
|
|
@@ -2075,7 +2338,7 @@ var WebSocketTransport = class {
|
|
|
2075
2338
|
this.reconnectAttempts++;
|
|
2076
2339
|
this.reconnectTimer = setTimeout(() => {
|
|
2077
2340
|
this.reconnectTimer = null;
|
|
2078
|
-
this.openWebSocket();
|
|
2341
|
+
void this.openWebSocket();
|
|
2079
2342
|
}, delay);
|
|
2080
2343
|
}
|
|
2081
2344
|
sendAndAwaitResponse(frame) {
|
|
@@ -2106,6 +2369,8 @@ var WebSocketTransport = class {
|
|
|
2106
2369
|
}
|
|
2107
2370
|
};
|
|
2108
2371
|
export {
|
|
2372
|
+
ActionProcessor,
|
|
2373
|
+
AutoSyncScheduler,
|
|
2109
2374
|
DbError,
|
|
2110
2375
|
HttpTransport,
|
|
2111
2376
|
IDBActionQueue,
|
|
@@ -2113,7 +2378,10 @@ export {
|
|
|
2113
2378
|
LocalDB,
|
|
2114
2379
|
LocalTransport,
|
|
2115
2380
|
MemoryActionQueue,
|
|
2381
|
+
MemoryOutbox,
|
|
2116
2382
|
MemoryQueue,
|
|
2383
|
+
PullFirstStrategy,
|
|
2384
|
+
PushFirstStrategy,
|
|
2117
2385
|
SchemaSynchroniser,
|
|
2118
2386
|
SyncCoordinator,
|
|
2119
2387
|
SyncTracker,
|