lakesync 0.1.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 (69) hide show
  1. package/README.md +74 -0
  2. package/dist/adapter.d.ts +369 -0
  3. package/dist/adapter.js +39 -0
  4. package/dist/adapter.js.map +1 -0
  5. package/dist/analyst.d.ts +268 -0
  6. package/dist/analyst.js +495 -0
  7. package/dist/analyst.js.map +1 -0
  8. package/dist/auth-CAVutXzx.d.ts +30 -0
  9. package/dist/base-poller-Qo_SmCZs.d.ts +82 -0
  10. package/dist/catalogue.d.ts +65 -0
  11. package/dist/catalogue.js +17 -0
  12. package/dist/catalogue.js.map +1 -0
  13. package/dist/chunk-4ARO6KTJ.js +257 -0
  14. package/dist/chunk-4ARO6KTJ.js.map +1 -0
  15. package/dist/chunk-5YOFCJQ7.js +1115 -0
  16. package/dist/chunk-5YOFCJQ7.js.map +1 -0
  17. package/dist/chunk-7D4SUZUM.js +38 -0
  18. package/dist/chunk-7D4SUZUM.js.map +1 -0
  19. package/dist/chunk-BNJOGBYK.js +335 -0
  20. package/dist/chunk-BNJOGBYK.js.map +1 -0
  21. package/dist/chunk-ICNT7I3K.js +1180 -0
  22. package/dist/chunk-ICNT7I3K.js.map +1 -0
  23. package/dist/chunk-P5DRFKIT.js +413 -0
  24. package/dist/chunk-P5DRFKIT.js.map +1 -0
  25. package/dist/chunk-X3RO5SYJ.js +880 -0
  26. package/dist/chunk-X3RO5SYJ.js.map +1 -0
  27. package/dist/client.d.ts +428 -0
  28. package/dist/client.js +2048 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/compactor.d.ts +342 -0
  31. package/dist/compactor.js +793 -0
  32. package/dist/compactor.js.map +1 -0
  33. package/dist/coordinator-CxckTzYW.d.ts +396 -0
  34. package/dist/db-types-BR6Kt4uf.d.ts +29 -0
  35. package/dist/gateway-D5SaaMvT.d.ts +337 -0
  36. package/dist/gateway-server.d.ts +306 -0
  37. package/dist/gateway-server.js +4663 -0
  38. package/dist/gateway-server.js.map +1 -0
  39. package/dist/gateway.d.ts +196 -0
  40. package/dist/gateway.js +79 -0
  41. package/dist/gateway.js.map +1 -0
  42. package/dist/hlc-DiD8QNG3.d.ts +70 -0
  43. package/dist/index.d.ts +245 -0
  44. package/dist/index.js +102 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/json-dYtqiL0F.d.ts +18 -0
  47. package/dist/nessie-client-DrNikVXy.d.ts +160 -0
  48. package/dist/parquet.d.ts +78 -0
  49. package/dist/parquet.js +15 -0
  50. package/dist/parquet.js.map +1 -0
  51. package/dist/proto.d.ts +434 -0
  52. package/dist/proto.js +67 -0
  53. package/dist/proto.js.map +1 -0
  54. package/dist/react.d.ts +147 -0
  55. package/dist/react.js +224 -0
  56. package/dist/react.js.map +1 -0
  57. package/dist/resolver-C3Wphi6O.d.ts +10 -0
  58. package/dist/result-CojzlFE2.d.ts +64 -0
  59. package/dist/src-QU2YLPZY.js +383 -0
  60. package/dist/src-QU2YLPZY.js.map +1 -0
  61. package/dist/src-WYBF5LOI.js +102 -0
  62. package/dist/src-WYBF5LOI.js.map +1 -0
  63. package/dist/src-WZNPHANQ.js +426 -0
  64. package/dist/src-WZNPHANQ.js.map +1 -0
  65. package/dist/types-Bs-QyOe-.d.ts +143 -0
  66. package/dist/types-DAQL_vU_.d.ts +118 -0
  67. package/dist/types-DSC_EiwR.d.ts +45 -0
  68. package/dist/types-V_jVu2sA.d.ts +73 -0
  69. package/package.json +119 -0
