@absolutejs/sync 1.4.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) {
@@ -1041,6 +1044,42 @@ var createVectorIndex = (options) => {
1041
1044
  var defineSchedule = (definition) => definition;
1042
1045
  // src/engine/mutation.ts
1043
1046
  var defineMutation = (definition) => definition;
1047
+ // src/engine/retry.ts
1048
+ var PG_RETRY_CODES = new Set(["40001", "40P01"]);
1049
+ var isSerializationFailure = (error) => {
1050
+ if (error === null || typeof error !== "object")
1051
+ return false;
1052
+ const code = error.code;
1053
+ if (typeof code === "string" && PG_RETRY_CODES.has(code))
1054
+ return true;
1055
+ const cause = error.cause;
1056
+ if (cause !== undefined)
1057
+ return isSerializationFailure(cause);
1058
+ return false;
1059
+ };
1060
+ var exponentialBackoff = (options = {}) => (attempt) => {
1061
+ const base = options.baseMs ?? 25;
1062
+ const factor = options.factor ?? 2;
1063
+ const max = options.maxMs ?? 1000;
1064
+ const jitter = options.jitter ?? 0.2;
1065
+ const raw = Math.min(max, base * factor ** Math.max(0, attempt - 1));
1066
+ const spread = raw * jitter;
1067
+ return raw + (Math.random() * 2 - 1) * spread;
1068
+ };
1069
+
1070
+ class RetriesExhaustedError extends Error {
1071
+ attempts;
1072
+ elapsedMs;
1073
+ cause;
1074
+ constructor(attempts, elapsedMs, cause) {
1075
+ const message = cause instanceof Error ? cause.message : String(cause);
1076
+ super(`retries exhausted after ${attempts} attempts (${elapsedMs}ms): ${message}`);
1077
+ this.name = "RetriesExhaustedError";
1078
+ this.attempts = attempts;
1079
+ this.elapsedMs = elapsedMs;
1080
+ this.cause = cause;
1081
+ }
1082
+ }
1044
1083
  // src/engine/sandbox.ts
1045
1084
  var isolatedJscModule;
1046
1085
  var loadIsolatedJsc = async () => {
@@ -1125,6 +1164,28 @@ class SchemaError extends Error {
1125
1164
  this.name = "SchemaError";
1126
1165
  }
1127
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
+ }
1128
1189
  var defaultKey = (row) => row.id;
1129
1190
  var shallowEqual4 = (a, b) => {
1130
1191
  if (a === b) {
@@ -1270,6 +1331,7 @@ var createSyncEngine = (options = {}) => {
1270
1331
  listener(event);
1271
1332
  }
1272
1333
  };
1334
+ const streamSubscribers = new Set;
1273
1335
  const runInTransaction = options.transaction;
1274
1336
  const instanceId = globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
1275
1337
  let clusterBus;
@@ -1641,6 +1703,9 @@ var createSyncEngine = (options = {}) => {
1641
1703
  if (changeLog.length > changeLogSize) {
1642
1704
  changeLog.shift();
1643
1705
  }
1706
+ for (const subscriber of streamSubscribers) {
1707
+ subscriber(entry);
1708
+ }
1644
1709
  };
1645
1710
  const applyChange = async (table, change, shouldBroadcast = true) => {
1646
1711
  version += 1;
@@ -2120,25 +2185,61 @@ var createSyncEngine = (options = {}) => {
2120
2185
  const result = await invokeHandler(args, ctx, actions);
2121
2186
  return { buffered, result };
2122
2187
  };
2123
- try {
2124
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2125
- await applyChangeBatch(buffered);
2126
- emitActivity({
2127
- type: "mutation",
2128
- at: Date.now(),
2129
- name,
2130
- status: "ok"
2131
- });
2132
- return result;
2133
- } catch (error) {
2134
- emitActivity({
2135
- type: "mutation",
2136
- at: Date.now(),
2137
- name,
2138
- status: "error"
2139
- });
2140
- throw error;
2188
+ const retry = mutation.retry;
2189
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
2190
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
2191
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
2192
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
2193
+ const startedAt = Date.now();
2194
+ let lastError;
2195
+ let attemptsMade = 0;
2196
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2197
+ attemptsMade = attempt;
2198
+ try {
2199
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2200
+ await applyChangeBatch(buffered);
2201
+ emitActivity({
2202
+ type: "mutation",
2203
+ at: Date.now(),
2204
+ name,
2205
+ status: "ok"
2206
+ });
2207
+ return result;
2208
+ } catch (error) {
2209
+ lastError = error;
2210
+ const elapsedMs = Date.now() - startedAt;
2211
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2212
+ if (!canRetry)
2213
+ break;
2214
+ const rawDelay = computeDelay(attempt);
2215
+ const remaining = maxElapsedMs - elapsedMs;
2216
+ if (remaining <= 0)
2217
+ break;
2218
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2219
+ emitActivity({
2220
+ type: "mutationRetry",
2221
+ at: Date.now(),
2222
+ name,
2223
+ attempt,
2224
+ delayMs,
2225
+ errorName: error instanceof Error ? error.name : "Error",
2226
+ errorMessage: error instanceof Error ? error.message : String(error)
2227
+ });
2228
+ if (delayMs > 0) {
2229
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2230
+ }
2231
+ }
2232
+ }
2233
+ emitActivity({
2234
+ type: "mutation",
2235
+ at: Date.now(),
2236
+ name,
2237
+ status: "error"
2238
+ });
2239
+ if (attemptsMade > 1) {
2240
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2141
2241
  }
2242
+ throw lastError;
2142
2243
  },
2143
2244
  registerSchedule: (schedule) => {
2144
2245
  schedules.set(schedule.name, schedule);
@@ -2204,9 +2305,170 @@ var createSyncEngine = (options = {}) => {
2204
2305
  return () => {
2205
2306
  activityListeners.delete(listener);
2206
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
+ };
2207
2384
  }
2208
2385
  };
2209
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
+ });
2210
2472
  // src/engine/schema.ts
2211
2473
  var defineSchema = (schemas) => schemas;
2212
2474
  var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
@@ -2455,6 +2717,7 @@ var createSyncConnection = ({
2455
2717
  return { handle, close };
2456
2718
  };
2457
2719
  export {
2720
+ syncCdc,
2458
2721
  query,
2459
2722
  parseOutboxRow,
2460
2723
  orderByOp,
@@ -2462,11 +2725,13 @@ export {
2462
2725
  materialize,
2463
2726
  mapOp,
2464
2727
  joinNode,
2728
+ isSerializationFailure,
2465
2729
  isEmptyViewDiff,
2466
2730
  hydrateRoute,
2467
2731
  fromRowChange,
2468
2732
  filterOp,
2469
2733
  field,
2734
+ exponentialBackoff,
2470
2735
  defineSearchCollection,
2471
2736
  defineSchema,
2472
2737
  defineSchedule,
@@ -2490,8 +2755,11 @@ export {
2490
2755
  aggregateOp,
2491
2756
  UnauthorizedError,
2492
2757
  SchemaError,
2493
- SEARCH_SCORE_FIELD
2758
+ SEARCH_SCORE_FIELD,
2759
+ RetriesExhaustedError,
2760
+ MissedChangesError,
2761
+ CdcConsumerSlowError
2494
2762
  };
2495
2763
 
2496
- //# debugId=03508BC44A5CF91064756E2164756E21
2764
+ //# debugId=3FBF66DA7F03006864756E2164756E21
2497
2765
  //# sourceMappingURL=index.js.map