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.
Files changed (65) hide show
  1. package/dist/adapter.d.ts +199 -19
  2. package/dist/adapter.js +19 -3
  3. package/dist/analyst.js +2 -2
  4. package/dist/{base-poller-CBvhdvcj.d.ts → base-poller-Bj9kX9dv.d.ts} +76 -19
  5. package/dist/catalogue.d.ts +1 -1
  6. package/dist/catalogue.js +3 -3
  7. package/dist/chunk-DGUM43GV.js +11 -0
  8. package/dist/{chunk-PWGQ3PXE.js → chunk-JI4C4R5H.js} +280 -140
  9. package/dist/chunk-JI4C4R5H.js.map +1 -0
  10. package/dist/{chunk-L4ZL5JA7.js → chunk-KVSWLIJR.js} +2 -2
  11. package/dist/{chunk-7UBS6MFH.js → chunk-LDFFCG2K.js} +377 -247
  12. package/dist/chunk-LDFFCG2K.js.map +1 -0
  13. package/dist/{chunk-Z7FGLEQU.js → chunk-LPWXOYNS.js} +376 -287
  14. package/dist/chunk-LPWXOYNS.js.map +1 -0
  15. package/dist/{chunk-SZSGSTVZ.js → chunk-PYRS74YP.js} +15 -4
  16. package/dist/{chunk-SZSGSTVZ.js.map → chunk-PYRS74YP.js.map} +1 -1
  17. package/dist/{chunk-TVLTXHW6.js → chunk-QNITY4F6.js} +30 -7
  18. package/dist/{chunk-TVLTXHW6.js.map → chunk-QNITY4F6.js.map} +1 -1
  19. package/dist/{chunk-46CKACNC.js → chunk-SSICS5KI.js} +2 -2
  20. package/dist/{chunk-B3QEUG6E.js → chunk-TMLG32QV.js} +2 -2
  21. package/dist/client.d.ts +164 -13
  22. package/dist/client.js +310 -163
  23. package/dist/client.js.map +1 -1
  24. package/dist/compactor.d.ts +1 -1
  25. package/dist/compactor.js +4 -4
  26. package/dist/connector-jira.d.ts +2 -2
  27. package/dist/connector-jira.js +3 -3
  28. package/dist/connector-salesforce.d.ts +2 -2
  29. package/dist/connector-salesforce.js +3 -3
  30. package/dist/{coordinator-DN8D8C7W.d.ts → coordinator-NXy6tA0h.d.ts} +23 -16
  31. package/dist/{db-types-B6_JKQWK.d.ts → db-types-CfLMUBfW.d.ts} +1 -1
  32. package/dist/gateway-server.d.ts +158 -64
  33. package/dist/gateway-server.js +482 -4003
  34. package/dist/gateway-server.js.map +1 -1
  35. package/dist/gateway.d.ts +61 -104
  36. package/dist/gateway.js +12 -6
  37. package/dist/index.d.ts +45 -10
  38. package/dist/index.js +14 -2
  39. package/dist/parquet.d.ts +1 -1
  40. package/dist/parquet.js +3 -3
  41. package/dist/proto.d.ts +1 -1
  42. package/dist/proto.js +3 -3
  43. package/dist/react.d.ts +47 -10
  44. package/dist/react.js +88 -40
  45. package/dist/react.js.map +1 -1
  46. package/dist/{registry-BN_9spxE.d.ts → registry-BcspAtZI.d.ts} +19 -4
  47. package/dist/{gateway-CvO7Xy3T.d.ts → request-handler-pUvL7ozF.d.ts} +139 -10
  48. package/dist/{resolver-BZURzdlL.d.ts → resolver-CXxmC0jR.d.ts} +1 -1
  49. package/dist/{src-RR7I76OL.js → src-B6NLV3FP.js} +4 -4
  50. package/dist/{src-SLVE5567.js → src-ROW4XLO7.js} +15 -3
  51. package/dist/{src-V2CTPR7V.js → src-ZRHKG42A.js} +4 -4
  52. package/dist/{types-GGBfZBKQ.d.ts → types-BdGBv2ba.d.ts} +23 -2
  53. package/dist/{types-D-E0VrfS.d.ts → types-BrcD1oJg.d.ts} +26 -19
  54. package/package.json +1 -1
  55. package/dist/chunk-7D4SUZUM.js +0 -38
  56. package/dist/chunk-7UBS6MFH.js.map +0 -1
  57. package/dist/chunk-PWGQ3PXE.js.map +0 -1
  58. package/dist/chunk-Z7FGLEQU.js.map +0 -1
  59. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  60. /package/dist/{chunk-L4ZL5JA7.js.map → chunk-KVSWLIJR.js.map} +0 -0
  61. /package/dist/{chunk-46CKACNC.js.map → chunk-SSICS5KI.js.map} +0 -0
  62. /package/dist/{chunk-B3QEUG6E.js.map → chunk-TMLG32QV.js.map} +0 -0
  63. /package/dist/{src-RR7I76OL.js.map → src-B6NLV3FP.js.map} +0 -0
  64. /package/dist/{src-SLVE5567.js.map → src-ROW4XLO7.js.map} +0 -0
  65. /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-L4ZL5JA7.js";
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-7UBS6MFH.js";
26
- import "./chunk-7D4SUZUM.js";
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-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-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
- actionQueue;
988
- maxActionRetries;
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 10 seconds.
1376
+ * Synchronises (push + pull) on tab focus and every N seconds.
1134
1377
  */
1135
1378
  startAutoSync() {
1136
1379
  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
- }
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.actionQueue) {
1435
+ if (!this.actionProcessor) {
1207
1436
  this.emit("onError", new Error("No action queue configured"));
1208
1437
  return;
1209
1438
  }
1210
- const hlc = this.hlc.now();
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
- * 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.
1444
+ * Delegates to the ActionProcessor if one is configured.
1238
1445
  */
1239
1446
  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
- }
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
- if (this.syncIntervalId !== null) {
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
- entries = /* @__PURE__ */ new Map();
1732
- counter = 0;
1893
+ outbox = new MemoryOutbox("mem-action");
1733
1894
  /** Add an action to the queue. */
1734
1895
  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);
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 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);
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
- 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);
1908
+ return this.outbox.markSending(ids);
1760
1909
  }
1761
1910
  /** Acknowledge successful delivery (removes entries). */
1762
1911
  async ack(ids) {
1763
- for (const id of ids) {
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
- 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);
1916
+ return this.outbox.nack(ids);
1780
1917
  }
1781
1918
  /** Get the number of pending + sending entries. */
1782
1919
  async depth() {
1783
- const count = [...this.entries.values()].filter(
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.entries.clear();
1791
- return Ok(void 0);
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,