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.
Files changed (70) hide show
  1. package/dist/adapter-types-DwsQGQS4.d.ts +94 -0
  2. package/dist/adapter.d.ts +202 -63
  3. package/dist/adapter.js +20 -5
  4. package/dist/analyst.js +2 -2
  5. package/dist/{base-poller-BpUyuG2R.d.ts → base-poller-Y7ORYgUv.d.ts} +78 -19
  6. package/dist/catalogue.d.ts +1 -1
  7. package/dist/catalogue.js +3 -3
  8. package/dist/{chunk-P3FT7QCW.js → chunk-4SG66H5K.js} +395 -252
  9. package/dist/chunk-4SG66H5K.js.map +1 -0
  10. package/dist/{chunk-GUJWMK5P.js → chunk-C4KD6YKP.js} +419 -380
  11. package/dist/chunk-C4KD6YKP.js.map +1 -0
  12. package/dist/chunk-DGUM43GV.js +11 -0
  13. package/dist/{chunk-IRJ4QRWV.js → chunk-FIIHPQMQ.js} +396 -209
  14. package/dist/chunk-FIIHPQMQ.js.map +1 -0
  15. package/dist/{chunk-UAUQGP3B.js → chunk-U2NV4DUX.js} +2 -2
  16. package/dist/{chunk-NCZYFZ3B.js → chunk-XVP5DJJ7.js} +44 -18
  17. package/dist/{chunk-NCZYFZ3B.js.map → chunk-XVP5DJJ7.js.map} +1 -1
  18. package/dist/{chunk-FHVTUKXL.js → chunk-YHYBLU6W.js} +2 -2
  19. package/dist/{chunk-QMS7TGFL.js → chunk-ZNY4DSFU.js} +29 -15
  20. package/dist/{chunk-QMS7TGFL.js.map → chunk-ZNY4DSFU.js.map} +1 -1
  21. package/dist/{chunk-SF7Y6ZUA.js → chunk-ZU7RC7CT.js} +2 -2
  22. package/dist/client.d.ts +186 -17
  23. package/dist/client.js +456 -188
  24. package/dist/client.js.map +1 -1
  25. package/dist/compactor.d.ts +2 -2
  26. package/dist/compactor.js +4 -4
  27. package/dist/connector-jira.d.ts +13 -3
  28. package/dist/connector-jira.js +7 -3
  29. package/dist/connector-salesforce.d.ts +13 -3
  30. package/dist/connector-salesforce.js +7 -3
  31. package/dist/{coordinator-D32a5rNk.d.ts → coordinator-eGmZMnJ_.d.ts} +120 -30
  32. package/dist/create-poller-Cc2MGfhh.d.ts +55 -0
  33. package/dist/factory-DFfR-030.d.ts +33 -0
  34. package/dist/gateway-server.d.ts +516 -119
  35. package/dist/gateway-server.js +1201 -4035
  36. package/dist/gateway-server.js.map +1 -1
  37. package/dist/gateway.d.ts +69 -106
  38. package/dist/gateway.js +13 -6
  39. package/dist/index.d.ts +65 -58
  40. package/dist/index.js +18 -4
  41. package/dist/parquet.d.ts +1 -1
  42. package/dist/parquet.js +3 -3
  43. package/dist/proto.d.ts +1 -1
  44. package/dist/proto.js +3 -3
  45. package/dist/react.d.ts +47 -10
  46. package/dist/react.js +88 -40
  47. package/dist/react.js.map +1 -1
  48. package/dist/{registry-CPTgO9jv.d.ts → registry-Dd8JuW8T.d.ts} +19 -4
  49. package/dist/{gateway-Bpvatd9n.d.ts → request-handler-B1I5xDOx.d.ts} +193 -20
  50. package/dist/{resolver-CbuXm3nB.d.ts → resolver-CXxmC0jR.d.ts} +1 -1
  51. package/dist/{src-RHKJFQKR.js → src-WU7IBVC4.js} +19 -5
  52. package/dist/{types-CLlD4XOy.d.ts → types-BdGBv2ba.d.ts} +17 -2
  53. package/dist/{types-D-E0VrfS.d.ts → types-D2C9jTbL.d.ts} +39 -22
  54. package/package.json +1 -1
  55. package/dist/auth-CAVutXzx.d.ts +0 -30
  56. package/dist/chunk-7D4SUZUM.js +0 -38
  57. package/dist/chunk-GUJWMK5P.js.map +0 -1
  58. package/dist/chunk-IRJ4QRWV.js.map +0 -1
  59. package/dist/chunk-P3FT7QCW.js.map +0 -1
  60. package/dist/db-types-BlN-4KbQ.d.ts +0 -29
  61. package/dist/src-CLCALYDT.js +0 -25
  62. package/dist/src-FPJQYQNA.js +0 -27
  63. package/dist/src-FPJQYQNA.js.map +0 -1
  64. package/dist/src-RHKJFQKR.js.map +0 -1
  65. package/dist/types-DSC_EiwR.d.ts +0 -45
  66. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  67. /package/dist/{chunk-UAUQGP3B.js.map → chunk-U2NV4DUX.js.map} +0 -0
  68. /package/dist/{chunk-FHVTUKXL.js.map → chunk-YHYBLU6W.js.map} +0 -0
  69. /package/dist/{chunk-SF7Y6ZUA.js.map → chunk-ZU7RC7CT.js.map} +0 -0
  70. /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-FHVTUKXL.js";
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-P3FT7QCW.js";
26
- import "./chunk-7D4SUZUM.js";
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-queue.ts
372
- var MemoryQueue = class {
371
+ // ../client/src/queue/memory-outbox.ts
372
+ var MemoryOutbox = class {
373
373
  entries = /* @__PURE__ */ new Map();
374
374
  counter = 0;
375
- /** Add a delta to the queue */
376
- async push(delta) {
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: `mem-${++this.counter}`,
379
- delta,
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((e) => e.status !== "acked").length;
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
- actionQueue;
988
- maxActionRetries;
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.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;
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 arr = this.listeners[event];
1021
- const idx = arr.indexOf(listener);
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 10 seconds.
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
- const intervalMs = this.transport.supportsRealtime ? this.realtimeHeartbeatMs : this.autoSyncIntervalMs;
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
- }
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
- if (this.syncMode !== "pushOnly") {
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.actionQueue) {
1479
+ if (!this.actionProcessor) {
1207
1480
  this.emit("onError", new Error("No action queue configured"));
1208
1481
  return;
1209
1482
  }
1210
- const hlc = this.hlc.now();
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
- * Peeks at pending entries, sends them to the gateway via
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.actionQueue || !this.transport.executeAction) return;
1241
- const peekResult = await this.actionQueue.peek(100);
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
- if (this.syncIntervalId !== null) {
1312
- clearInterval(this.syncIntervalId);
1313
- this.syncIntervalId = null;
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
- if (this.visibilityHandler) {
1316
- if (typeof document !== "undefined") {
1317
- document.removeEventListener("visibilitychange", this.visibilityHandler);
1318
- }
1319
- this.visibilityHandler = null;
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
- const response = await this._fetch(url, {
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 ${this.token}`
1599
+ Authorization: `Bearer ${token}`
1350
1600
  },
1351
- body: JSON.stringify(msg, bigintReplacer)
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
- const response = await this._fetch(url, {
1643
+ let token = await this.resolveToken();
1644
+ let response = await this._fetch(url, {
1382
1645
  method: "GET",
1383
1646
  headers: {
1384
- Authorization: `Bearer ${this.token}`
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 ${this.token}`
1685
+ Authorization: `Bearer ${token}`
1412
1686
  },
1413
- body: JSON.stringify(msg, bigintReplacer)
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 ${this.token}`
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 ${this.token}`,
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
- entries = /* @__PURE__ */ new Map();
1732
- counter = 0;
2007
+ outbox = new MemoryOutbox("mem-action");
1733
2008
  /** Add an action to the queue. */
1734
2009
  async push(action) {
1735
- const entry = {
1736
- id: `mem-action-${++this.counter}`,
1737
- action,
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 now = Date.now();
1748
- 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);
1749
- return Ok(pending);
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
- for (const id of ids) {
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
- for (const id of ids) {
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
- for (const id of ids) {
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
- const count = [...this.entries.values()].filter(
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.entries.clear();
1791
- return Ok(void 0);
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 url = `${this.config.url}?token=${encodeURIComponent(this.config.token)}`;
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,