@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.
- 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/index.d.ts +4 -2
- package/dist/engine/index.js +197 -3
- package/dist/engine/index.js.map +7 -6
- package/dist/engine/pollingSource.d.ts +7 -0
- package/dist/engine/sandbox.d.ts +36 -6
- package/dist/engine/syncEngine.d.ts +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1690 -3
- package/dist/index.js.map +10 -3
- package/package.json +3 -3
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) {
|
|
@@ -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=
|
|
2765
|
+
//# debugId=EE73D3886B04BDA964756E2164756E21
|
|
2572
2766
|
//# sourceMappingURL=index.js.map
|