@@ -0,0 +1,1115 @@
1
+ import {
2
+ isDatabaseAdapter
3
+ } from "./chunk-X3RO5SYJ.js";
4
+ import {
5
+ buildPartitionSpec,
6
+ lakeSyncTableName,
7
+ tableSchemaToIceberg
8
+ } from "./chunk-P5DRFKIT.js";
9
+ import {
10
+ writeDeltasToParquet
11
+ } from "./chunk-4ARO6KTJ.js";
12
+ import {
13
+ AdapterNotFoundError,
14
+ BackpressureError,
15
+ Err,
16
+ FlushError,
17
+ HLC,
18
+ Ok,
19
+ SchemaError,
20
+ bigintReplacer,
21
+ bigintReviver,
22
+ filterDeltas,
23
+ resolveLWW,
24
+ rowKey,
25
+ toError,
26
+ validateAction,
27
+ validateConnectorConfig,
28
+ validateSyncRules
29
+ } from "./chunk-ICNT7I3K.js";
30
+
31
+ // ../gateway/src/action-dispatcher.ts
32
+ var ActionDispatcher = class {
33
+ actionHandlers = /* @__PURE__ */ new Map();
34
+ executedActions = /* @__PURE__ */ new Set();
35
+ idempotencyMap = /* @__PURE__ */ new Map();
36
+ constructor(handlers) {
37
+ if (handlers) {
38
+ for (const [name, handler] of Object.entries(handlers)) {
39
+ this.actionHandlers.set(name, handler);
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Dispatch an action push to registered handlers.
45
+ *
46
+ * Iterates over actions, dispatches each to the registered ActionHandler
47
+ * by connector name. Supports idempotency via actionId deduplication and
48
+ * idempotencyKey mapping.
49
+ *
50
+ * @param msg - The action push containing one or more actions.
51
+ * @param hlcNow - Callback to get the current server HLC timestamp.
52
+ * @param context - Optional auth context for permission checks.
53
+ * @returns A `Result` containing results for each action.
54
+ */
55
+ async dispatch(msg, hlcNow, context) {
56
+ const results = [];
57
+ for (const action of msg.actions) {
58
+ const validation = validateAction(action);
59
+ if (!validation.ok) {
60
+ return Err(validation.error);
61
+ }
62
+ if (this.executedActions.has(action.actionId)) {
63
+ const cached = this.idempotencyMap.get(action.actionId);
64
+ if (cached) {
65
+ results.push(cached);
66
+ continue;
67
+ }
68
+ continue;
69
+ }
70
+ if (action.idempotencyKey) {
71
+ const cached = this.idempotencyMap.get(`idem:${action.idempotencyKey}`);
72
+ if (cached) {
73
+ results.push(cached);
74
+ continue;
75
+ }
76
+ }
77
+ const handler = this.actionHandlers.get(action.connector);
78
+ if (!handler) {
79
+ const errorResult = {
80
+ actionId: action.actionId,
81
+ code: "ACTION_NOT_SUPPORTED",
82
+ message: `No action handler registered for connector "${action.connector}"`,
83
+ retryable: false
84
+ };
85
+ results.push(errorResult);
86
+ this.cacheActionResult(action, errorResult);
87
+ continue;
88
+ }
89
+ const supported = handler.supportedActions.some((d) => d.actionType === action.actionType);
90
+ if (!supported) {
91
+ const errorResult = {
92
+ actionId: action.actionId,
93
+ code: "ACTION_NOT_SUPPORTED",
94
+ message: `Action type "${action.actionType}" not supported by connector "${action.connector}"`,
95
+ retryable: false
96
+ };
97
+ results.push(errorResult);
98
+ this.cacheActionResult(action, errorResult);
99
+ continue;
100
+ }
101
+ const execResult = await handler.executeAction(action, context);
102
+ if (execResult.ok) {
103
+ results.push(execResult.value);
104
+ this.cacheActionResult(action, execResult.value);
105
+ } else {
106
+ const err = execResult.error;
107
+ const errorResult = {
108
+ actionId: action.actionId,
109
+ code: err.code,
110
+ message: err.message,
111
+ retryable: "retryable" in err ? err.retryable : false
112
+ };
113
+ results.push(errorResult);
114
+ if (!errorResult.retryable) {
115
+ this.cacheActionResult(action, errorResult);
116
+ }
117
+ }
118
+ }
119
+ const serverHlc = hlcNow();
120
+ return Ok({ results, serverHlc });
121
+ }
122
+ /**
123
+ * Register a named action handler.
124
+ *
125
+ * @param name - Connector name (matches `Action.connector`).
126
+ * @param handler - The action handler to register.
127
+ */
128
+ registerHandler(name, handler) {
129
+ this.actionHandlers.set(name, handler);
130
+ }
131
+ /**
132
+ * Unregister a named action handler.
133
+ *
134
+ * @param name - The connector name to remove.
135
+ */
136
+ unregisterHandler(name) {
137
+ this.actionHandlers.delete(name);
138
+ }
139
+ /**
140
+ * List all registered action handler names.
141
+ *
142
+ * @returns Array of registered connector names.
143
+ */
144
+ listHandlers() {
145
+ return [...this.actionHandlers.keys()];
146
+ }
147
+ /**
148
+ * Describe all registered action handlers and their supported actions.
149
+ *
150
+ * Returns a map of connector name to its {@link ActionDescriptor} array,
151
+ * enabling frontend discovery of available actions.
152
+ *
153
+ * @returns An {@link ActionDiscovery} object listing connectors and their actions.
154
+ */
155
+ describe() {
156
+ const connectors = {};
157
+ for (const [name, handler] of this.actionHandlers) {
158
+ connectors[name] = handler.supportedActions;
159
+ }
160
+ return { connectors };
161
+ }
162
+ /** Cache an action result for idempotency deduplication. */
163
+ cacheActionResult(action, result) {
164
+ this.executedActions.add(action.actionId);
165
+ this.idempotencyMap.set(action.actionId, result);
166
+ if (action.idempotencyKey) {
167
+ this.idempotencyMap.set(`idem:${action.idempotencyKey}`, result);
168
+ }
169
+ }
170
+ };
171
+
172
+ // ../gateway/src/buffer.ts
173
+ var BASE_DELTA_OVERHEAD = 8 + 8 + 8 + 8 + 1;
174
+ function estimateValueBytes(value) {
175
+ if (value === null || value === void 0) return 4;
176
+ switch (typeof value) {
177
+ case "boolean":
178
+ return 4;
179
+ case "number":
180
+ return 8;
181
+ case "bigint":
182
+ return 8;
183
+ case "string":
184
+ return value.length * 2;
185
+ // UTF-16
186
+ default:
187
+ try {
188
+ return JSON.stringify(value).length;
189
+ } catch {
190
+ return 100;
191
+ }
192
+ }
193
+ }
194
+ function estimateDeltaBytes(delta) {
195
+ let bytes = BASE_DELTA_OVERHEAD;
196
+ bytes += delta.deltaId.length;
197
+ bytes += delta.table.length * 2;
198
+ bytes += delta.rowId.length * 2;
199
+ bytes += delta.clientId.length * 2;
200
+ for (const col of delta.columns) {
201
+ bytes += col.column.length * 2;
202
+ bytes += estimateValueBytes(col.value);
203
+ }
204
+ return bytes;
205
+ }
206
+ var DeltaBuffer = class {
207
+ log = [];
208
+ index = /* @__PURE__ */ new Map();
209
+ deltaIds = /* @__PURE__ */ new Set();
210
+ estimatedBytes = 0;
211
+ createdAt = Date.now();
212
+ tableBytes = /* @__PURE__ */ new Map();
213
+ tableLog = /* @__PURE__ */ new Map();
214
+ /** Append a delta to the log and upsert the index (post-conflict-resolution). */
215
+ append(delta) {
216
+ this.log.push(delta);
217
+ const key = rowKey(delta.table, delta.rowId);
218
+ this.index.set(key, delta);
219
+ this.deltaIds.add(delta.deltaId);
220
+ const bytes = estimateDeltaBytes(delta);
221
+ this.estimatedBytes += bytes;
222
+ this.tableBytes.set(delta.table, (this.tableBytes.get(delta.table) ?? 0) + bytes);
223
+ const tableEntries = this.tableLog.get(delta.table);
224
+ if (tableEntries) {
225
+ tableEntries.push(delta);
226
+ } else {
227
+ this.tableLog.set(delta.table, [delta]);
228
+ }
229
+ }
230
+ /** Get the current merged state for a row (for conflict resolution). */
231
+ getRow(key) {
232
+ return this.index.get(key);
233
+ }
234
+ /** Check if a delta with this ID already exists in the log (for idempotency). */
235
+ hasDelta(deltaId) {
236
+ return this.deltaIds.has(deltaId);
237
+ }
238
+ /** Return change events from the log since a given HLC. */
239
+ getEventsSince(hlc, limit) {
240
+ let lo = 0;
241
+ let hi = this.log.length;
242
+ while (lo < hi) {
243
+ const mid = lo + hi >>> 1;
244
+ if (HLC.compare(this.log[mid].hlc, hlc) <= 0) {
245
+ lo = mid + 1;
246
+ } else {
247
+ hi = mid;
248
+ }
249
+ }
250
+ const hasMore = this.log.length - lo > limit;
251
+ return { deltas: this.log.slice(lo, lo + limit), hasMore };
252
+ }
253
+ /** Check if the buffer should be flushed based on size or age thresholds. */
254
+ shouldFlush(config) {
255
+ if (this.log.length === 0) return false;
256
+ return this.estimatedBytes >= config.maxBytes || Date.now() - this.createdAt >= config.maxAgeMs;
257
+ }
258
+ /** Per-table buffer statistics. */
259
+ tableStats() {
260
+ const stats = [];
261
+ for (const [table, bytes] of this.tableBytes) {
262
+ stats.push({
263
+ table,
264
+ byteSize: bytes,
265
+ deltaCount: this.tableLog.get(table)?.length ?? 0
266
+ });
267
+ }
268
+ return stats;
269
+ }
270
+ /** Drain only the specified table's deltas, leaving other tables intact. */
271
+ drainTable(table) {
272
+ const tableDeltas = this.tableLog.get(table) ?? [];
273
+ if (tableDeltas.length === 0) return [];
274
+ this.log = this.log.filter((d) => d.table !== table);
275
+ for (const delta of tableDeltas) {
276
+ this.index.delete(rowKey(delta.table, delta.rowId));
277
+ this.deltaIds.delete(delta.deltaId);
278
+ }
279
+ const tableByteSize = this.tableBytes.get(table) ?? 0;
280
+ this.estimatedBytes -= tableByteSize;
281
+ this.tableBytes.delete(table);
282
+ this.tableLog.delete(table);
283
+ return tableDeltas;
284
+ }
285
+ /** Drain the log for flush. Returns log entries and clears both structures. */
286
+ drain() {
287
+ const entries = [...this.log];
288
+ this.log = [];
289
+ this.index.clear();
290
+ this.deltaIds.clear();
291
+ this.estimatedBytes = 0;
292
+ this.createdAt = Date.now();
293
+ this.tableBytes.clear();
294
+ this.tableLog.clear();
295
+ return entries;
296
+ }
297
+ /** Number of log entries */
298
+ get logSize() {
299
+ return this.log.length;
300
+ }
301
+ /** Number of unique rows in the index */
302
+ get indexSize() {
303
+ return this.index.size;
304
+ }
305
+ /** Estimated byte size of the buffer */
306
+ get byteSize() {
307
+ return this.estimatedBytes;
308
+ }
309
+ /** Average byte size per delta in the buffer (0 if empty). */
310
+ get averageDeltaBytes() {
311
+ return this.log.length === 0 ? 0 : this.estimatedBytes / this.log.length;
312
+ }
313
+ };
314
+
315
+ // ../gateway/src/config-store.ts
316
+ var MemoryConfigStore = class {
317
+ schemas = /* @__PURE__ */ new Map();
318
+ syncRules = /* @__PURE__ */ new Map();
319
+ connectors = {};
320
+ async getSchema(gatewayId) {
321
+ return this.schemas.get(gatewayId);
322
+ }
323
+ async setSchema(gatewayId, schema) {
324
+ this.schemas.set(gatewayId, schema);
325
+ }
326
+ async getSyncRules(gatewayId) {
327
+ return this.syncRules.get(gatewayId);
328
+ }
329
+ async setSyncRules(gatewayId, rules) {
330
+ this.syncRules.set(gatewayId, rules);
331
+ }
332
+ async getConnectors() {
333
+ return { ...this.connectors };
334
+ }
335
+ async setConnectors(connectors) {
336
+ this.connectors = { ...connectors };
337
+ }
338
+ };
339
+
340
+ // ../gateway/src/constants.ts
341
+ var MAX_PUSH_PAYLOAD_BYTES = 1048576;
342
+ var MAX_DELTAS_PER_PUSH = 1e4;
343
+ var MAX_PULL_LIMIT = 1e4;
344
+ var DEFAULT_PULL_LIMIT = 100;
345
+ var VALID_COLUMN_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "json", "null"]);
346
+ var DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
347
+ var DEFAULT_MAX_BUFFER_AGE_MS = 3e4;
348
+
349
+ // ../gateway/src/flush.ts
350
+ function hlcRange(entries) {
351
+ let min = entries[0].hlc;
352
+ let max = entries[0].hlc;
353
+ for (let i = 1; i < entries.length; i++) {
354
+ const hlc = entries[i].hlc;
355
+ if (HLC.compare(hlc, min) < 0) min = hlc;
356
+ if (HLC.compare(hlc, max) > 0) max = hlc;
357
+ }
358
+ return { min, max };
359
+ }
360
+ async function flushEntries(entries, byteSize, deps, keyPrefix) {
361
+ if (isDatabaseAdapter(deps.adapter)) {
362
+ try {
363
+ const result = await deps.adapter.insertDeltas(entries);
364
+ if (!result.ok) {
365
+ deps.restoreEntries(entries);
366
+ return Err(new FlushError(`Database flush failed: ${result.error.message}`));
367
+ }
368
+ return Ok(void 0);
369
+ } catch (error) {
370
+ deps.restoreEntries(entries);
371
+ return Err(new FlushError(`Unexpected database flush failure: ${toError(error).message}`));
372
+ }
373
+ }
374
+ try {
375
+ const { min, max } = hlcRange(entries);
376
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
377
+ const prefix = keyPrefix ? `${keyPrefix}-` : "";
378
+ let objectKey;
379
+ let data;
380
+ let contentType;
381
+ if (deps.config.flushFormat === "json") {
382
+ const envelope = {
383
+ version: 1,
384
+ gatewayId: deps.config.gatewayId,
385
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
386
+ hlcRange: { min, max },
387
+ deltaCount: entries.length,
388
+ byteSize,
389
+ deltas: entries
390
+ };
391
+ objectKey = `deltas/${date}/${deps.config.gatewayId}/${prefix}${min.toString()}-${max.toString()}.json`;
392
+ data = new TextEncoder().encode(JSON.stringify(envelope, bigintReplacer));
393
+ contentType = "application/json";
394
+ } else {
395
+ if (!deps.config.tableSchema) {
396
+ deps.restoreEntries(entries);
397
+ return Err(new FlushError("tableSchema required for Parquet flush"));
398
+ }
399
+ const parquetResult = await writeDeltasToParquet(entries, deps.config.tableSchema);
400
+ if (!parquetResult.ok) {
401
+ deps.restoreEntries(entries);
402
+ return Err(parquetResult.error);
403
+ }
404
+ objectKey = `deltas/${date}/${deps.config.gatewayId}/${prefix}${min.toString()}-${max.toString()}.parquet`;
405
+ data = parquetResult.value;
406
+ contentType = "application/vnd.apache.parquet";
407
+ }
408
+ const result = await deps.adapter.putObject(objectKey, data, contentType);
409
+ if (!result.ok) {
410
+ deps.restoreEntries(entries);
411
+ return Err(new FlushError(`Failed to write flush envelope: ${result.error.message}`));
412
+ }
413
+ if (deps.config.catalogue && deps.config.tableSchema) {
414
+ await commitToCatalogue(
415
+ objectKey,
416
+ data.byteLength,
417
+ entries.length,
418
+ deps.config.catalogue,
419
+ deps.config.tableSchema
420
+ );
421
+ }
422
+ return Ok(void 0);
423
+ } catch (error) {
424
+ deps.restoreEntries(entries);
425
+ return Err(new FlushError(`Unexpected flush failure: ${toError(error).message}`));
426
+ }
427
+ }
428
+ async function commitToCatalogue(objectKey, fileSizeInBytes, recordCount, catalogue, schema) {
429
+ const { namespace, name } = lakeSyncTableName(schema.table);
430
+ const icebergSchema = tableSchemaToIceberg(schema);
431
+ const partitionSpec = buildPartitionSpec(icebergSchema);
432
+ await catalogue.createNamespace(namespace);
433
+ const createResult = await catalogue.createTable(namespace, name, icebergSchema, partitionSpec);
434
+ if (!createResult.ok && createResult.error.statusCode !== 409) {
435
+ return;
436
+ }
437
+ const dataFile = {
438
+ content: "data",
439
+ "file-path": objectKey,
440
+ "file-format": "PARQUET",
441
+ "record-count": recordCount,
442
+ "file-size-in-bytes": fileSizeInBytes
443
+ };
444
+ const appendResult = await catalogue.appendFiles(namespace, name, [dataFile]);
445
+ if (!appendResult.ok && appendResult.error.statusCode === 409) {
446
+ await catalogue.appendFiles(namespace, name, [dataFile]);
447
+ }
448
+ }
449
+
450
+ // ../gateway/src/gateway.ts
451
+ var SyncGateway = class {
452
+ hlc;
453
+ buffer;
454
+ actions;
455
+ config;
456
+ adapter;
457
+ flushing = false;
458
+ constructor(config, adapter) {
459
+ this.config = { sourceAdapters: {}, ...config };
460
+ this.hlc = new HLC();
461
+ this.buffer = new DeltaBuffer();
462
+ this.adapter = this.config.adapter ?? adapter ?? null;
463
+ this.actions = new ActionDispatcher(config.actionHandlers);
464
+ }
465
+ /** Restore drained entries back to the buffer for retry. */
466
+ restoreEntries(entries) {
467
+ for (const entry of entries) {
468
+ this.buffer.append(entry);
469
+ }
470
+ }
471
+ /**
472
+ * Handle an incoming push from a client.
473
+ *
474
+ * Validates HLC drift, resolves conflicts via LWW, and appends to the buffer.
475
+ *
476
+ * @param msg - The push message containing client deltas.
477
+ * @returns A `Result` with the new server HLC and accepted count,
478
+ * or a `ClockDriftError` if the client clock is too far ahead.
479
+ */
480
+ handlePush(msg) {
481
+ const backpressureLimit = this.config.maxBackpressureBytes ?? this.config.maxBufferBytes * 2;
482
+ if (this.buffer.byteSize >= backpressureLimit) {
483
+ return Err(
484
+ new BackpressureError(
485
+ `Buffer backpressure exceeded (${this.buffer.byteSize} >= ${backpressureLimit} bytes)`
486
+ )
487
+ );
488
+ }
489
+ let accepted = 0;
490
+ const ingested = [];
491
+ for (const delta of msg.deltas) {
492
+ if (this.buffer.hasDelta(delta.deltaId)) {
493
+ accepted++;
494
+ continue;
495
+ }
496
+ if (this.config.schemaManager) {
497
+ const schemaResult = this.config.schemaManager.validateDelta(delta);
498
+ if (!schemaResult.ok) {
499
+ return Err(schemaResult.error);
500
+ }
501
+ }
502
+ const recvResult = this.hlc.recv(delta.hlc);
503
+ if (!recvResult.ok) {
504
+ return Err(recvResult.error);
505
+ }
506
+ const key = rowKey(delta.table, delta.rowId);
507
+ const existing = this.buffer.getRow(key);
508
+ if (existing) {
509
+ const resolved = resolveLWW(existing, delta);
510
+ if (resolved.ok) {
511
+ this.buffer.append(resolved.value);
512
+ ingested.push(resolved.value);
513
+ }
514
+ } else {
515
+ this.buffer.append(delta);
516
+ ingested.push(delta);
517
+ }
518
+ accepted++;
519
+ }
520
+ const serverHlc = this.hlc.now();
521
+ return Ok({ serverHlc, accepted, deltas: ingested });
522
+ }
523
+ handlePull(msg, context) {
524
+ if (msg.source) {
525
+ return this.handleAdapterPull(msg, context);
526
+ }
527
+ return this.handleBufferPull(msg, context);
528
+ }
529
+ /** Pull from the in-memory buffer (original path). */
530
+ handleBufferPull(msg, context) {
531
+ if (!context) {
532
+ const { deltas, hasMore: hasMore2 } = this.buffer.getEventsSince(msg.sinceHlc, msg.maxDeltas);
533
+ const serverHlc2 = this.hlc.now();
534
+ return Ok({ deltas, serverHlc: serverHlc2, hasMore: hasMore2 });
535
+ }
536
+ const maxRetries = 5;
537
+ const overFetchMultiplier = 3;
538
+ let cursor = msg.sinceHlc;
539
+ const collected = [];
540
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
541
+ const fetchLimit = msg.maxDeltas * overFetchMultiplier;
542
+ const { deltas: raw, hasMore: rawHasMore } = this.buffer.getEventsSince(cursor, fetchLimit);
543
+ if (raw.length === 0) {
544
+ const serverHlc2 = this.hlc.now();
545
+ return Ok({ deltas: collected, serverHlc: serverHlc2, hasMore: false });
546
+ }
547
+ const filtered = filterDeltas(raw, context);
548
+ collected.push(...filtered);
549
+ if (collected.length >= msg.maxDeltas) {
550
+ const trimmed2 = collected.slice(0, msg.maxDeltas);
551
+ const serverHlc2 = this.hlc.now();
552
+ return Ok({ deltas: trimmed2, serverHlc: serverHlc2, hasMore: true });
553
+ }
554
+ if (!rawHasMore) {
555
+ const serverHlc2 = this.hlc.now();
556
+ return Ok({ deltas: collected, serverHlc: serverHlc2, hasMore: false });
557
+ }
558
+ cursor = raw[raw.length - 1].hlc;
559
+ }
560
+ const serverHlc = this.hlc.now();
561
+ const hasMore = collected.length >= msg.maxDeltas;
562
+ const trimmed = collected.slice(0, msg.maxDeltas);
563
+ return Ok({ deltas: trimmed, serverHlc, hasMore });
564
+ }
565
+ /** Pull from a named source adapter. */
566
+ async handleAdapterPull(msg, context) {
567
+ const adapter = this.config.sourceAdapters?.[msg.source];
568
+ if (!adapter) {
569
+ return Err(new AdapterNotFoundError(`Source adapter "${msg.source}" not found`));
570
+ }
571
+ const queryResult = await adapter.queryDeltasSince(msg.sinceHlc);
572
+ if (!queryResult.ok) {
573
+ return Err(queryResult.error);
574
+ }
575
+ let deltas = queryResult.value;
576
+ if (context) {
577
+ deltas = filterDeltas(deltas, context);
578
+ }
579
+ const hasMore = deltas.length > msg.maxDeltas;
580
+ const sliced = deltas.slice(0, msg.maxDeltas);
581
+ const serverHlc = this.hlc.now();
582
+ return Ok({ deltas: sliced, serverHlc, hasMore });
583
+ }
584
+ // -----------------------------------------------------------------------
585
+ // Flush — delegates to flush module
586
+ // -----------------------------------------------------------------------
587
+ /**
588
+ * Flush the buffer to the configured adapter.
589
+ *
590
+ * Writes deltas as either a Parquet file (default) or a JSON
591
+ * {@link FlushEnvelope} to the adapter, depending on
592
+ * `config.flushFormat`. If the write fails, the buffer entries
593
+ * are restored so they can be retried.
594
+ *
595
+ * @returns A `Result` indicating success or a `FlushError`.
596
+ */
597
+ async flush() {
598
+ if (this.flushing) {
599
+ return Err(new FlushError("Flush already in progress"));
600
+ }
601
+ if (this.buffer.logSize === 0) {
602
+ return Ok(void 0);
603
+ }
604
+ if (!this.adapter) {
605
+ return Err(new FlushError("No adapter configured"));
606
+ }
607
+ this.flushing = true;
608
+ if (isDatabaseAdapter(this.adapter)) {
609
+ const entries2 = this.buffer.drain();
610
+ if (entries2.length === 0) {
611
+ this.flushing = false;
612
+ return Ok(void 0);
613
+ }
614
+ try {
615
+ return await flushEntries(entries2, 0, {
616
+ adapter: this.adapter,
617
+ config: {
618
+ gatewayId: this.config.gatewayId,
619
+ flushFormat: this.config.flushFormat,
620
+ tableSchema: this.config.tableSchema,
621
+ catalogue: this.config.catalogue
622
+ },
623
+ restoreEntries: (e) => this.restoreEntries(e)
624
+ });
625
+ } finally {
626
+ this.flushing = false;
627
+ }
628
+ }
629
+ const byteSize = this.buffer.byteSize;
630
+ const entries = this.buffer.drain();
631
+ try {
632
+ return await flushEntries(entries, byteSize, {
633
+ adapter: this.adapter,
634
+ config: {
635
+ gatewayId: this.config.gatewayId,
636
+ flushFormat: this.config.flushFormat,
637
+ tableSchema: this.config.tableSchema,
638
+ catalogue: this.config.catalogue
639
+ },
640
+ restoreEntries: (e) => this.restoreEntries(e)
641
+ });
642
+ } finally {
643
+ this.flushing = false;
644
+ }
645
+ }
646
+ /**
647
+ * Flush a single table's deltas from the buffer.
648
+ *
649
+ * Drains only the specified table's deltas and flushes them,
650
+ * leaving other tables in the buffer.
651
+ */
652
+ async flushTable(table) {
653
+ if (this.flushing) {
654
+ return Err(new FlushError("Flush already in progress"));
655
+ }
656
+ if (!this.adapter) {
657
+ return Err(new FlushError("No adapter configured"));
658
+ }
659
+ const entries = this.buffer.drainTable(table);
660
+ if (entries.length === 0) {
661
+ return Ok(void 0);
662
+ }
663
+ this.flushing = true;
664
+ try {
665
+ return await flushEntries(
666
+ entries,
667
+ 0,
668
+ {
669
+ adapter: this.adapter,
670
+ config: {
671
+ gatewayId: this.config.gatewayId,
672
+ flushFormat: this.config.flushFormat,
673
+ tableSchema: this.config.tableSchema,
674
+ catalogue: this.config.catalogue
675
+ },
676
+ restoreEntries: (e) => this.restoreEntries(e)
677
+ },
678
+ table
679
+ );
680
+ } finally {
681
+ this.flushing = false;
682
+ }
683
+ }
684
+ // -----------------------------------------------------------------------
685
+ // Actions — delegates to ActionDispatcher
686
+ // -----------------------------------------------------------------------
687
+ /** Handle an incoming action push from a client. */
688
+ async handleAction(msg, context) {
689
+ return this.actions.dispatch(msg, () => this.hlc.now(), context);
690
+ }
691
+ /** Register a named action handler. */
692
+ registerActionHandler(name, handler) {
693
+ this.actions.registerHandler(name, handler);
694
+ }
695
+ /** Unregister a named action handler. */
696
+ unregisterActionHandler(name) {
697
+ this.actions.unregisterHandler(name);
698
+ }
699
+ /** List all registered action handler names. */
700
+ listActionHandlers() {
701
+ return this.actions.listHandlers();
702
+ }
703
+ /** Describe all registered action handlers and their supported actions. */
704
+ describeActions() {
705
+ return this.actions.describe();
706
+ }
707
+ // -----------------------------------------------------------------------
708
+ // Source adapters
709
+ // -----------------------------------------------------------------------
710
+ /**
711
+ * Register a named source adapter for adapter-sourced pulls.
712
+ *
713
+ * @param name - Unique source name (used as the `source` parameter in pull requests).
714
+ * @param adapter - The database adapter to register.
715
+ */
716
+ registerSource(name, adapter) {
717
+ this.config.sourceAdapters[name] = adapter;
718
+ }
719
+ /**
720
+ * Unregister a named source adapter.
721
+ *
722
+ * @param name - The source name to remove.
723
+ */
724
+ unregisterSource(name) {
725
+ delete this.config.sourceAdapters[name];
726
+ }
727
+ /**
728
+ * List all registered source adapter names.
729
+ *
730
+ * @returns Array of registered source adapter names.
731
+ */
732
+ listSources() {
733
+ return Object.keys(this.config.sourceAdapters);
734
+ }
735
+ // -----------------------------------------------------------------------
736
+ // Buffer queries
737
+ // -----------------------------------------------------------------------
738
+ /** Get per-table buffer statistics. */
739
+ get tableStats() {
740
+ return this.buffer.tableStats();
741
+ }
742
+ /**
743
+ * Get tables that exceed the per-table budget.
744
+ */
745
+ getTablesExceedingBudget() {
746
+ const budget = this.config.perTableBudgetBytes;
747
+ if (!budget) return [];
748
+ return this.buffer.tableStats().filter((s) => s.byteSize >= budget).map((s) => s.table);
749
+ }
750
+ /** Check if the buffer should be flushed based on config thresholds. */
751
+ shouldFlush() {
752
+ let effectiveMaxBytes = this.config.maxBufferBytes;
753
+ const adaptive = this.config.adaptiveBufferConfig;
754
+ if (adaptive && this.buffer.averageDeltaBytes > adaptive.wideColumnThreshold) {
755
+ effectiveMaxBytes = Math.floor(effectiveMaxBytes * adaptive.reductionFactor);
756
+ }
757
+ return this.buffer.shouldFlush({
758
+ maxBytes: effectiveMaxBytes,
759
+ maxAgeMs: this.config.maxBufferAgeMs
760
+ });
761
+ }
762
+ /** Get buffer statistics for monitoring. */
763
+ get bufferStats() {
764
+ return {
765
+ logSize: this.buffer.logSize,
766
+ indexSize: this.buffer.indexSize,
767
+ byteSize: this.buffer.byteSize
768
+ };
769
+ }
770
+ };
771
+
772
+ // ../gateway/src/validation.ts
773
+ function validatePushBody(raw, headerClientId) {
774
+ let body;
775
+ try {
776
+ body = JSON.parse(raw, bigintReviver);
777
+ } catch {
778
+ return Err({ status: 400, message: "Invalid JSON body" });
779
+ }
780
+ if (!body.clientId || !Array.isArray(body.deltas)) {
781
+ return Err({ status: 400, message: "Missing required fields: clientId, deltas" });
782
+ }
783
+ if (headerClientId && body.clientId !== headerClientId) {
784
+ return Err({
785
+ status: 403,
786
+ message: "Client ID mismatch: push clientId does not match authenticated identity"
787
+ });
788
+ }
789
+ if (body.deltas.length > MAX_DELTAS_PER_PUSH) {
790
+ return Err({ status: 400, message: "Too many deltas in a single push (max 10,000)" });
791
+ }
792
+ return Ok(body);
793
+ }
794
+ function parsePullParams(params) {
795
+ if (!params.since || !params.clientId) {
796
+ return Err({ status: 400, message: "Missing required query params: since, clientId" });
797
+ }
798
+ let sinceHlc;
799
+ try {
800
+ sinceHlc = BigInt(params.since);
801
+ } catch {
802
+ return Err({
803
+ status: 400,
804
+ message: "Invalid 'since' parameter \u2014 must be a decimal integer"
805
+ });
806
+ }
807
+ const rawLimit = params.limit ? Number.parseInt(params.limit, 10) : DEFAULT_PULL_LIMIT;
808
+ if (Number.isNaN(rawLimit) || rawLimit < 1) {
809
+ return Err({
810
+ status: 400,
811
+ message: "Invalid 'limit' parameter \u2014 must be a positive integer"
812
+ });
813
+ }
814
+ const maxDeltas = Math.min(rawLimit, MAX_PULL_LIMIT);
815
+ const msg = {
816
+ clientId: params.clientId,
817
+ sinceHlc,
818
+ maxDeltas,
819
+ ...params.source ? { source: params.source } : {}
820
+ };
821
+ return Ok(msg);
822
+ }
823
+ function validateActionBody(raw, headerClientId) {
824
+ let body;
825
+ try {
826
+ body = JSON.parse(raw, bigintReviver);
827
+ } catch {
828
+ return Err({ status: 400, message: "Invalid JSON body" });
829
+ }
830
+ if (!body.clientId || !Array.isArray(body.actions)) {
831
+ return Err({ status: 400, message: "Missing required fields: clientId, actions" });
832
+ }
833
+ if (headerClientId && body.clientId !== headerClientId) {
834
+ return Err({
835
+ status: 403,
836
+ message: "Client ID mismatch: action clientId does not match authenticated identity"
837
+ });
838
+ }
839
+ return Ok(body);
840
+ }
841
+ function validateSchemaBody(raw) {
842
+ let schema;
843
+ try {
844
+ schema = JSON.parse(raw);
845
+ } catch {
846
+ return Err({ status: 400, message: "Invalid JSON body" });
847
+ }
848
+ if (!schema.table || !Array.isArray(schema.columns)) {
849
+ return Err({ status: 400, message: "Missing required fields: table, columns" });
850
+ }
851
+ for (const col of schema.columns) {
852
+ if (typeof col.name !== "string" || col.name.length === 0) {
853
+ return Err({ status: 400, message: "Each column must have a non-empty 'name' string" });
854
+ }
855
+ if (!VALID_COLUMN_TYPES.has(col.type)) {
856
+ return Err({
857
+ status: 400,
858
+ message: `Invalid column type "${col.type}" for column "${col.name}". Allowed: string, number, boolean, json, null`
859
+ });
860
+ }
861
+ }
862
+ return Ok(schema);
863
+ }
864
+ function pushErrorToStatus(code) {
865
+ switch (code) {
866
+ case "CLOCK_DRIFT":
867
+ return 409;
868
+ case "SCHEMA_MISMATCH":
869
+ return 422;
870
+ case "BACKPRESSURE":
871
+ return 503;
872
+ default:
873
+ return 500;
874
+ }
875
+ }
876
+ function buildSyncRulesContext(rules, claims) {
877
+ if (!rules || rules.buckets.length === 0) {
878
+ return void 0;
879
+ }
880
+ return { claims, rules };
881
+ }
882
+
883
+ // ../gateway/src/request-handler.ts
884
+ function handlePushRequest(gateway, raw, headerClientId, opts) {
885
+ const validation = validatePushBody(raw, headerClientId);
886
+ if (!validation.ok) {
887
+ return { status: validation.error.status, body: { error: validation.error.message } };
888
+ }
889
+ const body = validation.value;
890
+ opts?.persistBatch?.(body.deltas);
891
+ const result = gateway.handlePush(body);
892
+ if (!result.ok) {
893
+ return {
894
+ status: pushErrorToStatus(result.error.code),
895
+ body: { error: result.error.message }
896
+ };
897
+ }
898
+ opts?.clearPersistence?.();
899
+ if (opts?.broadcastFn && result.value.deltas.length > 0) {
900
+ opts.broadcastFn(result.value.deltas, result.value.serverHlc, body.clientId);
901
+ }
902
+ return { status: 200, body: result.value };
903
+ }
904
+ async function handlePullRequest(gateway, params, claims, syncRules) {
905
+ const validation = parsePullParams(params);
906
+ if (!validation.ok) {
907
+ return { status: validation.error.status, body: { error: validation.error.message } };
908
+ }
909
+ const msg = validation.value;
910
+ const context = buildSyncRulesContext(syncRules, claims ?? {});
911
+ const result = msg.source ? await gateway.handlePull(
912
+ msg,
913
+ context
914
+ ) : gateway.handlePull(msg, context);
915
+ if (!result.ok) {
916
+ const err = result.error;
917
+ if (err.code === "ADAPTER_NOT_FOUND") {
918
+ return { status: 404, body: { error: err.message } };
919
+ }
920
+ return { status: 500, body: { error: err.message } };
921
+ }
922
+ return { status: 200, body: result.value };
923
+ }
924
+ async function handleActionRequest(gateway, raw, headerClientId, claims) {
925
+ const validation = validateActionBody(raw, headerClientId);
926
+ if (!validation.ok) {
927
+ return { status: validation.error.status, body: { error: validation.error.message } };
928
+ }
929
+ const context = claims ? { claims } : void 0;
930
+ const result = await gateway.handleAction(validation.value, context);
931
+ if (!result.ok) {
932
+ return { status: 400, body: { error: result.error.message } };
933
+ }
934
+ return { status: 200, body: result.value };
935
+ }
936
+ async function handleFlushRequest(gateway, opts) {
937
+ const result = await gateway.flush();
938
+ if (!result.ok) {
939
+ return { status: 500, body: { error: result.error.message } };
940
+ }
941
+ opts?.clearPersistence?.();
942
+ return { status: 200, body: { flushed: true } };
943
+ }
944
+ async function handleSaveSchema(raw, store, gatewayId) {
945
+ const validation = validateSchemaBody(raw);
946
+ if (!validation.ok) {
947
+ return { status: validation.error.status, body: { error: validation.error.message } };
948
+ }
949
+ await store.setSchema(gatewayId, validation.value);
950
+ return { status: 200, body: { saved: true } };
951
+ }
952
+ async function handleSaveSyncRules(raw, store, gatewayId) {
953
+ let config;
954
+ try {
955
+ config = JSON.parse(raw);
956
+ } catch {
957
+ return { status: 400, body: { error: "Invalid JSON body" } };
958
+ }
959
+ const validation = validateSyncRules(config);
960
+ if (!validation.ok) {
961
+ return { status: 400, body: { error: validation.error.message } };
962
+ }
963
+ await store.setSyncRules(gatewayId, config);
964
+ return { status: 200, body: { saved: true } };
965
+ }
966
+ async function handleRegisterConnector(raw, store) {
967
+ let body;
968
+ try {
969
+ body = JSON.parse(raw);
970
+ } catch {
971
+ return { status: 400, body: { error: "Invalid JSON body" } };
972
+ }
973
+ const validation = validateConnectorConfig(body);
974
+ if (!validation.ok) {
975
+ return { status: 400, body: { error: validation.error.message } };
976
+ }
977
+ const config = validation.value;
978
+ const connectors = await store.getConnectors();
979
+ if (connectors[config.name]) {
980
+ return { status: 409, body: { error: `Connector "${config.name}" already exists` } };
981
+ }
982
+ connectors[config.name] = config;
983
+ await store.setConnectors(connectors);
984
+ return { status: 200, body: { registered: true, name: config.name } };
985
+ }
986
+ async function handleUnregisterConnector(name, store) {
987
+ const connectors = await store.getConnectors();
988
+ if (!connectors[name]) {
989
+ return { status: 404, body: { error: `Connector "${name}" not found` } };
990
+ }
991
+ delete connectors[name];
992
+ await store.setConnectors(connectors);
993
+ return { status: 200, body: { unregistered: true, name } };
994
+ }
995
+ async function handleListConnectors(store) {
996
+ const connectors = await store.getConnectors();
997
+ const list = Object.values(connectors).map((c) => ({
998
+ name: c.name,
999
+ type: c.type,
1000
+ hasIngest: c.ingest !== void 0
1001
+ }));
1002
+ return { status: 200, body: list };
1003
+ }
1004
+ function handleMetrics(gateway, extra) {
1005
+ const stats = gateway.bufferStats;
1006
+ return { status: 200, body: { ...stats, ...extra } };
1007
+ }
1008
+
1009
+ // ../gateway/src/schema-manager.ts
1010
+ var SchemaManager = class {
1011
+ currentSchema;
1012
+ version;
1013
+ allowedColumns;
1014
+ constructor(schema, version) {
1015
+ this.currentSchema = schema;
1016
+ this.version = version ?? 1;
1017
+ this.allowedColumns = new Set(schema.columns.map((c) => c.name));
1018
+ }
1019
+ /** Get the current schema and version. */
1020
+ getSchema() {
1021
+ return { schema: this.currentSchema, version: this.version };
1022
+ }
1023
+ /**
1024
+ * Validate that a delta's columns are compatible with the current schema.
1025
+ *
1026
+ * Unknown columns result in a SchemaError. Missing columns are fine (sparse deltas).
1027
+ * DELETE ops with empty columns are always valid.
1028
+ */
1029
+ validateDelta(delta) {
1030
+ if (delta.op === "DELETE" && delta.columns.length === 0) {
1031
+ return Ok(void 0);
1032
+ }
1033
+ for (const col of delta.columns) {
1034
+ if (!this.allowedColumns.has(col.column)) {
1035
+ return Err(
1036
+ new SchemaError(
1037
+ `Unknown column "${col.column}" in delta for table "${delta.table}". Schema version ${this.version} does not include this column.`
1038
+ )
1039
+ );
1040
+ }
1041
+ }
1042
+ return Ok(void 0);
1043
+ }
1044
+ /**
1045
+ * Evolve the schema by adding new nullable columns.
1046
+ *
1047
+ * Only adding columns is allowed. Removing columns or changing types
1048
+ * returns a SchemaError.
1049
+ */
1050
+ evolveSchema(newSchema) {
1051
+ if (newSchema.table !== this.currentSchema.table) {
1052
+ return Err(new SchemaError("Cannot evolve schema: table name mismatch"));
1053
+ }
1054
+ const oldColumnMap = new Map(this.currentSchema.columns.map((c) => [c.name, c.type]));
1055
+ const newColumnMap = new Map(newSchema.columns.map((c) => [c.name, c.type]));
1056
+ for (const [name] of oldColumnMap) {
1057
+ if (!newColumnMap.has(name)) {
1058
+ return Err(
1059
+ new SchemaError(
1060
+ `Cannot remove column "${name}" \u2014 only adding nullable columns is supported`
1061
+ )
1062
+ );
1063
+ }
1064
+ }
1065
+ for (const [name, oldType] of oldColumnMap) {
1066
+ const newType = newColumnMap.get(name);
1067
+ if (newType && newType !== oldType) {
1068
+ return Err(
1069
+ new SchemaError(
1070
+ `Cannot change type of column "${name}" from "${oldType}" to "${newType}"`
1071
+ )
1072
+ );
1073
+ }
1074
+ }
1075
+ this.currentSchema = newSchema;
1076
+ this.version++;
1077
+ this.allowedColumns = new Set(newSchema.columns.map((c) => c.name));
1078
+ return Ok({ version: this.version });
1079
+ }
1080
+ };
1081
+
1082
+ export {
1083
+ ActionDispatcher,
1084
+ DeltaBuffer,
1085
+ MemoryConfigStore,
1086
+ MAX_PUSH_PAYLOAD_BYTES,
1087
+ MAX_DELTAS_PER_PUSH,
1088
+ MAX_PULL_LIMIT,
1089
+ DEFAULT_PULL_LIMIT,
1090
+ VALID_COLUMN_TYPES,
1091
+ DEFAULT_MAX_BUFFER_BYTES,
1092
+ DEFAULT_MAX_BUFFER_AGE_MS,
1093
+ hlcRange,
1094
+ flushEntries,
1095
+ commitToCatalogue,
1096
+ SyncGateway,
1097
+ validatePushBody,
1098
+ parsePullParams,
1099
+ validateActionBody,
1100
+ validateSchemaBody,
1101
+ pushErrorToStatus,
1102
+ buildSyncRulesContext,
1103
+ handlePushRequest,
1104
+ handlePullRequest,
1105
+ handleActionRequest,
1106
+ handleFlushRequest,
1107
+ handleSaveSchema,
1108
+ handleSaveSyncRules,
1109
+ handleRegisterConnector,
1110
+ handleUnregisterConnector,
1111
+ handleListConnectors,
1112
+ handleMetrics,
1113
+ SchemaManager
1114
+ };
1115
+ //# sourceMappingURL=chunk-5YOFCJQ7.js.map