@absolutejs/sync 1.19.0 → 1.20.1
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 +13 -0
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +181 -74
- package/dist/engine/index.js.map +3 -3
- package/dist/engine/syncEngine.d.ts +76 -0
- package/dist/index.js +179 -74
- package/dist/index.js.map +3 -3
- package/dist/testing.js +179 -74
- package/dist/testing.js.map +3 -3
- package/package.json +1 -1
|
@@ -82,6 +82,14 @@ export type EngineMetrics = {
|
|
|
82
82
|
total: number;
|
|
83
83
|
/** Per-collection breakdown — the values sum to `total`. */
|
|
84
84
|
byCollection: Record<string, number>;
|
|
85
|
+
/**
|
|
86
|
+
* Per-tenant tally — populated only when
|
|
87
|
+
* `SyncEngineOptions.subscriptionLimit` is set; otherwise `{}`. The
|
|
88
|
+
* key is whatever `subscriptionLimit.key(ctx, args)` returns; the
|
|
89
|
+
* value is the count of active subscriptions for that key. Added
|
|
90
|
+
* in 1.20.1.
|
|
91
|
+
*/
|
|
92
|
+
byTenant: Record<string, number>;
|
|
85
93
|
};
|
|
86
94
|
reactiveCache: {
|
|
87
95
|
entries: number;
|
|
@@ -96,6 +104,11 @@ export type EngineMetrics = {
|
|
|
96
104
|
retried: number;
|
|
97
105
|
/** Currently running, not yet committed or failed. */
|
|
98
106
|
inFlight: number;
|
|
107
|
+
/**
|
|
108
|
+
* Waiting in the `mutationConcurrency` queue. Always `0` when
|
|
109
|
+
* `mutationConcurrency` is unset (no semaphore). Added in 1.20.0.
|
|
110
|
+
*/
|
|
111
|
+
queued: number;
|
|
99
112
|
};
|
|
100
113
|
schedules: {
|
|
101
114
|
registered: number;
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -47,8 +47,8 @@ export type { MutationActions, MutationDefinition, MutationHandler, TableWriter,
|
|
|
47
47
|
export type { BridgeFetchConfig, BridgeFetchEndpoint, BridgeFetchResponse, HandlerMetricsHook, HandlerMetricsRecord, SandboxConfig } from './sandbox';
|
|
48
48
|
export { exponentialBackoff, isSerializationFailure, RetriesExhaustedError } from './retry';
|
|
49
49
|
export type { ExponentialBackoffOptions, RetryPolicy } from './retry';
|
|
50
|
-
export { CdcConsumerSlowError, createSyncEngine, MissedChangesError, SchemaError, UnauthorizedError } from './syncEngine';
|
|
51
|
-
export type { CrdtFields, LoggedChange, StreamChangesOptions, SubscribeArgs, Subscription, SyncEngine } from './syncEngine';
|
|
50
|
+
export { CdcConsumerSlowError, createSyncEngine, MissedChangesError, MutationQueueOverflowError, SchemaError, SubscriptionLimitError, UnauthorizedError } from './syncEngine';
|
|
51
|
+
export type { ChangeLogSnapshot, CrdtFields, LoggedChange, StreamChangesOptions, SubscribeArgs, Subscription, SyncEngine, SyncEngineOptions } from './syncEngine';
|
|
52
52
|
export { syncCdc } from './cdc';
|
|
53
53
|
export type { SyncCdcOptions } from './cdc';
|
|
54
54
|
export type { CrdtMergeable } from '../crdt';
|
package/dist/engine/index.js
CHANGED
|
@@ -1410,6 +1410,28 @@ class CdcConsumerSlowError extends Error {
|
|
|
1410
1410
|
this.lastDeliveredVersion = lastDeliveredVersion;
|
|
1411
1411
|
}
|
|
1412
1412
|
}
|
|
1413
|
+
|
|
1414
|
+
class MutationQueueOverflowError extends Error {
|
|
1415
|
+
queueLimit;
|
|
1416
|
+
constructor(queueLimit) {
|
|
1417
|
+
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.`);
|
|
1418
|
+
this.name = "MutationQueueOverflowError";
|
|
1419
|
+
this.queueLimit = queueLimit;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
class SubscriptionLimitError extends Error {
|
|
1424
|
+
tenantKey;
|
|
1425
|
+
limit;
|
|
1426
|
+
active;
|
|
1427
|
+
constructor(tenantKey, limit, active) {
|
|
1428
|
+
super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
|
|
1429
|
+
this.name = "SubscriptionLimitError";
|
|
1430
|
+
this.tenantKey = tenantKey;
|
|
1431
|
+
this.limit = limit;
|
|
1432
|
+
this.active = active;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1413
1435
|
var defaultKey = (row) => row.id;
|
|
1414
1436
|
var shallowEqual4 = (a, b) => {
|
|
1415
1437
|
if (a === b) {
|
|
@@ -1526,6 +1548,65 @@ var createSyncEngine = (options = {}) => {
|
|
|
1526
1548
|
let mutationsFailed = 0;
|
|
1527
1549
|
let mutationsRetried = 0;
|
|
1528
1550
|
let mutationsInFlight = 0;
|
|
1551
|
+
const mutationWaiters = [];
|
|
1552
|
+
let mutationsQueued = 0;
|
|
1553
|
+
const acquireMutationSlot = async () => {
|
|
1554
|
+
const limit = options.mutationConcurrency;
|
|
1555
|
+
if (limit === undefined) {
|
|
1556
|
+
mutationsInFlight += 1;
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (mutationsInFlight < limit && mutationWaiters.length === 0) {
|
|
1560
|
+
mutationsInFlight += 1;
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const queueLimit = options.mutationQueueLimit;
|
|
1564
|
+
if (queueLimit !== undefined && mutationsQueued >= queueLimit) {
|
|
1565
|
+
throw new MutationQueueOverflowError(queueLimit);
|
|
1566
|
+
}
|
|
1567
|
+
mutationsQueued += 1;
|
|
1568
|
+
try {
|
|
1569
|
+
await new Promise((resolve) => {
|
|
1570
|
+
mutationWaiters.push(resolve);
|
|
1571
|
+
});
|
|
1572
|
+
} finally {
|
|
1573
|
+
mutationsQueued -= 1;
|
|
1574
|
+
}
|
|
1575
|
+
mutationsInFlight += 1;
|
|
1576
|
+
};
|
|
1577
|
+
const releaseMutationSlot = () => {
|
|
1578
|
+
mutationsInFlight -= 1;
|
|
1579
|
+
if (options.mutationConcurrency === undefined)
|
|
1580
|
+
return;
|
|
1581
|
+
const next = mutationWaiters.shift();
|
|
1582
|
+
if (next !== undefined)
|
|
1583
|
+
next();
|
|
1584
|
+
};
|
|
1585
|
+
const subscriptionsByTenant = new Map;
|
|
1586
|
+
const acquireSubscriptionSlot = (ctx, args) => {
|
|
1587
|
+
const cap = options.subscriptionLimit;
|
|
1588
|
+
if (cap === undefined)
|
|
1589
|
+
return;
|
|
1590
|
+
const tenantKey = cap.key(ctx, args);
|
|
1591
|
+
if (tenantKey === undefined)
|
|
1592
|
+
return;
|
|
1593
|
+
const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
|
|
1594
|
+
if (active2 >= cap.max) {
|
|
1595
|
+
throw new SubscriptionLimitError(tenantKey, cap.max, active2);
|
|
1596
|
+
}
|
|
1597
|
+
subscriptionsByTenant.set(tenantKey, active2 + 1);
|
|
1598
|
+
return tenantKey;
|
|
1599
|
+
};
|
|
1600
|
+
const releaseSubscriptionSlot = (tenantKey) => {
|
|
1601
|
+
if (tenantKey === undefined)
|
|
1602
|
+
return;
|
|
1603
|
+
const active2 = subscriptionsByTenant.get(tenantKey);
|
|
1604
|
+
if (active2 === undefined || active2 <= 1) {
|
|
1605
|
+
subscriptionsByTenant.delete(tenantKey);
|
|
1606
|
+
} else {
|
|
1607
|
+
subscriptionsByTenant.set(tenantKey, active2 - 1);
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1529
1610
|
const reactiveCacheMax = options.reactiveCache?.max ?? 256;
|
|
1530
1611
|
const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
|
|
1531
1612
|
const cachedReruns = new Map;
|
|
@@ -2363,84 +2444,103 @@ var createSyncEngine = (options = {}) => {
|
|
|
2363
2444
|
if (registered === undefined) {
|
|
2364
2445
|
throw new Error(`Unknown collection "${collection}"`);
|
|
2365
2446
|
}
|
|
2366
|
-
const
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
const
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2447
|
+
const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
|
|
2448
|
+
let slotHandedOff = false;
|
|
2449
|
+
try {
|
|
2450
|
+
const typedOnDiff = onDiff;
|
|
2451
|
+
const subscribeSet = subsFor(collection);
|
|
2452
|
+
const wrapReturn = (sub) => {
|
|
2453
|
+
checkAborted(signal);
|
|
2454
|
+
const innerUnsubscribe = sub.unsubscribe;
|
|
2455
|
+
let released = false;
|
|
2456
|
+
const wrappedUnsubscribe = () => {
|
|
2457
|
+
if (released)
|
|
2458
|
+
return;
|
|
2459
|
+
released = true;
|
|
2460
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2461
|
+
innerUnsubscribe();
|
|
2462
|
+
};
|
|
2463
|
+
const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
|
|
2464
|
+
linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
|
|
2465
|
+
slotHandedOff = true;
|
|
2466
|
+
return wrapped;
|
|
2467
|
+
};
|
|
2468
|
+
const registeredKind = registered.kind;
|
|
2469
|
+
if (registeredKind === "join") {
|
|
2470
|
+
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2471
|
+
return wrapReturn(joined);
|
|
2472
|
+
}
|
|
2473
|
+
if (registeredKind === "graph") {
|
|
2474
|
+
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2475
|
+
return wrapReturn(graphed);
|
|
2476
|
+
}
|
|
2477
|
+
if (registeredKind === "reactive") {
|
|
2478
|
+
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2479
|
+
return wrapReturn(reactived);
|
|
2480
|
+
}
|
|
2481
|
+
if (registeredKind === "search") {
|
|
2482
|
+
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2483
|
+
return wrapReturn(searched);
|
|
2484
|
+
}
|
|
2485
|
+
const definition = registered;
|
|
2486
|
+
if (definition.authorize !== undefined) {
|
|
2487
|
+
const allowed = await definition.authorize(params, ctx);
|
|
2488
|
+
if (!allowed) {
|
|
2489
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
const key = definition.key ?? defaultKey;
|
|
2493
|
+
const match = definition.match;
|
|
2494
|
+
const tables = definition.tables ?? [collection];
|
|
2495
|
+
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2496
|
+
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2497
|
+
const rehydrate = async () => {
|
|
2498
|
+
const raw = [...await definition.hydrate(params, ctx)];
|
|
2499
|
+
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2500
|
+
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2501
|
+
};
|
|
2502
|
+
const incremental = match !== undefined && tables.length === 1;
|
|
2503
|
+
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2504
|
+
const view = createMaterializedView({
|
|
2505
|
+
key,
|
|
2506
|
+
match: boundMatch
|
|
2507
|
+
});
|
|
2508
|
+
const resuming = since !== undefined && canResume(since, incremental);
|
|
2509
|
+
view.hydrate([...await rehydrate()]);
|
|
2510
|
+
const atVersion = version;
|
|
2511
|
+
const subscription = {
|
|
2512
|
+
kind: "view",
|
|
2513
|
+
collection,
|
|
2514
|
+
view,
|
|
2515
|
+
incremental,
|
|
2516
|
+
rehydrate,
|
|
2517
|
+
key,
|
|
2518
|
+
onDiff: typedOnDiff
|
|
2519
|
+
};
|
|
2520
|
+
subscribeSet.add(subscription);
|
|
2521
|
+
const unsubscribe = () => {
|
|
2522
|
+
subscribeSet.delete(subscription);
|
|
2523
|
+
};
|
|
2524
|
+
if (resuming) {
|
|
2525
|
+
return wrapReturn({
|
|
2526
|
+
initial: [],
|
|
2527
|
+
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2528
|
+
cursor: currentCursor(),
|
|
2529
|
+
version: atVersion,
|
|
2530
|
+
unsubscribe
|
|
2531
|
+
});
|
|
2395
2532
|
}
|
|
2396
|
-
}
|
|
2397
|
-
const key = definition.key ?? defaultKey;
|
|
2398
|
-
const match = definition.match;
|
|
2399
|
-
const tables = definition.tables ?? [collection];
|
|
2400
|
-
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2401
|
-
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2402
|
-
const rehydrate = async () => {
|
|
2403
|
-
const raw = [...await definition.hydrate(params, ctx)];
|
|
2404
|
-
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2405
|
-
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2406
|
-
};
|
|
2407
|
-
const incremental = match !== undefined && tables.length === 1;
|
|
2408
|
-
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2409
|
-
const view = createMaterializedView({
|
|
2410
|
-
key,
|
|
2411
|
-
match: boundMatch
|
|
2412
|
-
});
|
|
2413
|
-
const resuming = since !== undefined && canResume(since, incremental);
|
|
2414
|
-
view.hydrate([...await rehydrate()]);
|
|
2415
|
-
const atVersion = version;
|
|
2416
|
-
const subscription = {
|
|
2417
|
-
kind: "view",
|
|
2418
|
-
collection,
|
|
2419
|
-
view,
|
|
2420
|
-
incremental,
|
|
2421
|
-
rehydrate,
|
|
2422
|
-
key,
|
|
2423
|
-
onDiff: typedOnDiff
|
|
2424
|
-
};
|
|
2425
|
-
subscribeSet.add(subscription);
|
|
2426
|
-
const unsubscribe = () => {
|
|
2427
|
-
subscribeSet.delete(subscription);
|
|
2428
|
-
};
|
|
2429
|
-
if (resuming) {
|
|
2430
2533
|
return wrapReturn({
|
|
2431
|
-
initial:
|
|
2432
|
-
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2534
|
+
initial: view.rows(),
|
|
2433
2535
|
cursor: currentCursor(),
|
|
2434
2536
|
version: atVersion,
|
|
2435
2537
|
unsubscribe
|
|
2436
2538
|
});
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
if (!slotHandedOff)
|
|
2541
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2542
|
+
throw error;
|
|
2437
2543
|
}
|
|
2438
|
-
return wrapReturn({
|
|
2439
|
-
initial: view.rows(),
|
|
2440
|
-
cursor: currentCursor(),
|
|
2441
|
-
version: atVersion,
|
|
2442
|
-
unsubscribe
|
|
2443
|
-
});
|
|
2444
2544
|
},
|
|
2445
2545
|
hydrate: async (collection, params, ctx, options2) => {
|
|
2446
2546
|
const signal = options2?.signal;
|
|
@@ -2553,6 +2653,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2553
2653
|
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2554
2654
|
}
|
|
2555
2655
|
}
|
|
2656
|
+
await acquireMutationSlot();
|
|
2556
2657
|
const sandboxRunner = sandboxRunners.get(name);
|
|
2557
2658
|
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2558
2659
|
const runHandler = async (tx) => {
|
|
@@ -2568,7 +2669,6 @@ var createSyncEngine = (options = {}) => {
|
|
|
2568
2669
|
const startedAt = Date.now();
|
|
2569
2670
|
let lastError;
|
|
2570
2671
|
let attemptsMade = 0;
|
|
2571
|
-
mutationsInFlight += 1;
|
|
2572
2672
|
try {
|
|
2573
2673
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2574
2674
|
attemptsMade = attempt;
|
|
@@ -2621,7 +2721,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2621
2721
|
}
|
|
2622
2722
|
throw lastError;
|
|
2623
2723
|
} finally {
|
|
2624
|
-
|
|
2724
|
+
releaseMutationSlot();
|
|
2625
2725
|
}
|
|
2626
2726
|
},
|
|
2627
2727
|
runMutations: async (specs, ctx) => {
|
|
@@ -2634,6 +2734,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2634
2734
|
}
|
|
2635
2735
|
return { args: spec.args, mutation, name: spec.name };
|
|
2636
2736
|
});
|
|
2737
|
+
await acquireMutationSlot();
|
|
2637
2738
|
const runBatch = async (tx) => {
|
|
2638
2739
|
const results = [];
|
|
2639
2740
|
const accumulated = [];
|
|
@@ -2671,6 +2772,8 @@ var createSyncEngine = (options = {}) => {
|
|
|
2671
2772
|
status: "error"
|
|
2672
2773
|
});
|
|
2673
2774
|
throw error;
|
|
2775
|
+
} finally {
|
|
2776
|
+
releaseMutationSlot();
|
|
2674
2777
|
}
|
|
2675
2778
|
},
|
|
2676
2779
|
registerSchedule: (schedule) => {
|
|
@@ -2890,6 +2993,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2890
2993
|
completed: mutationsCompleted,
|
|
2891
2994
|
failed: mutationsFailed,
|
|
2892
2995
|
inFlight: mutationsInFlight,
|
|
2996
|
+
queued: mutationsQueued,
|
|
2893
2997
|
retried: mutationsRetried
|
|
2894
2998
|
},
|
|
2895
2999
|
reactiveCache: {
|
|
@@ -2901,6 +3005,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2901
3005
|
},
|
|
2902
3006
|
subscriptions: {
|
|
2903
3007
|
byCollection,
|
|
3008
|
+
byTenant: Object.fromEntries(subscriptionsByTenant),
|
|
2904
3009
|
total: totalSubscriptions
|
|
2905
3010
|
},
|
|
2906
3011
|
uptimeMs: now - engineStartedAt,
|
|
@@ -3429,14 +3534,16 @@ export {
|
|
|
3429
3534
|
chain,
|
|
3430
3535
|
aggregateOp,
|
|
3431
3536
|
UnauthorizedError,
|
|
3537
|
+
SubscriptionLimitError,
|
|
3432
3538
|
SchemaError,
|
|
3433
3539
|
SEARCH_SCORE_FIELD,
|
|
3434
3540
|
RetriesExhaustedError,
|
|
3435
3541
|
PackTableConflictError,
|
|
3436
3542
|
PackMissingDependencyError,
|
|
3543
|
+
MutationQueueOverflowError,
|
|
3437
3544
|
MissedChangesError,
|
|
3438
3545
|
CdcConsumerSlowError
|
|
3439
3546
|
};
|
|
3440
3547
|
|
|
3441
|
-
//# debugId=
|
|
3548
|
+
//# debugId=251EC576E67A0CAF64756E2164756E21
|
|
3442
3549
|
//# sourceMappingURL=index.js.map
|