@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.
- package/dist/adapters/mysql/index.d.ts +7 -0
- package/dist/adapters/mysql/index.js +12 -3
- package/dist/adapters/mysql/index.js.map +4 -4
- package/dist/adapters/postgres/index.d.ts +7 -0
- package/dist/adapters/postgres/index.js +6 -3
- package/dist/adapters/postgres/index.js.map +3 -3
- package/dist/adapters/sqlite/index.js +5 -2
- package/dist/adapters/sqlite/index.js.map +3 -3
- package/dist/engine/cdc.d.ts +74 -0
- package/dist/engine/devtools.d.ts +11 -0
- package/dist/engine/index.d.ts +6 -2
- package/dist/engine/index.js +289 -21
- package/dist/engine/index.js.map +7 -5
- package/dist/engine/pollingSource.d.ts +7 -0
- package/dist/engine/syncEngine.d.ts +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1689 -3
- package/dist/index.js.map +10 -3
- package/package.json +1 -1
package/dist/engine/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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=
|
|
2764
|
+
//# debugId=3FBF66DA7F03006864756E2164756E21
|
|
2497
2765
|
//# sourceMappingURL=index.js.map
|