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
@@ -1,18 +1,15 @@
1
- import {
2
- isDatabaseAdapter,
3
- isMaterialisable
4
- } from "./chunk-GUJWMK5P.js";
5
1
  import {
6
2
  buildPartitionSpec,
7
3
  lakeSyncTableName,
8
4
  tableSchemaToIceberg
9
- } from "./chunk-UAUQGP3B.js";
5
+ } from "./chunk-U2NV4DUX.js";
10
6
  import {
11
7
  writeDeltasToParquet
12
- } from "./chunk-SF7Y6ZUA.js";
8
+ } from "./chunk-ZU7RC7CT.js";
13
9
  import {
14
10
  AdapterNotFoundError,
15
11
  BackpressureError,
12
+ COLUMN_TYPES,
16
13
  Err,
17
14
  FlushError,
18
15
  HLC,
@@ -21,6 +18,8 @@ import {
21
18
  bigintReplacer,
22
19
  bigintReviver,
23
20
  filterDeltas,
21
+ isDatabaseAdapter,
22
+ isMaterialisable,
24
23
  listConnectorDescriptors,
25
24
  resolveLWW,
26
25
  rowKey,
@@ -28,19 +27,84 @@ import {
28
27
  validateAction,
29
28
  validateConnectorConfig,
30
29
  validateSyncRules
31
- } from "./chunk-P3FT7QCW.js";
30
+ } from "./chunk-4SG66H5K.js";
31
+
32
+ // ../gateway/src/idempotency-cache.ts
33
+ var DEFAULT_MAX_CACHE_SIZE = 1e4;
34
+ var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
35
+ var MemoryIdempotencyCache = class {
36
+ entries = /* @__PURE__ */ new Map();
37
+ maxSize;
38
+ ttlMs;
39
+ constructor(config) {
40
+ this.maxSize = config?.maxSize ?? DEFAULT_MAX_CACHE_SIZE;
41
+ this.ttlMs = config?.ttlMs ?? DEFAULT_CACHE_TTL_MS;
42
+ }
43
+ /** {@inheritDoc IdempotencyCache.has} */
44
+ has(actionId) {
45
+ const entry = this.entries.get(actionId);
46
+ if (!entry) return false;
47
+ if (Date.now() - entry.cachedAt > this.ttlMs) {
48
+ this.entries.delete(actionId);
49
+ return false;
50
+ }
51
+ return true;
52
+ }
53
+ /** {@inheritDoc IdempotencyCache.get} */
54
+ get(key) {
55
+ const entry = this.entries.get(key);
56
+ if (!entry) return void 0;
57
+ if (Date.now() - entry.cachedAt > this.ttlMs) {
58
+ this.entries.delete(key);
59
+ return void 0;
60
+ }
61
+ return entry.value;
62
+ }
63
+ /** {@inheritDoc IdempotencyCache.set} */
64
+ set(actionId, result, idempotencyKey) {
65
+ this.evictStaleEntries();
66
+ const entry = { value: result, cachedAt: Date.now() };
67
+ this.entries.set(actionId, entry);
68
+ if (idempotencyKey) {
69
+ this.entries.set(`idem:${idempotencyKey}`, entry);
70
+ }
71
+ }
72
+ /** Evict expired entries and trim to max size (counting only non-idem entries). */
73
+ evictStaleEntries() {
74
+ const now = Date.now();
75
+ for (const [key, entry] of this.entries) {
76
+ if (now - entry.cachedAt > this.ttlMs) {
77
+ this.entries.delete(key);
78
+ }
79
+ }
80
+ const actionKeys = [...this.entries.keys()].filter((k) => !k.startsWith("idem:"));
81
+ if (actionKeys.length > this.maxSize) {
82
+ const excess = actionKeys.length - this.maxSize;
83
+ for (let i = 0; i < excess; i++) {
84
+ this.entries.delete(actionKeys[i]);
85
+ }
86
+ }
87
+ }
88
+ };
32
89
 
33
90
  // ../gateway/src/action-dispatcher.ts
34
91
  var ActionDispatcher = class {
35
92
  actionHandlers = /* @__PURE__ */ new Map();
36
- executedActions = /* @__PURE__ */ new Set();
37
- idempotencyMap = /* @__PURE__ */ new Map();
38
- constructor(handlers) {
93
+ cache;
94
+ /**
95
+ * Create an ActionDispatcher.
96
+ *
97
+ * @param handlers - Optional map of connector name to action handler.
98
+ * @param cacheConfig - Optional cache configuration (used when no `cache` is provided).
99
+ * @param cache - Optional pre-built idempotency cache; defaults to a {@link MemoryIdempotencyCache}.
100
+ */
101
+ constructor(handlers, cacheConfig, cache) {
39
102
  if (handlers) {
40
103
  for (const [name, handler] of Object.entries(handlers)) {
41
104
  this.actionHandlers.set(name, handler);
42
105
  }
43
106
  }
107
+ this.cache = cache ?? new MemoryIdempotencyCache(cacheConfig);
44
108
  }
45
109
  /**
46
110
  * Dispatch an action push to registered handlers.
@@ -61,8 +125,8 @@ var ActionDispatcher = class {
61
125
  if (!validation.ok) {
62
126
  return Err(validation.error);
63
127
  }
64
- if (this.executedActions.has(action.actionId)) {
65
- const cached = this.idempotencyMap.get(action.actionId);
128
+ if (this.cache.has(action.actionId)) {
129
+ const cached = this.cache.get(action.actionId);
66
130
  if (cached) {
67
131
  results.push(cached);
68
132
  continue;
@@ -70,7 +134,7 @@ var ActionDispatcher = class {
70
134
  continue;
71
135
  }
72
136
  if (action.idempotencyKey) {
73
- const cached = this.idempotencyMap.get(`idem:${action.idempotencyKey}`);
137
+ const cached = this.cache.get(`idem:${action.idempotencyKey}`);
74
138
  if (cached) {
75
139
  results.push(cached);
76
140
  continue;
@@ -85,7 +149,7 @@ var ActionDispatcher = class {
85
149
  retryable: false
86
150
  };
87
151
  results.push(errorResult);
88
- this.cacheActionResult(action, errorResult);
152
+ this.cache.set(action.actionId, errorResult, action.idempotencyKey);
89
153
  continue;
90
154
  }
91
155
  const supported = handler.supportedActions.some((d) => d.actionType === action.actionType);
@@ -97,13 +161,13 @@ var ActionDispatcher = class {
97
161
  retryable: false
98
162
  };
99
163
  results.push(errorResult);
100
- this.cacheActionResult(action, errorResult);
164
+ this.cache.set(action.actionId, errorResult, action.idempotencyKey);
101
165
  continue;
102
166
  }
103
167
  const execResult = await handler.executeAction(action, context);
104
168
  if (execResult.ok) {
105
169
  results.push(execResult.value);
106
- this.cacheActionResult(action, execResult.value);
170
+ this.cache.set(action.actionId, execResult.value, action.idempotencyKey);
107
171
  } else {
108
172
  const err = execResult.error;
109
173
  const errorResult = {
@@ -114,7 +178,7 @@ var ActionDispatcher = class {
114
178
  };
115
179
  results.push(errorResult);
116
180
  if (!errorResult.retryable) {
117
- this.cacheActionResult(action, errorResult);
181
+ this.cache.set(action.actionId, errorResult, action.idempotencyKey);
118
182
  }
119
183
  }
120
184
  }
@@ -161,14 +225,6 @@ var ActionDispatcher = class {
161
225
  }
162
226
  return { connectors };
163
227
  }
164
- /** Cache an action result for idempotency deduplication. */
165
- cacheActionResult(action, result) {
166
- this.executedActions.add(action.actionId);
167
- this.idempotencyMap.set(action.actionId, result);
168
- if (action.idempotencyKey) {
169
- this.idempotencyMap.set(`idem:${action.idempotencyKey}`, result);
170
- }
171
- }
172
228
  };
173
229
 
174
230
  // ../gateway/src/buffer.ts
@@ -284,9 +340,18 @@ var DeltaBuffer = class {
284
340
  this.tableLog.delete(table);
285
341
  return tableDeltas;
286
342
  }
287
- /** Drain the log for flush. Returns log entries and clears both structures. */
288
- drain() {
289
- const entries = [...this.log];
343
+ /**
344
+ * Snapshot the current buffer state without clearing it.
345
+ *
346
+ * Useful for inspecting the buffer contents without draining.
347
+ * Use {@link clear} separately after a successful flush for
348
+ * transactional semantics.
349
+ */
350
+ snapshot() {
351
+ return { entries: [...this.log], byteSize: this.estimatedBytes };
352
+ }
353
+ /** Clear all buffer state. */
354
+ clear() {
290
355
  this.log = [];
291
356
  this.index.clear();
292
357
  this.deltaIds.clear();
@@ -294,6 +359,11 @@ var DeltaBuffer = class {
294
359
  this.createdAt = Date.now();
295
360
  this.tableBytes.clear();
296
361
  this.tableLog.clear();
362
+ }
363
+ /** Drain the log for flush. Returns log entries and clears both structures. */
364
+ drain() {
365
+ const { entries } = this.snapshot();
366
+ this.clear();
297
367
  return entries;
298
368
  }
299
369
  /** Number of log entries */
@@ -344,7 +414,7 @@ var MAX_PUSH_PAYLOAD_BYTES = 1048576;
344
414
  var MAX_DELTAS_PER_PUSH = 1e4;
345
415
  var MAX_PULL_LIMIT = 1e4;
346
416
  var DEFAULT_PULL_LIMIT = 100;
347
- var VALID_COLUMN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "json", "null"]);
417
+ var VALID_COLUMN_TYPES = new Set(COLUMN_TYPES);
348
418
  var DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
349
419
  var DEFAULT_MAX_BUFFER_AGE_MS = 3e4;
350
420
 
@@ -359,26 +429,39 @@ function hlcRange(entries) {
359
429
  }
360
430
  return { min, max };
361
431
  }
362
- async function flushEntries(entries, byteSize, deps, keyPrefix) {
363
- if (isDatabaseAdapter(deps.adapter)) {
432
+ function notifyMaterialisationFailure(entries, error, config) {
433
+ if (!config.onMaterialisationFailure) return;
434
+ const tables = new Set(entries.map((e) => e.table));
435
+ for (const table of tables) {
436
+ const count = entries.filter((e) => e.table === table).length;
437
+ config.onMaterialisationFailure(table, count, error);
438
+ }
439
+ }
440
+ var DatabaseFlushStrategy = class {
441
+ async flush(entries, _byteSize, deps) {
442
+ const adapter = deps.adapter;
364
443
  try {
365
- const result = await deps.adapter.insertDeltas(entries);
444
+ const result = await adapter.insertDeltas(entries);
366
445
  if (!result.ok) {
367
446
  deps.restoreEntries(entries);
368
447
  return Err(new FlushError(`Database flush failed: ${result.error.message}`));
369
448
  }
370
- if (deps.schemas && deps.schemas.length > 0 && isMaterialisable(deps.adapter)) {
449
+ if (deps.schemas && deps.schemas.length > 0 && isMaterialisable(adapter)) {
371
450
  try {
372
- const matResult = await deps.adapter.materialise(entries, deps.schemas);
451
+ const matResult = await adapter.materialise(entries, deps.schemas);
373
452
  if (!matResult.ok) {
453
+ const error = new Error(matResult.error.message);
374
454
  console.warn(
375
455
  `[lakesync] Materialisation failed (${entries.length} deltas): ${matResult.error.message}`
376
456
  );
457
+ notifyMaterialisationFailure(entries, error, deps.config);
377
458
  }
378
459
  } catch (error) {
460
+ const err = error instanceof Error ? error : new Error(String(error));
379
461
  console.warn(
380
- `[lakesync] Materialisation error (${entries.length} deltas): ${error instanceof Error ? error.message : String(error)}`
462
+ `[lakesync] Materialisation error (${entries.length} deltas): ${err.message}`
381
463
  );
464
+ notifyMaterialisationFailure(entries, err, deps.config);
382
465
  }
383
466
  }
384
467
  return Ok(void 0);
@@ -387,14 +470,14 @@ async function flushEntries(entries, byteSize, deps, keyPrefix) {
387
470
  return Err(new FlushError(`Unexpected database flush failure: ${toError(error).message}`));
388
471
  }
389
472
  }
390
- try {
391
- const { min, max } = hlcRange(entries);
392
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
393
- const prefix = keyPrefix ? `${keyPrefix}-` : "";
394
- let objectKey;
395
- let data;
396
- let contentType;
397
- if (deps.config.flushFormat === "json") {
473
+ };
474
+ var LakeJsonFlushStrategy = class {
475
+ async flush(entries, byteSize, deps, keyPrefix) {
476
+ const adapter = deps.adapter;
477
+ try {
478
+ const { min, max } = hlcRange(entries);
479
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
480
+ const prefix = keyPrefix ? `${keyPrefix}-` : "";
398
481
  const envelope = {
399
482
  version: 1,
400
483
  gatewayId: deps.config.gatewayId,
@@ -404,10 +487,33 @@ async function flushEntries(entries, byteSize, deps, keyPrefix) {
404
487
  byteSize,
405
488
  deltas: entries
406
489
  };
407
- objectKey = `deltas/${date}/${deps.config.gatewayId}/${prefix}${min.toString()}-${max.toString()}.json`;
408
- data = new TextEncoder().encode(JSON.stringify(envelope, bigintReplacer));
409
- contentType = "application/json";
410
- } else {
490
+ const objectKey = `deltas/${date}/${deps.config.gatewayId}/${prefix}${min.toString()}-${max.toString()}.json`;
491
+ const data = new TextEncoder().encode(JSON.stringify(envelope, bigintReplacer));
492
+ const result = await adapter.putObject(objectKey, data, "application/json");
493
+ if (!result.ok) {
494
+ deps.restoreEntries(entries);
495
+ return Err(new FlushError(`Failed to write flush envelope: ${result.error.message}`));
496
+ }
497
+ if (deps.config.catalogue && deps.config.tableSchema) {
498
+ await commitToCatalogue(
499
+ objectKey,
500
+ data.byteLength,
501
+ entries.length,
502
+ deps.config.catalogue,
503
+ deps.config.tableSchema
504
+ );
505
+ }
506
+ return Ok(void 0);
507
+ } catch (error) {
508
+ deps.restoreEntries(entries);
509
+ return Err(new FlushError(`Unexpected flush failure: ${toError(error).message}`));
510
+ }
511
+ }
512
+ };
513
+ var LakeParquetFlushStrategy = class {
514
+ async flush(entries, _byteSize, deps, keyPrefix) {
515
+ const adapter = deps.adapter;
516
+ try {
411
517
  if (!deps.config.tableSchema) {
412
518
  deps.restoreEntries(entries);
413
519
  return Err(new FlushError("tableSchema required for Parquet flush"));
@@ -417,29 +523,43 @@ async function flushEntries(entries, byteSize, deps, keyPrefix) {
417
523
  deps.restoreEntries(entries);
418
524
  return Err(parquetResult.error);
419
525
  }
420
- objectKey = `deltas/${date}/${deps.config.gatewayId}/${prefix}${min.toString()}-${max.toString()}.parquet`;
421
- data = parquetResult.value;
422
- contentType = "application/vnd.apache.parquet";
423
- }
424
- const result = await deps.adapter.putObject(objectKey, data, contentType);
425
- if (!result.ok) {
526
+ const { min, max } = hlcRange(entries);
527
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
528
+ const prefix = keyPrefix ? `${keyPrefix}-` : "";
529
+ const objectKey = `deltas/${date}/${deps.config.gatewayId}/${prefix}${min.toString()}-${max.toString()}.parquet`;
530
+ const data = parquetResult.value;
531
+ const result = await adapter.putObject(objectKey, data, "application/vnd.apache.parquet");
532
+ if (!result.ok) {
533
+ deps.restoreEntries(entries);
534
+ return Err(new FlushError(`Failed to write flush envelope: ${result.error.message}`));
535
+ }
536
+ if (deps.config.catalogue && deps.config.tableSchema) {
537
+ await commitToCatalogue(
538
+ objectKey,
539
+ data.byteLength,
540
+ entries.length,
541
+ deps.config.catalogue,
542
+ deps.config.tableSchema
543
+ );
544
+ }
545
+ return Ok(void 0);
546
+ } catch (error) {
426
547
  deps.restoreEntries(entries);
427
- return Err(new FlushError(`Failed to write flush envelope: ${result.error.message}`));
548
+ return Err(new FlushError(`Unexpected flush failure: ${toError(error).message}`));
428
549
  }
429
- if (deps.config.catalogue && deps.config.tableSchema) {
430
- await commitToCatalogue(
431
- objectKey,
432
- data.byteLength,
433
- entries.length,
434
- deps.config.catalogue,
435
- deps.config.tableSchema
436
- );
437
- }
438
- return Ok(void 0);
439
- } catch (error) {
440
- deps.restoreEntries(entries);
441
- return Err(new FlushError(`Unexpected flush failure: ${toError(error).message}`));
442
550
  }
551
+ };
552
+ var databaseStrategy = new DatabaseFlushStrategy();
553
+ var lakeJsonStrategy = new LakeJsonFlushStrategy();
554
+ var lakeParquetStrategy = new LakeParquetFlushStrategy();
555
+ function selectFlushStrategy(adapter, format) {
556
+ if (isDatabaseAdapter(adapter)) return databaseStrategy;
557
+ if (format === "json") return lakeJsonStrategy;
558
+ return lakeParquetStrategy;
559
+ }
560
+ async function flushEntries(entries, byteSize, deps, keyPrefix) {
561
+ const strategy = selectFlushStrategy(deps.adapter, deps.config.flushFormat);
562
+ return strategy.flush(entries, byteSize, deps, keyPrefix);
443
563
  }
444
564
  async function commitToCatalogue(objectKey, fileSizeInBytes, recordCount, catalogue, schema) {
445
565
  const { namespace, name } = lakeSyncTableName(schema.table);
@@ -463,6 +583,117 @@ async function commitToCatalogue(objectKey, fileSizeInBytes, recordCount, catalo
463
583
  }
464
584
  }
465
585
 
586
+ // ../gateway/src/flush-coordinator.ts
587
+ var FlushCoordinator = class {
588
+ flushing = false;
589
+ /** Whether a flush is currently in progress. */
590
+ get isFlushing() {
591
+ return this.flushing;
592
+ }
593
+ /**
594
+ * Flush all entries from the buffer to the adapter.
595
+ *
596
+ * Drains the buffer first, then writes to the adapter. On failure,
597
+ * entries are restored to the buffer.
598
+ */
599
+ async flush(buffer, adapter, deps) {
600
+ if (this.flushing) {
601
+ return Err(new FlushError("Flush already in progress"));
602
+ }
603
+ if (buffer.logSize === 0) {
604
+ return Ok(void 0);
605
+ }
606
+ if (!adapter) {
607
+ return Err(new FlushError("No adapter configured"));
608
+ }
609
+ this.flushing = true;
610
+ const byteSize = isDatabaseAdapter(adapter) ? 0 : buffer.byteSize;
611
+ const entries = buffer.drain();
612
+ if (entries.length === 0) {
613
+ this.flushing = false;
614
+ return Ok(void 0);
615
+ }
616
+ try {
617
+ return await flushEntries(entries, byteSize, {
618
+ adapter,
619
+ config: deps.config,
620
+ restoreEntries: (e) => this.restoreEntries(buffer, e),
621
+ schemas: deps.schemas
622
+ });
623
+ } finally {
624
+ this.flushing = false;
625
+ }
626
+ }
627
+ /**
628
+ * Flush a single table's deltas from the buffer.
629
+ *
630
+ * Drains only the specified table's deltas and flushes them,
631
+ * leaving other tables in the buffer.
632
+ */
633
+ async flushTable(table, buffer, adapter, deps) {
634
+ if (this.flushing) {
635
+ return Err(new FlushError("Flush already in progress"));
636
+ }
637
+ if (!adapter) {
638
+ return Err(new FlushError("No adapter configured"));
639
+ }
640
+ const entries = buffer.drainTable(table);
641
+ if (entries.length === 0) {
642
+ return Ok(void 0);
643
+ }
644
+ this.flushing = true;
645
+ try {
646
+ return await flushEntries(
647
+ entries,
648
+ 0,
649
+ {
650
+ adapter,
651
+ config: deps.config,
652
+ restoreEntries: (e) => this.restoreEntries(buffer, e),
653
+ schemas: deps.schemas
654
+ },
655
+ table
656
+ );
657
+ } finally {
658
+ this.flushing = false;
659
+ }
660
+ }
661
+ /** Restore drained entries back to the buffer for retry. */
662
+ restoreEntries(buffer, entries) {
663
+ for (const entry of entries) {
664
+ buffer.append(entry);
665
+ }
666
+ }
667
+ };
668
+
669
+ // ../gateway/src/source-registry.ts
670
+ var SourceRegistry = class {
671
+ sources = /* @__PURE__ */ new Map();
672
+ constructor(initial) {
673
+ if (initial) {
674
+ for (const [name, adapter] of Object.entries(initial)) {
675
+ this.sources.set(name, adapter);
676
+ }
677
+ }
678
+ }
679
+ /** Register a named source adapter. */
680
+ register(name, adapter) {
681
+ this.sources.set(name, adapter);
682
+ }
683
+ /** Unregister a named source adapter. */
684
+ unregister(name) {
685
+ this.sources.delete(name);
686
+ }
687
+ /** Get a source adapter by name, or undefined if not registered. */
688
+ get(name) {
689
+ return this.sources.get(name);
690
+ }
691
+ /** List all registered source adapter names. */
692
+ list() {
693
+ return [...this.sources.keys()];
694
+ }
695
+ };
696
+
466
697
  // ../gateway/src/gateway.ts
467
698
  var SyncGateway = class {
468
699
  hlc;
@@ -470,20 +701,20 @@ var SyncGateway = class {
470
701
  actions;
471
702
  config;
472
703
  adapter;
473
- flushing = false;
704
+ sources;
705
+ flushCoordinator;
474
706
  constructor(config, adapter) {
475
707
  this.config = { sourceAdapters: {}, ...config };
476
708
  this.hlc = new HLC();
477
709
  this.buffer = new DeltaBuffer();
478
710
  this.adapter = this.config.adapter ?? adapter ?? null;
479
711
  this.actions = new ActionDispatcher(config.actionHandlers);
712
+ this.sources = new SourceRegistry(this.config.sourceAdapters);
713
+ this.flushCoordinator = new FlushCoordinator();
480
714
  }
481
- /** Restore drained entries back to the buffer for retry. */
482
- restoreEntries(entries) {
483
- for (const entry of entries) {
484
- this.buffer.append(entry);
485
- }
486
- }
715
+ // -----------------------------------------------------------------------
716
+ // Push — pipeline of validation steps then buffer append
717
+ // -----------------------------------------------------------------------
487
718
  /**
488
719
  * Handle an incoming push from a client.
489
720
  *
@@ -494,14 +725,8 @@ var SyncGateway = class {
494
725
  * or a `ClockDriftError` if the client clock is too far ahead.
495
726
  */
496
727
  handlePush(msg) {
497
- const backpressureLimit = this.config.maxBackpressureBytes ?? this.config.maxBufferBytes * 2;
498
- if (this.buffer.byteSize >= backpressureLimit) {
499
- return Err(
500
- new BackpressureError(
501
- `Buffer backpressure exceeded (${this.buffer.byteSize} >= ${backpressureLimit} bytes)`
502
- )
503
- );
504
- }
728
+ const bpResult = this.checkBackpressure();
729
+ if (!bpResult.ok) return bpResult;
505
730
  let accepted = 0;
506
731
  const ingested = [];
507
732
  for (const delta of msg.deltas) {
@@ -536,6 +761,18 @@ var SyncGateway = class {
536
761
  const serverHlc = this.hlc.now();
537
762
  return Ok({ serverHlc, accepted, deltas: ingested });
538
763
  }
764
+ /** Check buffer backpressure. */
765
+ checkBackpressure() {
766
+ const backpressureLimit = this.config.maxBackpressureBytes ?? this.config.maxBufferBytes * 2;
767
+ if (this.buffer.byteSize >= backpressureLimit) {
768
+ return Err(
769
+ new BackpressureError(
770
+ `Buffer backpressure exceeded (${this.buffer.byteSize} >= ${backpressureLimit} bytes)`
771
+ )
772
+ );
773
+ }
774
+ return Ok(void 0);
775
+ }
539
776
  handlePull(msg, context) {
540
777
  if (msg.source) {
541
778
  return this.handleAdapterPull(msg, context);
@@ -580,7 +817,7 @@ var SyncGateway = class {
580
817
  }
581
818
  /** Pull from a named source adapter. */
582
819
  async handleAdapterPull(msg, context) {
583
- const adapter = this.config.sourceAdapters?.[msg.source];
820
+ const adapter = this.sources.get(msg.source);
584
821
  if (!adapter) {
585
822
  return Err(new AdapterNotFoundError(`Source adapter "${msg.source}" not found`));
586
823
  }
@@ -598,7 +835,7 @@ var SyncGateway = class {
598
835
  return Ok({ deltas: sliced, serverHlc, hasMore });
599
836
  }
600
837
  // -----------------------------------------------------------------------
601
- // Flush — delegates to flush module
838
+ // Flush — delegates to FlushCoordinator
602
839
  // -----------------------------------------------------------------------
603
840
  /**
604
841
  * Flush the buffer to the configured adapter.
@@ -611,55 +848,15 @@ var SyncGateway = class {
611
848
  * @returns A `Result` indicating success or a `FlushError`.
612
849
  */
613
850
  async flush() {
614
- if (this.flushing) {
615
- return Err(new FlushError("Flush already in progress"));
616
- }
617
- if (this.buffer.logSize === 0) {
618
- return Ok(void 0);
619
- }
620
- if (!this.adapter) {
621
- return Err(new FlushError("No adapter configured"));
622
- }
623
- this.flushing = true;
624
- if (isDatabaseAdapter(this.adapter)) {
625
- const entries2 = this.buffer.drain();
626
- if (entries2.length === 0) {
627
- this.flushing = false;
628
- return Ok(void 0);
629
- }
630
- try {
631
- return await flushEntries(entries2, 0, {
632
- adapter: this.adapter,
633
- config: {
634
- gatewayId: this.config.gatewayId,
635
- flushFormat: this.config.flushFormat,
636
- tableSchema: this.config.tableSchema,
637
- catalogue: this.config.catalogue
638
- },
639
- restoreEntries: (e) => this.restoreEntries(e),
640
- schemas: this.config.schemas
641
- });
642
- } finally {
643
- this.flushing = false;
644
- }
645
- }
646
- const byteSize = this.buffer.byteSize;
647
- const entries = this.buffer.drain();
648
- try {
649
- return await flushEntries(entries, byteSize, {
650
- adapter: this.adapter,
651
- config: {
652
- gatewayId: this.config.gatewayId,
653
- flushFormat: this.config.flushFormat,
654
- tableSchema: this.config.tableSchema,
655
- catalogue: this.config.catalogue
656
- },
657
- restoreEntries: (e) => this.restoreEntries(e),
658
- schemas: this.config.schemas
659
- });
660
- } finally {
661
- this.flushing = false;
662
- }
851
+ return this.flushCoordinator.flush(this.buffer, this.adapter, {
852
+ config: {
853
+ gatewayId: this.config.gatewayId,
854
+ flushFormat: this.config.flushFormat,
855
+ tableSchema: this.config.tableSchema,
856
+ catalogue: this.config.catalogue
857
+ },
858
+ schemas: this.config.schemas
859
+ });
663
860
  }
664
861
  /**
665
862
  * Flush a single table's deltas from the buffer.
@@ -668,37 +865,15 @@ var SyncGateway = class {
668
865
  * leaving other tables in the buffer.
669
866
  */
670
867
  async flushTable(table) {
671
- if (this.flushing) {
672
- return Err(new FlushError("Flush already in progress"));
673
- }
674
- if (!this.adapter) {
675
- return Err(new FlushError("No adapter configured"));
676
- }
677
- const entries = this.buffer.drainTable(table);
678
- if (entries.length === 0) {
679
- return Ok(void 0);
680
- }
681
- this.flushing = true;
682
- try {
683
- return await flushEntries(
684
- entries,
685
- 0,
686
- {
687
- adapter: this.adapter,
688
- config: {
689
- gatewayId: this.config.gatewayId,
690
- flushFormat: this.config.flushFormat,
691
- tableSchema: this.config.tableSchema,
692
- catalogue: this.config.catalogue
693
- },
694
- restoreEntries: (e) => this.restoreEntries(e),
695
- schemas: this.config.schemas
696
- },
697
- table
698
- );
699
- } finally {
700
- this.flushing = false;
701
- }
868
+ return this.flushCoordinator.flushTable(table, this.buffer, this.adapter, {
869
+ config: {
870
+ gatewayId: this.config.gatewayId,
871
+ flushFormat: this.config.flushFormat,
872
+ tableSchema: this.config.tableSchema,
873
+ catalogue: this.config.catalogue
874
+ },
875
+ schemas: this.config.schemas
876
+ });
702
877
  }
703
878
  // -----------------------------------------------------------------------
704
879
  // Actions — delegates to ActionDispatcher
@@ -724,7 +899,7 @@ var SyncGateway = class {
724
899
  return this.actions.describe();
725
900
  }
726
901
  // -----------------------------------------------------------------------
727
- // Source adapters
902
+ // Source adapters — delegates to SourceRegistry
728
903
  // -----------------------------------------------------------------------
729
904
  /**
730
905
  * Register a named source adapter for adapter-sourced pulls.
@@ -733,7 +908,7 @@ var SyncGateway = class {
733
908
  * @param adapter - The database adapter to register.
734
909
  */
735
910
  registerSource(name, adapter) {
736
- this.config.sourceAdapters[name] = adapter;
911
+ this.sources.register(name, adapter);
737
912
  }
738
913
  /**
739
914
  * Unregister a named source adapter.
@@ -741,7 +916,7 @@ var SyncGateway = class {
741
916
  * @param name - The source name to remove.
742
917
  */
743
918
  unregisterSource(name) {
744
- delete this.config.sourceAdapters[name];
919
+ this.sources.unregister(name);
745
920
  }
746
921
  /**
747
922
  * List all registered source adapter names.
@@ -749,7 +924,16 @@ var SyncGateway = class {
749
924
  * @returns Array of registered source adapter names.
750
925
  */
751
926
  listSources() {
752
- return Object.keys(this.config.sourceAdapters);
927
+ return this.sources.list();
928
+ }
929
+ // -----------------------------------------------------------------------
930
+ // Rehydration — restore persisted deltas without push validation
931
+ // -----------------------------------------------------------------------
932
+ /** Rehydrate the buffer with persisted deltas (bypasses push validation). */
933
+ rehydrate(deltas) {
934
+ for (const delta of deltas) {
935
+ this.buffer.append(delta);
936
+ }
753
937
  }
754
938
  // -----------------------------------------------------------------------
755
939
  // Buffer queries
@@ -789,13 +973,17 @@ var SyncGateway = class {
789
973
  };
790
974
 
791
975
  // ../gateway/src/validation.ts
792
- function validatePushBody(raw, headerClientId) {
793
- let body;
976
+ function parseJson(raw, reviver) {
794
977
  try {
795
- body = JSON.parse(raw, bigintReviver);
978
+ return Ok(JSON.parse(raw, reviver));
796
979
  } catch {
797
980
  return Err({ status: 400, message: "Invalid JSON body" });
798
981
  }
982
+ }
983
+ function validatePushBody(raw, headerClientId) {
984
+ const parsed = parseJson(raw, bigintReviver);
985
+ if (!parsed.ok) return parsed;
986
+ const body = parsed.value;
799
987
  if (!body.clientId || !Array.isArray(body.deltas)) {
800
988
  return Err({ status: 400, message: "Missing required fields: clientId, deltas" });
801
989
  }
@@ -840,12 +1028,9 @@ function parsePullParams(params) {
840
1028
  return Ok(msg);
841
1029
  }
842
1030
  function validateActionBody(raw, headerClientId) {
843
- let body;
844
- try {
845
- body = JSON.parse(raw, bigintReviver);
846
- } catch {
847
- return Err({ status: 400, message: "Invalid JSON body" });
848
- }
1031
+ const parsed = parseJson(raw, bigintReviver);
1032
+ if (!parsed.ok) return parsed;
1033
+ const body = parsed.value;
849
1034
  if (!body.clientId || !Array.isArray(body.actions)) {
850
1035
  return Err({ status: 400, message: "Missing required fields: clientId, actions" });
851
1036
  }
@@ -858,12 +1043,9 @@ function validateActionBody(raw, headerClientId) {
858
1043
  return Ok(body);
859
1044
  }
860
1045
  function validateSchemaBody(raw) {
861
- let schema;
862
- try {
863
- schema = JSON.parse(raw);
864
- } catch {
865
- return Err({ status: 400, message: "Invalid JSON body" });
866
- }
1046
+ const parsed = parseJson(raw);
1047
+ if (!parsed.ok) return parsed;
1048
+ const schema = parsed.value;
867
1049
  if (!schema.table || !Array.isArray(schema.columns)) {
868
1050
  return Err({ status: 400, message: "Missing required fields: table, columns" });
869
1051
  }
@@ -1000,12 +1182,11 @@ async function handleSaveSchema(raw, store, gatewayId) {
1000
1182
  return { status: 200, body: { saved: true } };
1001
1183
  }
1002
1184
  async function handleSaveSyncRules(raw, store, gatewayId) {
1003
- let config;
1004
- try {
1005
- config = JSON.parse(raw);
1006
- } catch {
1007
- return { status: 400, body: { error: "Invalid JSON body" } };
1185
+ const parsed = parseJson(raw);
1186
+ if (!parsed.ok) {
1187
+ return { status: parsed.error.status, body: { error: parsed.error.message } };
1008
1188
  }
1189
+ const config = parsed.value;
1009
1190
  const validation = validateSyncRules(config);
1010
1191
  if (!validation.ok) {
1011
1192
  return { status: 400, body: { error: validation.error.message } };
@@ -1014,12 +1195,11 @@ async function handleSaveSyncRules(raw, store, gatewayId) {
1014
1195
  return { status: 200, body: { saved: true } };
1015
1196
  }
1016
1197
  async function handleRegisterConnector(raw, store) {
1017
- let body;
1018
- try {
1019
- body = JSON.parse(raw);
1020
- } catch {
1021
- return { status: 400, body: { error: "Invalid JSON body" } };
1198
+ const parsed = parseJson(raw);
1199
+ if (!parsed.ok) {
1200
+ return { status: parsed.error.status, body: { error: parsed.error.message } };
1022
1201
  }
1202
+ const body = parsed.value;
1023
1203
  const validation = validateConnectorConfig(body);
1024
1204
  if (!validation.ok) {
1025
1205
  return { status: 400, body: { error: validation.error.message } };
@@ -1061,17 +1241,17 @@ function handleMetrics(gateway, extra) {
1061
1241
 
1062
1242
  // ../gateway/src/schema-manager.ts
1063
1243
  var SchemaManager = class {
1064
- currentSchema;
1065
- version;
1066
- allowedColumns;
1244
+ state;
1067
1245
  constructor(schema, version) {
1068
- this.currentSchema = schema;
1069
- this.version = version ?? 1;
1070
- this.allowedColumns = new Set(schema.columns.map((c) => c.name));
1246
+ this.state = {
1247
+ schema,
1248
+ version: version ?? 1,
1249
+ allowedColumns: new Set(schema.columns.map((c) => c.name))
1250
+ };
1071
1251
  }
1072
1252
  /** Get the current schema and version. */
1073
1253
  getSchema() {
1074
- return { schema: this.currentSchema, version: this.version };
1254
+ return { schema: this.state.schema, version: this.state.version };
1075
1255
  }
1076
1256
  /**
1077
1257
  * Validate that a delta's columns are compatible with the current schema.
@@ -1084,10 +1264,10 @@ var SchemaManager = class {
1084
1264
  return Ok(void 0);
1085
1265
  }
1086
1266
  for (const col of delta.columns) {
1087
- if (!this.allowedColumns.has(col.column)) {
1267
+ if (!this.state.allowedColumns.has(col.column)) {
1088
1268
  return Err(
1089
1269
  new SchemaError(
1090
- `Unknown column "${col.column}" in delta for table "${delta.table}". Schema version ${this.version} does not include this column.`
1270
+ `Unknown column "${col.column}" in delta for table "${delta.table}". Schema version ${this.state.version} does not include this column.`
1091
1271
  )
1092
1272
  );
1093
1273
  }
@@ -1101,10 +1281,10 @@ var SchemaManager = class {
1101
1281
  * returns a SchemaError.
1102
1282
  */
1103
1283
  evolveSchema(newSchema) {
1104
- if (newSchema.table !== this.currentSchema.table) {
1284
+ if (newSchema.table !== this.state.schema.table) {
1105
1285
  return Err(new SchemaError("Cannot evolve schema: table name mismatch"));
1106
1286
  }
1107
- const oldColumnMap = new Map(this.currentSchema.columns.map((c) => [c.name, c.type]));
1287
+ const oldColumnMap = new Map(this.state.schema.columns.map((c) => [c.name, c.type]));
1108
1288
  const newColumnMap = new Map(newSchema.columns.map((c) => [c.name, c.type]));
1109
1289
  for (const [name] of oldColumnMap) {
1110
1290
  if (!newColumnMap.has(name)) {
@@ -1125,14 +1305,18 @@ var SchemaManager = class {
1125
1305
  );
1126
1306
  }
1127
1307
  }
1128
- this.currentSchema = newSchema;
1129
- this.version++;
1130
- this.allowedColumns = new Set(newSchema.columns.map((c) => c.name));
1131
- return Ok({ version: this.version });
1308
+ const newVersion = this.state.version + 1;
1309
+ this.state = {
1310
+ schema: newSchema,
1311
+ version: newVersion,
1312
+ allowedColumns: new Set(newSchema.columns.map((c) => c.name))
1313
+ };
1314
+ return Ok({ version: newVersion });
1132
1315
  }
1133
1316
  };
1134
1317
 
1135
1318
  export {
1319
+ MemoryIdempotencyCache,
1136
1320
  ActionDispatcher,
1137
1321
  DeltaBuffer,
1138
1322
  MemoryConfigStore,
@@ -1146,7 +1330,10 @@ export {
1146
1330
  hlcRange,
1147
1331
  flushEntries,
1148
1332
  commitToCatalogue,
1333
+ FlushCoordinator,
1334
+ SourceRegistry,
1149
1335
  SyncGateway,
1336
+ parseJson,
1150
1337
  validatePushBody,
1151
1338
  parsePullParams,
1152
1339
  validateActionBody,
@@ -1166,4 +1353,4 @@ export {
1166
1353
  handleMetrics,
1167
1354
  SchemaManager
1168
1355
  };
1169
- //# sourceMappingURL=chunk-IRJ4QRWV.js.map
1356
+ //# sourceMappingURL=chunk-FIIHPQMQ.js.map