@absolutejs/sync 1.18.2 → 1.20.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/engine/devtools.d.ts +5 -0
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +83 -3
- package/dist/engine/index.js.map +3 -3
- package/dist/engine/syncEngine.d.ts +115 -0
- package/dist/index.js +82 -3
- package/dist/index.js.map +3 -3
- package/dist/testing.js +82 -3
- package/dist/testing.js.map +3 -3
- package/package.json +1 -1
|
@@ -266,6 +266,36 @@ export type SyncEngine = {
|
|
|
266
266
|
* Added in 1.13.0.
|
|
267
267
|
*/
|
|
268
268
|
metrics: () => EngineMetrics;
|
|
269
|
+
/**
|
|
270
|
+
* Capture the engine's change log + version as a serializable
|
|
271
|
+
* {@link ChangeLogSnapshot} the host can persist (disk, S3, the cluster
|
|
272
|
+
* bus) and restore on the next boot via
|
|
273
|
+
* {@link SyncEngineOptions.initialChangeLog} or
|
|
274
|
+
* {@link SyncEngine.importChangeLog}. The receiving engine MUST share this
|
|
275
|
+
* engine's `instanceId` — otherwise the resume contract silently breaks.
|
|
276
|
+
*
|
|
277
|
+
* Cheap: the snapshot's `entries` is a shallow copy of the bounded log
|
|
278
|
+
* (capped by `changeLogSize` / `changeLogRetainMs`). Call on a timer or on
|
|
279
|
+
* graceful shutdown — both are fine; the snapshot is monotonic in commit
|
|
280
|
+
* order, so a partial roll-forward (apply entries newer than the snapshot
|
|
281
|
+
* from another source) is safe.
|
|
282
|
+
*
|
|
283
|
+
* Added in 1.19.0.
|
|
284
|
+
*/
|
|
285
|
+
exportChangeLog: () => ChangeLogSnapshot;
|
|
286
|
+
/**
|
|
287
|
+
* Adopt a {@link ChangeLogSnapshot} into a running engine that has not yet
|
|
288
|
+
* committed any local changes (its `version` is 0). The snapshot's
|
|
289
|
+
* `instanceId` MUST match this engine's `instanceId`. Throws otherwise.
|
|
290
|
+
*
|
|
291
|
+
* Convenience for hosts that want to set up the engine, register surfaces,
|
|
292
|
+
* AND THEN restore. Equivalent to passing the snapshot via
|
|
293
|
+
* `createSyncEngine({ initialChangeLog })` if you have it at construction
|
|
294
|
+
* time. Returns the number of entries imported.
|
|
295
|
+
*
|
|
296
|
+
* Added in 1.19.0.
|
|
297
|
+
*/
|
|
298
|
+
importChangeLog: (snapshot: ChangeLogSnapshot) => number;
|
|
269
299
|
/**
|
|
270
300
|
* Subscribe to the live engine activity stream (changes, mutation outcomes,
|
|
271
301
|
* subscribe/unsubscribe). Returns an unsubscribe. Powers the devtools feed.
|
|
@@ -384,6 +414,46 @@ export declare class CdcConsumerSlowError extends Error {
|
|
|
384
414
|
readonly lastDeliveredVersion: number;
|
|
385
415
|
constructor(maxBuffer: number, lastDeliveredVersion: number);
|
|
386
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Thrown by `runMutation` / `runMutations` when `mutationConcurrency` is
|
|
419
|
+
* saturated AND the waiting queue is already at `mutationQueueLimit`. The
|
|
420
|
+
* caller sees this immediately (no queue time) so the host can shed load
|
|
421
|
+
* with a clean 429 instead of letting the queue grow unboundedly. Added
|
|
422
|
+
* in 1.20.0.
|
|
423
|
+
*/
|
|
424
|
+
export declare class MutationQueueOverflowError extends Error {
|
|
425
|
+
readonly queueLimit: number;
|
|
426
|
+
constructor(queueLimit: number);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Serializable snapshot of an engine's change log + monotonic version, returned
|
|
430
|
+
* by {@link SyncEngine.exportChangeLog} and consumed by
|
|
431
|
+
* {@link SyncEngineOptions.initialChangeLog} or
|
|
432
|
+
* {@link SyncEngine.importChangeLog}.
|
|
433
|
+
*
|
|
434
|
+
* The PaaS host persists this on shard rotation (every N seconds or on graceful
|
|
435
|
+
* shutdown) and hands it back to the replacement engine so resume cursors
|
|
436
|
+
* referencing this `instanceId` keep working past the restart. Bounded by the
|
|
437
|
+
* receiving engine's `changeLogSize` + `changeLogRetainMs` policies — entries
|
|
438
|
+
* that exceed either cap on import are trimmed exactly as if they had been
|
|
439
|
+
* logged live.
|
|
440
|
+
*
|
|
441
|
+
* Added in 1.19.0.
|
|
442
|
+
*/
|
|
443
|
+
export type ChangeLogSnapshot = {
|
|
444
|
+
/** The exporting engine's `instanceId`. Receiver MUST match. */
|
|
445
|
+
instanceId: string;
|
|
446
|
+
/** The exporting engine's monotonic version at snapshot time. */
|
|
447
|
+
version: number;
|
|
448
|
+
/** Every retained log entry, in commit order (oldest first). */
|
|
449
|
+
entries: ReadonlyArray<LoggedChange>;
|
|
450
|
+
/**
|
|
451
|
+
* Optional version-stamp the host may use to compare snapshots without
|
|
452
|
+
* deserializing the entries (e.g. for incremental persistence). Set to
|
|
453
|
+
* `Date.now()` at export time. Receivers ignore this field.
|
|
454
|
+
*/
|
|
455
|
+
exportedAt?: number;
|
|
456
|
+
};
|
|
387
457
|
export type SyncEngineOptions = {
|
|
388
458
|
/**
|
|
389
459
|
* Stable identifier for this engine instance. Defaults to a per-process
|
|
@@ -482,6 +552,51 @@ export type SyncEngineOptions = {
|
|
|
482
552
|
* @see {@link BridgeFetchConfig}
|
|
483
553
|
*/
|
|
484
554
|
bridgeFetch?: BridgeFetchConfig;
|
|
555
|
+
/**
|
|
556
|
+
* Seed the engine's change log on boot from a prior snapshot — produced by
|
|
557
|
+
* {@link SyncEngine.exportChangeLog} on the previous instance, persisted by
|
|
558
|
+
* the host across a shard reboot, then handed back here. Cursors that
|
|
559
|
+
* referenced this engine's `instanceId` stay resumable past the restart
|
|
560
|
+
* (provided their last-seen point still lives in the retained log).
|
|
561
|
+
*
|
|
562
|
+
* The snapshot's `instanceId` MUST match `options.instanceId` (otherwise
|
|
563
|
+
* `createSyncEngine` throws — a wrong-id restore would silently break the
|
|
564
|
+
* resume contract). Snapshot `version` becomes this engine's local
|
|
565
|
+
* monotonic version; entries are inserted in version order. Subscribers,
|
|
566
|
+
* permissions, schemas, schedules, packs, mutations, and the reactive
|
|
567
|
+
* cache are NOT in the snapshot — re-register them as normal after
|
|
568
|
+
* `createSyncEngine` returns. Added in 1.19.0.
|
|
569
|
+
*/
|
|
570
|
+
initialChangeLog?: ChangeLogSnapshot;
|
|
571
|
+
/**
|
|
572
|
+
* Maximum concurrent in-flight mutations (`runMutation` + `runMutations`).
|
|
573
|
+
* Calls beyond the limit wait in a FIFO queue and run as slots free up;
|
|
574
|
+
* `engine.metrics().mutations.queued` surfaces the queue depth.
|
|
575
|
+
*
|
|
576
|
+
* A single tenant flooding `runMutation` can otherwise drive unbounded
|
|
577
|
+
* memory growth (per-mutation `actions` buffers, retry timers, sandbox
|
|
578
|
+
* invocations queued against the isolate pool). Set this to a value
|
|
579
|
+
* appropriate for the host's tenant tier — e.g. `32` for a free tier,
|
|
580
|
+
* `256` for paid. Without this option the engine is unbounded
|
|
581
|
+
* (matching pre-1.20 behavior).
|
|
582
|
+
*
|
|
583
|
+
* Sandboxed mutations are gated by the same semaphore. If you need
|
|
584
|
+
* finer-grained control (sandbox-only throttling), see
|
|
585
|
+
* `@absolutejs/isolated-jsc`'s pool size — that's the lower layer.
|
|
586
|
+
*
|
|
587
|
+
* Added in 1.20.0.
|
|
588
|
+
*/
|
|
589
|
+
mutationConcurrency?: number;
|
|
590
|
+
/**
|
|
591
|
+
* Cap on the queue of waiting mutations once `mutationConcurrency` is
|
|
592
|
+
* saturated. Calls beyond this cap throw {@link MutationQueueOverflowError}
|
|
593
|
+
* immediately instead of queueing — the host can surface a clean 429 or
|
|
594
|
+
* apply a tenant-specific shed policy. Defaults to unbounded (queue
|
|
595
|
+
* never rejects). Only meaningful when `mutationConcurrency` is set.
|
|
596
|
+
*
|
|
597
|
+
* Added in 1.20.0.
|
|
598
|
+
*/
|
|
599
|
+
mutationQueueLimit?: number;
|
|
485
600
|
};
|
|
486
601
|
/**
|
|
487
602
|
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
package/dist/index.js
CHANGED
|
@@ -1152,6 +1152,15 @@ class CdcConsumerSlowError extends Error {
|
|
|
1152
1152
|
this.lastDeliveredVersion = lastDeliveredVersion;
|
|
1153
1153
|
}
|
|
1154
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
class MutationQueueOverflowError extends Error {
|
|
1157
|
+
queueLimit;
|
|
1158
|
+
constructor(queueLimit) {
|
|
1159
|
+
super(`Mutation queue overflowed (limit ${queueLimit}); the engine is at ` + `its mutationConcurrency cap and the waiting queue is full. ` + `Retry later or shed load at the gateway.`);
|
|
1160
|
+
this.name = "MutationQueueOverflowError";
|
|
1161
|
+
this.queueLimit = queueLimit;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1155
1164
|
var defaultKey = (row) => row.id;
|
|
1156
1165
|
var shallowEqual3 = (a, b) => {
|
|
1157
1166
|
if (a === b) {
|
|
@@ -1268,6 +1277,40 @@ var createSyncEngine = (options = {}) => {
|
|
|
1268
1277
|
let mutationsFailed = 0;
|
|
1269
1278
|
let mutationsRetried = 0;
|
|
1270
1279
|
let mutationsInFlight = 0;
|
|
1280
|
+
const mutationWaiters = [];
|
|
1281
|
+
let mutationsQueued = 0;
|
|
1282
|
+
const acquireMutationSlot = async () => {
|
|
1283
|
+
const limit = options.mutationConcurrency;
|
|
1284
|
+
if (limit === undefined) {
|
|
1285
|
+
mutationsInFlight += 1;
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (mutationsInFlight < limit && mutationWaiters.length === 0) {
|
|
1289
|
+
mutationsInFlight += 1;
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const queueLimit = options.mutationQueueLimit;
|
|
1293
|
+
if (queueLimit !== undefined && mutationsQueued >= queueLimit) {
|
|
1294
|
+
throw new MutationQueueOverflowError(queueLimit);
|
|
1295
|
+
}
|
|
1296
|
+
mutationsQueued += 1;
|
|
1297
|
+
try {
|
|
1298
|
+
await new Promise((resolve) => {
|
|
1299
|
+
mutationWaiters.push(resolve);
|
|
1300
|
+
});
|
|
1301
|
+
} finally {
|
|
1302
|
+
mutationsQueued -= 1;
|
|
1303
|
+
}
|
|
1304
|
+
mutationsInFlight += 1;
|
|
1305
|
+
};
|
|
1306
|
+
const releaseMutationSlot = () => {
|
|
1307
|
+
mutationsInFlight -= 1;
|
|
1308
|
+
if (options.mutationConcurrency === undefined)
|
|
1309
|
+
return;
|
|
1310
|
+
const next = mutationWaiters.shift();
|
|
1311
|
+
if (next !== undefined)
|
|
1312
|
+
next();
|
|
1313
|
+
};
|
|
1271
1314
|
const reactiveCacheMax = options.reactiveCache?.max ?? 256;
|
|
1272
1315
|
const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
|
|
1273
1316
|
const cachedReruns = new Map;
|
|
@@ -1309,6 +1352,31 @@ var createSyncEngine = (options = {}) => {
|
|
|
1309
1352
|
const runInTransaction = options.transaction;
|
|
1310
1353
|
const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
|
|
1311
1354
|
let clusterBus;
|
|
1355
|
+
const importChangeLog = (snapshot) => {
|
|
1356
|
+
if (version !== 0) {
|
|
1357
|
+
throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
|
|
1358
|
+
}
|
|
1359
|
+
if (snapshot.instanceId !== instanceId) {
|
|
1360
|
+
throw new Error(`[sync] importChangeLog: snapshot instanceId "${snapshot.instanceId}" ` + `does not match this engine's instanceId "${instanceId}". ` + `Pass options.instanceId = "${snapshot.instanceId}" to createSyncEngine.`);
|
|
1361
|
+
}
|
|
1362
|
+
version = snapshot.version;
|
|
1363
|
+
for (const entry of snapshot.entries) {
|
|
1364
|
+
changeLog.push(entry);
|
|
1365
|
+
}
|
|
1366
|
+
while (changeLog.length > changeLogSize) {
|
|
1367
|
+
changeLog.shift();
|
|
1368
|
+
}
|
|
1369
|
+
if (changeLogRetainMs !== null && changeLogRetainMs > 0) {
|
|
1370
|
+
const cutoff = Date.now() - changeLogRetainMs;
|
|
1371
|
+
while (changeLog.length > 0 && changeLog[0].at < cutoff) {
|
|
1372
|
+
changeLog.shift();
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return snapshot.entries.length;
|
|
1376
|
+
};
|
|
1377
|
+
if (options.initialChangeLog !== undefined) {
|
|
1378
|
+
importChangeLog(options.initialChangeLog);
|
|
1379
|
+
}
|
|
1312
1380
|
const broadcast = (changes, originVersion) => {
|
|
1313
1381
|
if (clusterBus !== undefined && changes.length > 0) {
|
|
1314
1382
|
clusterBus.publish({ changes, origin: instanceId, originVersion });
|
|
@@ -2270,6 +2338,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2270
2338
|
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2271
2339
|
}
|
|
2272
2340
|
}
|
|
2341
|
+
await acquireMutationSlot();
|
|
2273
2342
|
const sandboxRunner = sandboxRunners.get(name);
|
|
2274
2343
|
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2275
2344
|
const runHandler = async (tx) => {
|
|
@@ -2285,7 +2354,6 @@ var createSyncEngine = (options = {}) => {
|
|
|
2285
2354
|
const startedAt = Date.now();
|
|
2286
2355
|
let lastError;
|
|
2287
2356
|
let attemptsMade = 0;
|
|
2288
|
-
mutationsInFlight += 1;
|
|
2289
2357
|
try {
|
|
2290
2358
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2291
2359
|
attemptsMade = attempt;
|
|
@@ -2338,7 +2406,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2338
2406
|
}
|
|
2339
2407
|
throw lastError;
|
|
2340
2408
|
} finally {
|
|
2341
|
-
|
|
2409
|
+
releaseMutationSlot();
|
|
2342
2410
|
}
|
|
2343
2411
|
},
|
|
2344
2412
|
runMutations: async (specs, ctx) => {
|
|
@@ -2351,6 +2419,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2351
2419
|
}
|
|
2352
2420
|
return { args: spec.args, mutation, name: spec.name };
|
|
2353
2421
|
});
|
|
2422
|
+
await acquireMutationSlot();
|
|
2354
2423
|
const runBatch = async (tx) => {
|
|
2355
2424
|
const results = [];
|
|
2356
2425
|
const accumulated = [];
|
|
@@ -2388,6 +2457,8 @@ var createSyncEngine = (options = {}) => {
|
|
|
2388
2457
|
status: "error"
|
|
2389
2458
|
});
|
|
2390
2459
|
throw error;
|
|
2460
|
+
} finally {
|
|
2461
|
+
releaseMutationSlot();
|
|
2391
2462
|
}
|
|
2392
2463
|
},
|
|
2393
2464
|
registerSchedule: (schedule) => {
|
|
@@ -2578,6 +2649,13 @@ var createSyncEngine = (options = {}) => {
|
|
|
2578
2649
|
}))
|
|
2579
2650
|
};
|
|
2580
2651
|
},
|
|
2652
|
+
exportChangeLog: () => ({
|
|
2653
|
+
entries: changeLog.slice(),
|
|
2654
|
+
exportedAt: Date.now(),
|
|
2655
|
+
instanceId,
|
|
2656
|
+
version
|
|
2657
|
+
}),
|
|
2658
|
+
importChangeLog,
|
|
2581
2659
|
metrics: () => {
|
|
2582
2660
|
const now = Date.now();
|
|
2583
2661
|
const byCollection = {};
|
|
@@ -2600,6 +2678,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2600
2678
|
completed: mutationsCompleted,
|
|
2601
2679
|
failed: mutationsFailed,
|
|
2602
2680
|
inFlight: mutationsInFlight,
|
|
2681
|
+
queued: mutationsQueued,
|
|
2603
2682
|
retried: mutationsRetried
|
|
2604
2683
|
},
|
|
2605
2684
|
reactiveCache: {
|
|
@@ -2993,5 +3072,5 @@ export {
|
|
|
2993
3072
|
createPresenceHub
|
|
2994
3073
|
};
|
|
2995
3074
|
|
|
2996
|
-
//# debugId=
|
|
3075
|
+
//# debugId=9DD57DF563EB9CB564756E2164756E21
|
|
2997
3076
|
//# sourceMappingURL=index.js.map
|