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