@absolutejs/sync 1.5.0 → 1.6.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.
@@ -569,6 +569,7 @@ var createPollingChangeSource = (options) => {
569
569
  const onError = options.onError ?? ((error) => {
570
570
  console.warn("[sync] polling change source error:", error);
571
571
  });
572
+ const onSkip = options.onSkip;
572
573
  let cursor = options.startSeq ?? 0;
573
574
  let running = false;
574
575
  let timer;
@@ -580,7 +581,9 @@ var createPollingChangeSource = (options) => {
580
581
  const rows = await options.poll(cursor);
581
582
  for (const row of rows) {
582
583
  const parsed = parse(row);
583
- if (parsed !== undefined) {
584
+ if (parsed === undefined) {
585
+ onSkip?.(row, "parse-failed");
586
+ } else {
584
587
  await emit(parsed.table, parsed.change);
585
588
  }
586
589
  if (typeof row.seq === "number" && row.seq > cursor) {
@@ -1161,6 +1164,28 @@ class SchemaError extends Error {
1161
1164
  this.name = "SchemaError";
1162
1165
  }
1163
1166
  }
1167
+
1168
+ class MissedChangesError extends Error {
1169
+ requestedSince;
1170
+ availableSince;
1171
+ constructor(requestedSince, availableSince) {
1172
+ super(`Change log no longer covers version ${requestedSince}; oldest available is ${availableSince}. ` + `Re-bootstrap and resume from ${availableSince}.`);
1173
+ this.name = "MissedChangesError";
1174
+ this.requestedSince = requestedSince;
1175
+ this.availableSince = availableSince;
1176
+ }
1177
+ }
1178
+
1179
+ class CdcConsumerSlowError extends Error {
1180
+ maxBuffer;
1181
+ lastDeliveredVersion;
1182
+ constructor(maxBuffer, lastDeliveredVersion) {
1183
+ super(`CDC stream buffer overflowed (max ${maxBuffer}); consumer fell behind. ` + `Last delivered version: ${lastDeliveredVersion}. Resubscribe with since=${lastDeliveredVersion}.`);
1184
+ this.name = "CdcConsumerSlowError";
1185
+ this.maxBuffer = maxBuffer;
1186
+ this.lastDeliveredVersion = lastDeliveredVersion;
1187
+ }
1188
+ }
1164
1189
  var defaultKey = (row) => row.id;
1165
1190
  var shallowEqual4 = (a, b) => {
1166
1191
  if (a === b) {
@@ -1306,6 +1331,7 @@ var createSyncEngine = (options = {}) => {
1306
1331
  listener(event);
1307
1332
  }
1308
1333
  };
1334
+ const streamSubscribers = new Set;
1309
1335
  const runInTransaction = options.transaction;
1310
1336
  const instanceId = globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
1311
1337
  let clusterBus;
@@ -1677,6 +1703,9 @@ var createSyncEngine = (options = {}) => {
1677
1703
  if (changeLog.length > changeLogSize) {
1678
1704
  changeLog.shift();
1679
1705
  }
1706
+ for (const subscriber of streamSubscribers) {
1707
+ subscriber(entry);
1708
+ }
1680
1709
  };
1681
1710
  const applyChange = async (table, change, shouldBroadcast = true) => {
1682
1711
  version += 1;
@@ -2276,9 +2305,170 @@ var createSyncEngine = (options = {}) => {
2276
2305
  return () => {
2277
2306
  activityListeners.delete(listener);
2278
2307
  };
2308
+ },
2309
+ streamChanges: ({
2310
+ since = 0,
2311
+ signal,
2312
+ maxBuffer = 1e4
2313
+ } = {}) => {
2314
+ const oldest = changeLog[0];
2315
+ if (since > 0 && oldest !== undefined && oldest.version > since + 1) {
2316
+ const err = new MissedChangesError(since, oldest.version);
2317
+ return {
2318
+ [Symbol.asyncIterator]() {
2319
+ return {
2320
+ next: () => Promise.reject(err)
2321
+ };
2322
+ }
2323
+ };
2324
+ }
2325
+ const buffer = [];
2326
+ let waiter = null;
2327
+ let overflow = false;
2328
+ const wake = () => {
2329
+ if (waiter !== null) {
2330
+ const resume = waiter;
2331
+ waiter = null;
2332
+ resume();
2333
+ }
2334
+ };
2335
+ const subscriber = (entry) => {
2336
+ if (buffer.length >= maxBuffer) {
2337
+ overflow = true;
2338
+ wake();
2339
+ return;
2340
+ }
2341
+ buffer.push(entry);
2342
+ wake();
2343
+ };
2344
+ streamSubscribers.add(subscriber);
2345
+ const onAbort = () => wake();
2346
+ signal?.addEventListener("abort", onAbort, { once: true });
2347
+ let lastDelivered = since;
2348
+ return {
2349
+ async* [Symbol.asyncIterator]() {
2350
+ try {
2351
+ const history = [...changeLog];
2352
+ const headVersion = history.length > 0 ? history[history.length - 1].version : since;
2353
+ for (const entry of history) {
2354
+ if (signal?.aborted)
2355
+ return;
2356
+ if (entry.version > since) {
2357
+ lastDelivered = entry.version;
2358
+ yield entry;
2359
+ }
2360
+ }
2361
+ while (!signal?.aborted) {
2362
+ while (buffer.length > 0) {
2363
+ const entry = buffer.shift();
2364
+ if (entry.version > headVersion) {
2365
+ lastDelivered = entry.version;
2366
+ yield entry;
2367
+ }
2368
+ }
2369
+ if (overflow) {
2370
+ throw new CdcConsumerSlowError(maxBuffer, lastDelivered);
2371
+ }
2372
+ if (signal?.aborted)
2373
+ return;
2374
+ await new Promise((resolve) => {
2375
+ waiter = resolve;
2376
+ });
2377
+ }
2378
+ } finally {
2379
+ streamSubscribers.delete(subscriber);
2380
+ signal?.removeEventListener("abort", onAbort);
2381
+ }
2382
+ }
2383
+ };
2279
2384
  }
2280
2385
  };
2281
2386
  };
2387
+ // src/engine/cdc.ts
2388
+ import { Elysia } from "elysia";
2389
+ var parseSince = (query2, lastEventId) => {
2390
+ const raw = query2.since ?? lastEventId ?? "0";
2391
+ const parsed = Number(raw);
2392
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
2393
+ };
2394
+ var encodeEvent = (event, id, data) => {
2395
+ const parts = [];
2396
+ if (id !== null)
2397
+ parts.push(`id: ${id}`);
2398
+ parts.push(`event: ${event}`);
2399
+ parts.push(`data: ${JSON.stringify(data)}`);
2400
+ return `${parts.join(`
2401
+ `)}
2402
+
2403
+ `;
2404
+ };
2405
+ var syncCdc = ({
2406
+ engine,
2407
+ path = "/sync/cdc",
2408
+ heartbeatMs = 25000,
2409
+ maxBuffer = 1e4
2410
+ }) => new Elysia({ name: "@absolutejs/sync/cdc" }).get(path, (context) => {
2411
+ const lastEventId = context.request.headers.get("last-event-id");
2412
+ const since = parseSince(context.query, lastEventId);
2413
+ const encoder = new TextEncoder;
2414
+ const stream = new ReadableStream({
2415
+ async start(controller) {
2416
+ const write = (chunk) => {
2417
+ try {
2418
+ controller.enqueue(encoder.encode(chunk));
2419
+ } catch {}
2420
+ };
2421
+ write(encodeEvent("open", null, {
2422
+ since,
2423
+ at: Date.now()
2424
+ }));
2425
+ const heartbeat = setInterval(() => write(`: ping
2426
+
2427
+ `), heartbeatMs);
2428
+ try {
2429
+ for await (const entry of engine.streamChanges({
2430
+ since,
2431
+ signal: context.request.signal,
2432
+ maxBuffer
2433
+ })) {
2434
+ write(encodeEvent("change", entry.version, entry));
2435
+ }
2436
+ } catch (error) {
2437
+ if (error instanceof MissedChangesError) {
2438
+ write(encodeEvent("error", null, {
2439
+ name: "MissedChangesError",
2440
+ message: error.message,
2441
+ requestedSince: error.requestedSince,
2442
+ availableSince: error.availableSince
2443
+ }));
2444
+ } else if (error instanceof CdcConsumerSlowError) {
2445
+ write(encodeEvent("error", null, {
2446
+ name: "CdcConsumerSlowError",
2447
+ message: error.message,
2448
+ lastDeliveredVersion: error.lastDeliveredVersion
2449
+ }));
2450
+ } else {
2451
+ write(encodeEvent("error", null, {
2452
+ name: error instanceof Error ? error.name : "Error",
2453
+ message: error instanceof Error ? error.message : String(error)
2454
+ }));
2455
+ }
2456
+ } finally {
2457
+ clearInterval(heartbeat);
2458
+ try {
2459
+ controller.close();
2460
+ } catch {}
2461
+ }
2462
+ }
2463
+ });
2464
+ return new Response(stream, {
2465
+ headers: {
2466
+ "cache-control": "no-cache, no-transform",
2467
+ connection: "keep-alive",
2468
+ "content-type": "text/event-stream"
2469
+ }
2470
+ });
2471
+ });
2282
2472
  // src/engine/schema.ts
2283
2473
  var defineSchema = (schemas) => schemas;
2284
2474
  var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
@@ -2527,6 +2717,7 @@ var createSyncConnection = ({
2527
2717
  return { handle, close };
2528
2718
  };
2529
2719
  export {
2720
+ syncCdc,
2530
2721
  query,
2531
2722
  parseOutboxRow,
2532
2723
  orderByOp,
@@ -2565,8 +2756,10 @@ export {
2565
2756
  UnauthorizedError,
2566
2757
  SchemaError,
2567
2758
  SEARCH_SCORE_FIELD,
2568
- RetriesExhaustedError
2759
+ RetriesExhaustedError,
2760
+ MissedChangesError,
2761
+ CdcConsumerSlowError
2569
2762
  };
2570
2763
 
2571
- //# debugId=000A0D740DBD7CBA64756E2164756E21
2764
+ //# debugId=3FBF66DA7F03006864756E2164756E21
2572
2765
  //# sourceMappingURL=index.js.map