@absolutejs/sync 1.20.0 → 1.21.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 +8 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +299 -152
- package/dist/engine/index.js.map +5 -4
- package/dist/engine/syncEngine.d.ts +58 -0
- package/dist/index.js +298 -152
- package/dist/index.js.map +5 -4
- package/dist/testing.js +298 -152
- package/dist/testing.js.map +5 -4
- package/package.json +5 -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;
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ 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, MutationQueueOverflowError, SchemaError, UnauthorizedError } from './syncEngine';
|
|
50
|
+
export { CdcConsumerSlowError, createSyncEngine, MissedChangesError, MutationQueueOverflowError, SchemaError, SubscriptionLimitError, UnauthorizedError } from './syncEngine';
|
|
51
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';
|
package/dist/engine/index.js
CHANGED
|
@@ -1119,6 +1119,59 @@ class RetriesExhaustedError extends Error {
|
|
|
1119
1119
|
this.cause = cause;
|
|
1120
1120
|
}
|
|
1121
1121
|
}
|
|
1122
|
+
// node_modules/@absolutejs/telemetry/dist/index.js
|
|
1123
|
+
var NOOP_SPAN_CONTEXT = {
|
|
1124
|
+
spanId: "0000000000000000",
|
|
1125
|
+
traceFlags: 0,
|
|
1126
|
+
traceId: "00000000000000000000000000000000"
|
|
1127
|
+
};
|
|
1128
|
+
var noopSpan = {
|
|
1129
|
+
addEvent: () => noopSpan,
|
|
1130
|
+
end: () => {},
|
|
1131
|
+
isRecording: () => false,
|
|
1132
|
+
recordException: () => {},
|
|
1133
|
+
setAttribute: () => noopSpan,
|
|
1134
|
+
setAttributes: () => noopSpan,
|
|
1135
|
+
setStatus: () => noopSpan,
|
|
1136
|
+
spanContext: () => NOOP_SPAN_CONTEXT,
|
|
1137
|
+
updateName: () => noopSpan
|
|
1138
|
+
};
|
|
1139
|
+
var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
|
|
1140
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
1141
|
+
return fn(noopSpan);
|
|
1142
|
+
};
|
|
1143
|
+
var noopTracer = {
|
|
1144
|
+
startActiveSpan: startActiveSpanNoop,
|
|
1145
|
+
startSpan: () => noopSpan
|
|
1146
|
+
};
|
|
1147
|
+
var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
|
|
1148
|
+
var ABS_ATTRS = {
|
|
1149
|
+
tenant: "abs.tenant",
|
|
1150
|
+
shardId: "abs.shard.id",
|
|
1151
|
+
engineId: "abs.engine.id",
|
|
1152
|
+
collection: "abs.collection",
|
|
1153
|
+
mutation: "abs.mutation",
|
|
1154
|
+
mutationAttempt: "abs.mutation.attempt",
|
|
1155
|
+
subscriptionId: "abs.subscription.id",
|
|
1156
|
+
batchSize: "abs.batch.size",
|
|
1157
|
+
clusterMessageOrigin: "abs.cluster.origin",
|
|
1158
|
+
jobId: "abs.job.id",
|
|
1159
|
+
jobKind: "abs.job.kind",
|
|
1160
|
+
jobAttempt: "abs.job.attempt",
|
|
1161
|
+
jobMaxAttempts: "abs.job.max_attempts",
|
|
1162
|
+
workerId: "abs.worker.id",
|
|
1163
|
+
runtimeKey: "abs.runtime.key",
|
|
1164
|
+
runtimePid: "abs.runtime.pid",
|
|
1165
|
+
runtimePort: "abs.runtime.port",
|
|
1166
|
+
runtimeExitReason: "abs.runtime.exit_reason",
|
|
1167
|
+
runtimeReadinessMs: "abs.runtime.readiness_ms",
|
|
1168
|
+
routeShard: "abs.route.shard",
|
|
1169
|
+
routeDecision: "abs.route.decision",
|
|
1170
|
+
secretName: "abs.secret.name",
|
|
1171
|
+
secretFingerprint: "abs.secret.fingerprint",
|
|
1172
|
+
auditKind: "abs.audit.kind"
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1122
1175
|
// src/engine/sandbox.ts
|
|
1123
1176
|
var isolatedJscModule;
|
|
1124
1177
|
var loadIsolatedJsc = async () => {
|
|
@@ -1419,6 +1472,19 @@ class MutationQueueOverflowError extends Error {
|
|
|
1419
1472
|
this.queueLimit = queueLimit;
|
|
1420
1473
|
}
|
|
1421
1474
|
}
|
|
1475
|
+
|
|
1476
|
+
class SubscriptionLimitError extends Error {
|
|
1477
|
+
tenantKey;
|
|
1478
|
+
limit;
|
|
1479
|
+
active;
|
|
1480
|
+
constructor(tenantKey, limit, active) {
|
|
1481
|
+
super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
|
|
1482
|
+
this.name = "SubscriptionLimitError";
|
|
1483
|
+
this.tenantKey = tenantKey;
|
|
1484
|
+
this.limit = limit;
|
|
1485
|
+
this.active = active;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1422
1488
|
var defaultKey = (row) => row.id;
|
|
1423
1489
|
var shallowEqual4 = (a, b) => {
|
|
1424
1490
|
if (a === b) {
|
|
@@ -1569,6 +1635,31 @@ var createSyncEngine = (options = {}) => {
|
|
|
1569
1635
|
if (next !== undefined)
|
|
1570
1636
|
next();
|
|
1571
1637
|
};
|
|
1638
|
+
const subscriptionsByTenant = new Map;
|
|
1639
|
+
const acquireSubscriptionSlot = (ctx, args) => {
|
|
1640
|
+
const cap = options.subscriptionLimit;
|
|
1641
|
+
if (cap === undefined)
|
|
1642
|
+
return;
|
|
1643
|
+
const tenantKey = cap.key(ctx, args);
|
|
1644
|
+
if (tenantKey === undefined)
|
|
1645
|
+
return;
|
|
1646
|
+
const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
|
|
1647
|
+
if (active2 >= cap.max) {
|
|
1648
|
+
throw new SubscriptionLimitError(tenantKey, cap.max, active2);
|
|
1649
|
+
}
|
|
1650
|
+
subscriptionsByTenant.set(tenantKey, active2 + 1);
|
|
1651
|
+
return tenantKey;
|
|
1652
|
+
};
|
|
1653
|
+
const releaseSubscriptionSlot = (tenantKey) => {
|
|
1654
|
+
if (tenantKey === undefined)
|
|
1655
|
+
return;
|
|
1656
|
+
const active2 = subscriptionsByTenant.get(tenantKey);
|
|
1657
|
+
if (active2 === undefined || active2 <= 1) {
|
|
1658
|
+
subscriptionsByTenant.delete(tenantKey);
|
|
1659
|
+
} else {
|
|
1660
|
+
subscriptionsByTenant.set(tenantKey, active2 - 1);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1572
1663
|
const reactiveCacheMax = options.reactiveCache?.max ?? 256;
|
|
1573
1664
|
const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
|
|
1574
1665
|
const cachedReruns = new Map;
|
|
@@ -1610,6 +1701,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1610
1701
|
const runInTransaction = options.transaction;
|
|
1611
1702
|
const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
|
|
1612
1703
|
let clusterBus;
|
|
1704
|
+
const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
|
|
1613
1705
|
const importChangeLog = (snapshot) => {
|
|
1614
1706
|
if (version !== 0) {
|
|
1615
1707
|
throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
|
|
@@ -2401,89 +2493,125 @@ var createSyncEngine = (options = {}) => {
|
|
|
2401
2493
|
registry.set(collection.name, collection);
|
|
2402
2494
|
},
|
|
2403
2495
|
subscribe: async ({ collection, params, ctx, onDiff, since, signal }) => {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
}
|
|
2409
|
-
const typedOnDiff = onDiff;
|
|
2410
|
-
const subscribeSet = subsFor(collection);
|
|
2411
|
-
const wrapReturn = (sub) => {
|
|
2412
|
-
checkAborted(signal);
|
|
2413
|
-
linkAbortToUnsubscribe(signal, sub.unsubscribe);
|
|
2414
|
-
return sub;
|
|
2415
|
-
};
|
|
2416
|
-
const registeredKind = registered.kind;
|
|
2417
|
-
if (registeredKind === "join") {
|
|
2418
|
-
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2419
|
-
return wrapReturn(joined);
|
|
2420
|
-
}
|
|
2421
|
-
if (registeredKind === "graph") {
|
|
2422
|
-
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2423
|
-
return wrapReturn(graphed);
|
|
2424
|
-
}
|
|
2425
|
-
if (registeredKind === "reactive") {
|
|
2426
|
-
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2427
|
-
return wrapReturn(reactived);
|
|
2428
|
-
}
|
|
2429
|
-
if (registeredKind === "search") {
|
|
2430
|
-
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2431
|
-
return wrapReturn(searched);
|
|
2432
|
-
}
|
|
2433
|
-
const definition = registered;
|
|
2434
|
-
if (definition.authorize !== undefined) {
|
|
2435
|
-
const allowed = await definition.authorize(params, ctx);
|
|
2436
|
-
if (!allowed) {
|
|
2437
|
-
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
2496
|
+
const subscribeSpan = tracer.startSpan("sync.subscribe", {
|
|
2497
|
+
attributes: {
|
|
2498
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
2499
|
+
[ABS_ATTRS.collection]: collection
|
|
2438
2500
|
}
|
|
2439
|
-
}
|
|
2440
|
-
const key = definition.key ?? defaultKey;
|
|
2441
|
-
const match = definition.match;
|
|
2442
|
-
const tables = definition.tables ?? [collection];
|
|
2443
|
-
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2444
|
-
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2445
|
-
const rehydrate = async () => {
|
|
2446
|
-
const raw = [...await definition.hydrate(params, ctx)];
|
|
2447
|
-
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2448
|
-
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2449
|
-
};
|
|
2450
|
-
const incremental = match !== undefined && tables.length === 1;
|
|
2451
|
-
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2452
|
-
const view = createMaterializedView({
|
|
2453
|
-
key,
|
|
2454
|
-
match: boundMatch
|
|
2455
2501
|
});
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2502
|
+
try {
|
|
2503
|
+
checkAborted(signal);
|
|
2504
|
+
const registered = registry.get(collection);
|
|
2505
|
+
if (registered === undefined) {
|
|
2506
|
+
throw new Error(`Unknown collection "${collection}"`);
|
|
2507
|
+
}
|
|
2508
|
+
const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
|
|
2509
|
+
let slotHandedOff = false;
|
|
2510
|
+
try {
|
|
2511
|
+
const typedOnDiff = onDiff;
|
|
2512
|
+
const subscribeSet = subsFor(collection);
|
|
2513
|
+
const wrapReturn = (sub) => {
|
|
2514
|
+
checkAborted(signal);
|
|
2515
|
+
const innerUnsubscribe = sub.unsubscribe;
|
|
2516
|
+
let released = false;
|
|
2517
|
+
const wrappedUnsubscribe = () => {
|
|
2518
|
+
if (released)
|
|
2519
|
+
return;
|
|
2520
|
+
released = true;
|
|
2521
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2522
|
+
innerUnsubscribe();
|
|
2523
|
+
};
|
|
2524
|
+
const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
|
|
2525
|
+
linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
|
|
2526
|
+
slotHandedOff = true;
|
|
2527
|
+
return wrapped;
|
|
2528
|
+
};
|
|
2529
|
+
const registeredKind = registered.kind;
|
|
2530
|
+
if (registeredKind === "join") {
|
|
2531
|
+
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2532
|
+
return wrapReturn(joined);
|
|
2533
|
+
}
|
|
2534
|
+
if (registeredKind === "graph") {
|
|
2535
|
+
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2536
|
+
return wrapReturn(graphed);
|
|
2537
|
+
}
|
|
2538
|
+
if (registeredKind === "reactive") {
|
|
2539
|
+
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2540
|
+
return wrapReturn(reactived);
|
|
2541
|
+
}
|
|
2542
|
+
if (registeredKind === "search") {
|
|
2543
|
+
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2544
|
+
return wrapReturn(searched);
|
|
2545
|
+
}
|
|
2546
|
+
const definition = registered;
|
|
2547
|
+
if (definition.authorize !== undefined) {
|
|
2548
|
+
const allowed = await definition.authorize(params, ctx);
|
|
2549
|
+
if (!allowed) {
|
|
2550
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
const key = definition.key ?? defaultKey;
|
|
2554
|
+
const match = definition.match;
|
|
2555
|
+
const tables = definition.tables ?? [collection];
|
|
2556
|
+
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2557
|
+
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2558
|
+
const rehydrate = async () => {
|
|
2559
|
+
const raw = [...await definition.hydrate(params, ctx)];
|
|
2560
|
+
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2561
|
+
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2562
|
+
};
|
|
2563
|
+
const incremental = match !== undefined && tables.length === 1;
|
|
2564
|
+
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2565
|
+
const view = createMaterializedView({
|
|
2566
|
+
key,
|
|
2567
|
+
match: boundMatch
|
|
2568
|
+
});
|
|
2569
|
+
const resuming = since !== undefined && canResume(since, incremental);
|
|
2570
|
+
view.hydrate([...await rehydrate()]);
|
|
2571
|
+
const atVersion = version;
|
|
2572
|
+
const subscription = {
|
|
2573
|
+
kind: "view",
|
|
2574
|
+
collection,
|
|
2575
|
+
view,
|
|
2576
|
+
incremental,
|
|
2577
|
+
rehydrate,
|
|
2578
|
+
key,
|
|
2579
|
+
onDiff: typedOnDiff
|
|
2580
|
+
};
|
|
2581
|
+
subscribeSet.add(subscription);
|
|
2582
|
+
const unsubscribe = () => {
|
|
2583
|
+
subscribeSet.delete(subscription);
|
|
2584
|
+
};
|
|
2585
|
+
if (resuming) {
|
|
2586
|
+
return wrapReturn({
|
|
2587
|
+
initial: [],
|
|
2588
|
+
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2589
|
+
cursor: currentCursor(),
|
|
2590
|
+
version: atVersion,
|
|
2591
|
+
unsubscribe
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
return wrapReturn({
|
|
2595
|
+
initial: view.rows(),
|
|
2596
|
+
cursor: currentCursor(),
|
|
2597
|
+
version: atVersion,
|
|
2598
|
+
unsubscribe
|
|
2599
|
+
});
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
if (!slotHandedOff)
|
|
2602
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2603
|
+
throw error;
|
|
2604
|
+
}
|
|
2605
|
+
} catch (spanError) {
|
|
2606
|
+
subscribeSpan.recordException(spanError);
|
|
2607
|
+
subscribeSpan.setStatus({
|
|
2608
|
+
code: 2,
|
|
2609
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
2479
2610
|
});
|
|
2611
|
+
throw spanError;
|
|
2612
|
+
} finally {
|
|
2613
|
+
subscribeSpan.end();
|
|
2480
2614
|
}
|
|
2481
|
-
return wrapReturn({
|
|
2482
|
-
initial: view.rows(),
|
|
2483
|
-
cursor: currentCursor(),
|
|
2484
|
-
version: atVersion,
|
|
2485
|
-
unsubscribe
|
|
2486
|
-
});
|
|
2487
2615
|
},
|
|
2488
2616
|
hydrate: async (collection, params, ctx, options2) => {
|
|
2489
2617
|
const signal = options2?.signal;
|
|
@@ -2586,85 +2714,102 @@ var createSyncEngine = (options = {}) => {
|
|
|
2586
2714
|
},
|
|
2587
2715
|
migrate: (table, row) => migrateRow(table, row),
|
|
2588
2716
|
runMutation: async (name, args, ctx) => {
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
if (mutation.authorize !== undefined) {
|
|
2594
|
-
const allowed = await mutation.authorize(args, ctx);
|
|
2595
|
-
if (!allowed) {
|
|
2596
|
-
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2717
|
+
const span = tracer.startSpan("sync.runMutation", {
|
|
2718
|
+
attributes: {
|
|
2719
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
2720
|
+
[ABS_ATTRS.mutation]: name
|
|
2597
2721
|
}
|
|
2598
|
-
}
|
|
2599
|
-
await acquireMutationSlot();
|
|
2600
|
-
const sandboxRunner = sandboxRunners.get(name);
|
|
2601
|
-
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2602
|
-
const runHandler = async (tx) => {
|
|
2603
|
-
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2604
|
-
const result = await invokeHandler(args, ctx, actions);
|
|
2605
|
-
return { buffered, result };
|
|
2606
|
-
};
|
|
2607
|
-
const retry = mutation.retry;
|
|
2608
|
-
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2609
|
-
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2610
|
-
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2611
|
-
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2612
|
-
const startedAt = Date.now();
|
|
2613
|
-
let lastError;
|
|
2614
|
-
let attemptsMade = 0;
|
|
2722
|
+
});
|
|
2615
2723
|
try {
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2724
|
+
const mutation = mutations.get(name);
|
|
2725
|
+
if (mutation === undefined) {
|
|
2726
|
+
throw new Error(`Unknown mutation "${name}"`);
|
|
2727
|
+
}
|
|
2728
|
+
if (mutation.authorize !== undefined) {
|
|
2729
|
+
const allowed = await mutation.authorize(args, ctx);
|
|
2730
|
+
if (!allowed) {
|
|
2731
|
+
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
await acquireMutationSlot();
|
|
2735
|
+
const sandboxRunner = sandboxRunners.get(name);
|
|
2736
|
+
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2737
|
+
const runHandler = async (tx) => {
|
|
2738
|
+
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2739
|
+
const result = await invokeHandler(args, ctx, actions);
|
|
2740
|
+
return { buffered, result };
|
|
2741
|
+
};
|
|
2742
|
+
const retry = mutation.retry;
|
|
2743
|
+
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2744
|
+
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2745
|
+
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2746
|
+
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2747
|
+
const startedAt = Date.now();
|
|
2748
|
+
let lastError;
|
|
2749
|
+
let attemptsMade = 0;
|
|
2750
|
+
try {
|
|
2751
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2752
|
+
attemptsMade = attempt;
|
|
2753
|
+
try {
|
|
2754
|
+
const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
|
|
2755
|
+
await applyChangeBatch(buffered);
|
|
2756
|
+
mutationsCompleted += 1;
|
|
2757
|
+
emitActivity({
|
|
2758
|
+
type: "mutation",
|
|
2759
|
+
at: Date.now(),
|
|
2760
|
+
name,
|
|
2761
|
+
status: "ok"
|
|
2762
|
+
});
|
|
2763
|
+
return result;
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
lastError = error;
|
|
2766
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2767
|
+
const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
|
|
2768
|
+
if (!canRetry)
|
|
2769
|
+
break;
|
|
2770
|
+
mutationsRetried += 1;
|
|
2771
|
+
const rawDelay = computeDelay(attempt);
|
|
2772
|
+
const remaining = maxElapsedMs - elapsedMs;
|
|
2773
|
+
if (remaining <= 0)
|
|
2774
|
+
break;
|
|
2775
|
+
const delayMs = Math.max(0, Math.min(rawDelay, remaining));
|
|
2776
|
+
emitActivity({
|
|
2777
|
+
type: "mutationRetry",
|
|
2778
|
+
at: Date.now(),
|
|
2779
|
+
name,
|
|
2780
|
+
attempt,
|
|
2781
|
+
delayMs,
|
|
2782
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
2783
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2784
|
+
});
|
|
2785
|
+
if (delayMs > 0) {
|
|
2786
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2787
|
+
}
|
|
2652
2788
|
}
|
|
2653
2789
|
}
|
|
2790
|
+
mutationsFailed += 1;
|
|
2791
|
+
emitActivity({
|
|
2792
|
+
type: "mutation",
|
|
2793
|
+
at: Date.now(),
|
|
2794
|
+
name,
|
|
2795
|
+
status: "error"
|
|
2796
|
+
});
|
|
2797
|
+
if (attemptsMade > 1) {
|
|
2798
|
+
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2799
|
+
}
|
|
2800
|
+
throw lastError;
|
|
2801
|
+
} finally {
|
|
2802
|
+
releaseMutationSlot();
|
|
2654
2803
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
status: "error"
|
|
2804
|
+
} catch (spanError) {
|
|
2805
|
+
span.recordException(spanError);
|
|
2806
|
+
span.setStatus({
|
|
2807
|
+
code: 2,
|
|
2808
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
2661
2809
|
});
|
|
2662
|
-
|
|
2663
|
-
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2664
|
-
}
|
|
2665
|
-
throw lastError;
|
|
2810
|
+
throw spanError;
|
|
2666
2811
|
} finally {
|
|
2667
|
-
|
|
2812
|
+
span.end();
|
|
2668
2813
|
}
|
|
2669
2814
|
},
|
|
2670
2815
|
runMutations: async (specs, ctx) => {
|
|
@@ -2948,6 +3093,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2948
3093
|
},
|
|
2949
3094
|
subscriptions: {
|
|
2950
3095
|
byCollection,
|
|
3096
|
+
byTenant: Object.fromEntries(subscriptionsByTenant),
|
|
2951
3097
|
total: totalSubscriptions
|
|
2952
3098
|
},
|
|
2953
3099
|
uptimeMs: now - engineStartedAt,
|
|
@@ -3476,6 +3622,7 @@ export {
|
|
|
3476
3622
|
chain,
|
|
3477
3623
|
aggregateOp,
|
|
3478
3624
|
UnauthorizedError,
|
|
3625
|
+
SubscriptionLimitError,
|
|
3479
3626
|
SchemaError,
|
|
3480
3627
|
SEARCH_SCORE_FIELD,
|
|
3481
3628
|
RetriesExhaustedError,
|
|
@@ -3486,5 +3633,5 @@ export {
|
|
|
3486
3633
|
CdcConsumerSlowError
|
|
3487
3634
|
};
|
|
3488
3635
|
|
|
3489
|
-
//# debugId=
|
|
3636
|
+
//# debugId=774BCA75D2571A6764756E2164756E21
|
|
3490
3637
|
//# sourceMappingURL=index.js.map
|