@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TracerProvider as TelemetryTracerProvider } from '@absolutejs/telemetry';
|
|
1
2
|
import type { CollectionContext, CollectionDefinition, JoinCollectionDefinition } from './collection';
|
|
2
3
|
import type { GraphCollectionDefinition } from './graph';
|
|
3
4
|
import type { MutationDefinition, TableWriter, TransactionRunner } from './mutation';
|
|
@@ -425,6 +426,18 @@ export declare class MutationQueueOverflowError extends Error {
|
|
|
425
426
|
readonly queueLimit: number;
|
|
426
427
|
constructor(queueLimit: number);
|
|
427
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Thrown by `engine.subscribe` when the calling tenant's active-subscription
|
|
431
|
+
* count is already at the configured `subscriptionLimit.max`. The caller sees
|
|
432
|
+
* this immediately — BEFORE authorize, hydrate, or any subscription state
|
|
433
|
+
* allocation — so a rejected call leaks nothing. Added in 1.20.1.
|
|
434
|
+
*/
|
|
435
|
+
export declare class SubscriptionLimitError extends Error {
|
|
436
|
+
readonly tenantKey: string;
|
|
437
|
+
readonly limit: number;
|
|
438
|
+
readonly active: number;
|
|
439
|
+
constructor(tenantKey: string, limit: number, active: number);
|
|
440
|
+
}
|
|
428
441
|
/**
|
|
429
442
|
* Serializable snapshot of an engine's change log + monotonic version, returned
|
|
430
443
|
* by {@link SyncEngine.exportChangeLog} and consumed by
|
|
@@ -597,6 +610,51 @@ export type SyncEngineOptions = {
|
|
|
597
610
|
* Added in 1.20.0.
|
|
598
611
|
*/
|
|
599
612
|
mutationQueueLimit?: number;
|
|
613
|
+
/**
|
|
614
|
+
* Per-tenant active-subscription cap. Symmetric to
|
|
615
|
+
* {@link SyncEngineOptions.mutationConcurrency} on the read side: a
|
|
616
|
+
* single tenant opening thousands of subscriptions would otherwise
|
|
617
|
+
* exhaust the engine's per-subscription bookkeeping
|
|
618
|
+
* (`active`/`tableIndex` Maps, the reactive cache, per-row diff
|
|
619
|
+
* computation cost).
|
|
620
|
+
*
|
|
621
|
+
* `key` derives a tenant identifier from `(ctx, args)`; returning
|
|
622
|
+
* `undefined` skips the cap for that call (e.g. internal/system
|
|
623
|
+
* subscriptions). When the active count for a key reaches `max`, the
|
|
624
|
+
* next `subscribe` throws {@link SubscriptionLimitError} BEFORE any
|
|
625
|
+
* authorize, hydrate, or state allocation — so a denied call leaks
|
|
626
|
+
* nothing.
|
|
627
|
+
*
|
|
628
|
+
* Active counts are surfaced through `engine.metrics().subscriptions.byTenant`
|
|
629
|
+
* for tier monitoring. Added in 1.20.1.
|
|
630
|
+
*/
|
|
631
|
+
subscriptionLimit?: {
|
|
632
|
+
max: number;
|
|
633
|
+
key: (ctx: unknown, args: {
|
|
634
|
+
collection: string;
|
|
635
|
+
}) => string | undefined;
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* Optional OpenTelemetry tracer provider. When set, the engine
|
|
639
|
+
* wraps `subscribe`, `runMutation`, `runMutations`, and cluster
|
|
640
|
+
* fan-out in spans named `sync.<op>` with `ABS_ATTRS` semantic
|
|
641
|
+
* conventions (`abs.engine.id`, `abs.collection`, `abs.mutation`,
|
|
642
|
+
* etc.). When absent, all tracing is a zero-allocation noop —
|
|
643
|
+
* existing call sites pay nothing. Added in 1.21.0.
|
|
644
|
+
*
|
|
645
|
+
* Pass any `@opentelemetry/api`-compatible `TracerProvider`. See
|
|
646
|
+
* `@absolutejs/telemetry` for the type shape — sync re-uses its
|
|
647
|
+
* helpers but doesn't peer-dep `@opentelemetry/api` directly.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```ts
|
|
651
|
+
* import { NodeTracerProvider } from '@opentelemetry/sdk-node';
|
|
652
|
+
* const tp = new NodeTracerProvider({ ... });
|
|
653
|
+
* tp.register();
|
|
654
|
+
* const engine = createSyncEngine({ tracerProvider: tp });
|
|
655
|
+
* ```
|
|
656
|
+
*/
|
|
657
|
+
tracerProvider?: TelemetryTracerProvider;
|
|
600
658
|
};
|
|
601
659
|
/**
|
|
602
660
|
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
package/dist/index.js
CHANGED
|
@@ -542,6 +542,59 @@ var syncSocket = ({
|
|
|
542
542
|
}
|
|
543
543
|
});
|
|
544
544
|
};
|
|
545
|
+
// node_modules/@absolutejs/telemetry/dist/index.js
|
|
546
|
+
var NOOP_SPAN_CONTEXT = {
|
|
547
|
+
spanId: "0000000000000000",
|
|
548
|
+
traceFlags: 0,
|
|
549
|
+
traceId: "00000000000000000000000000000000"
|
|
550
|
+
};
|
|
551
|
+
var noopSpan = {
|
|
552
|
+
addEvent: () => noopSpan,
|
|
553
|
+
end: () => {},
|
|
554
|
+
isRecording: () => false,
|
|
555
|
+
recordException: () => {},
|
|
556
|
+
setAttribute: () => noopSpan,
|
|
557
|
+
setAttributes: () => noopSpan,
|
|
558
|
+
setStatus: () => noopSpan,
|
|
559
|
+
spanContext: () => NOOP_SPAN_CONTEXT,
|
|
560
|
+
updateName: () => noopSpan
|
|
561
|
+
};
|
|
562
|
+
var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
|
|
563
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
564
|
+
return fn(noopSpan);
|
|
565
|
+
};
|
|
566
|
+
var noopTracer = {
|
|
567
|
+
startActiveSpan: startActiveSpanNoop,
|
|
568
|
+
startSpan: () => noopSpan
|
|
569
|
+
};
|
|
570
|
+
var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
|
|
571
|
+
var ABS_ATTRS = {
|
|
572
|
+
tenant: "abs.tenant",
|
|
573
|
+
shardId: "abs.shard.id",
|
|
574
|
+
engineId: "abs.engine.id",
|
|
575
|
+
collection: "abs.collection",
|
|
576
|
+
mutation: "abs.mutation",
|
|
577
|
+
mutationAttempt: "abs.mutation.attempt",
|
|
578
|
+
subscriptionId: "abs.subscription.id",
|
|
579
|
+
batchSize: "abs.batch.size",
|
|
580
|
+
clusterMessageOrigin: "abs.cluster.origin",
|
|
581
|
+
jobId: "abs.job.id",
|
|
582
|
+
jobKind: "abs.job.kind",
|
|
583
|
+
jobAttempt: "abs.job.attempt",
|
|
584
|
+
jobMaxAttempts: "abs.job.max_attempts",
|
|
585
|
+
workerId: "abs.worker.id",
|
|
586
|
+
runtimeKey: "abs.runtime.key",
|
|
587
|
+
runtimePid: "abs.runtime.pid",
|
|
588
|
+
runtimePort: "abs.runtime.port",
|
|
589
|
+
runtimeExitReason: "abs.runtime.exit_reason",
|
|
590
|
+
runtimeReadinessMs: "abs.runtime.readiness_ms",
|
|
591
|
+
routeShard: "abs.route.shard",
|
|
592
|
+
routeDecision: "abs.route.decision",
|
|
593
|
+
secretName: "abs.secret.name",
|
|
594
|
+
secretFingerprint: "abs.secret.fingerprint",
|
|
595
|
+
auditKind: "abs.audit.kind"
|
|
596
|
+
};
|
|
597
|
+
|
|
545
598
|
// src/engine/equiJoin.ts
|
|
546
599
|
var shallowEqual = (a, b) => {
|
|
547
600
|
if (a === b) {
|
|
@@ -1161,6 +1214,19 @@ class MutationQueueOverflowError extends Error {
|
|
|
1161
1214
|
this.queueLimit = queueLimit;
|
|
1162
1215
|
}
|
|
1163
1216
|
}
|
|
1217
|
+
|
|
1218
|
+
class SubscriptionLimitError extends Error {
|
|
1219
|
+
tenantKey;
|
|
1220
|
+
limit;
|
|
1221
|
+
active;
|
|
1222
|
+
constructor(tenantKey, limit, active) {
|
|
1223
|
+
super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
|
|
1224
|
+
this.name = "SubscriptionLimitError";
|
|
1225
|
+
this.tenantKey = tenantKey;
|
|
1226
|
+
this.limit = limit;
|
|
1227
|
+
this.active = active;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1164
1230
|
var defaultKey = (row) => row.id;
|
|
1165
1231
|
var shallowEqual3 = (a, b) => {
|
|
1166
1232
|
if (a === b) {
|
|
@@ -1311,6 +1377,31 @@ var createSyncEngine = (options = {}) => {
|
|
|
1311
1377
|
if (next !== undefined)
|
|
1312
1378
|
next();
|
|
1313
1379
|
};
|
|
1380
|
+
const subscriptionsByTenant = new Map;
|
|
1381
|
+
const acquireSubscriptionSlot = (ctx, args) => {
|
|
1382
|
+
const cap = options.subscriptionLimit;
|
|
1383
|
+
if (cap === undefined)
|
|
1384
|
+
return;
|
|
1385
|
+
const tenantKey = cap.key(ctx, args);
|
|
1386
|
+
if (tenantKey === undefined)
|
|
1387
|
+
return;
|
|
1388
|
+
const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
|
|
1389
|
+
if (active2 >= cap.max) {
|
|
1390
|
+
throw new SubscriptionLimitError(tenantKey, cap.max, active2);
|
|
1391
|
+
}
|
|
1392
|
+
subscriptionsByTenant.set(tenantKey, active2 + 1);
|
|
1393
|
+
return tenantKey;
|
|
1394
|
+
};
|
|
1395
|
+
const releaseSubscriptionSlot = (tenantKey) => {
|
|
1396
|
+
if (tenantKey === undefined)
|
|
1397
|
+
return;
|
|
1398
|
+
const active2 = subscriptionsByTenant.get(tenantKey);
|
|
1399
|
+
if (active2 === undefined || active2 <= 1) {
|
|
1400
|
+
subscriptionsByTenant.delete(tenantKey);
|
|
1401
|
+
} else {
|
|
1402
|
+
subscriptionsByTenant.set(tenantKey, active2 - 1);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1314
1405
|
const reactiveCacheMax = options.reactiveCache?.max ?? 256;
|
|
1315
1406
|
const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
|
|
1316
1407
|
const cachedReruns = new Map;
|
|
@@ -1352,6 +1443,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1352
1443
|
const runInTransaction = options.transaction;
|
|
1353
1444
|
const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
|
|
1354
1445
|
let clusterBus;
|
|
1446
|
+
const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
|
|
1355
1447
|
const importChangeLog = (snapshot) => {
|
|
1356
1448
|
if (version !== 0) {
|
|
1357
1449
|
throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
|
|
@@ -2143,89 +2235,125 @@ var createSyncEngine = (options = {}) => {
|
|
|
2143
2235
|
registry.set(collection.name, collection);
|
|
2144
2236
|
},
|
|
2145
2237
|
subscribe: async ({ collection, params, ctx, onDiff, since, signal }) => {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
const typedOnDiff = onDiff;
|
|
2152
|
-
const subscribeSet = subsFor(collection);
|
|
2153
|
-
const wrapReturn = (sub) => {
|
|
2154
|
-
checkAborted(signal);
|
|
2155
|
-
linkAbortToUnsubscribe(signal, sub.unsubscribe);
|
|
2156
|
-
return sub;
|
|
2157
|
-
};
|
|
2158
|
-
const registeredKind = registered.kind;
|
|
2159
|
-
if (registeredKind === "join") {
|
|
2160
|
-
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2161
|
-
return wrapReturn(joined);
|
|
2162
|
-
}
|
|
2163
|
-
if (registeredKind === "graph") {
|
|
2164
|
-
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2165
|
-
return wrapReturn(graphed);
|
|
2166
|
-
}
|
|
2167
|
-
if (registeredKind === "reactive") {
|
|
2168
|
-
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2169
|
-
return wrapReturn(reactived);
|
|
2170
|
-
}
|
|
2171
|
-
if (registeredKind === "search") {
|
|
2172
|
-
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2173
|
-
return wrapReturn(searched);
|
|
2174
|
-
}
|
|
2175
|
-
const definition = registered;
|
|
2176
|
-
if (definition.authorize !== undefined) {
|
|
2177
|
-
const allowed = await definition.authorize(params, ctx);
|
|
2178
|
-
if (!allowed) {
|
|
2179
|
-
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
2238
|
+
const subscribeSpan = tracer.startSpan("sync.subscribe", {
|
|
2239
|
+
attributes: {
|
|
2240
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
2241
|
+
[ABS_ATTRS.collection]: collection
|
|
2180
2242
|
}
|
|
2181
|
-
}
|
|
2182
|
-
const key = definition.key ?? defaultKey;
|
|
2183
|
-
const match = definition.match;
|
|
2184
|
-
const tables = definition.tables ?? [collection];
|
|
2185
|
-
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2186
|
-
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2187
|
-
const rehydrate = async () => {
|
|
2188
|
-
const raw = [...await definition.hydrate(params, ctx)];
|
|
2189
|
-
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2190
|
-
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2191
|
-
};
|
|
2192
|
-
const incremental = match !== undefined && tables.length === 1;
|
|
2193
|
-
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2194
|
-
const view = createMaterializedView({
|
|
2195
|
-
key,
|
|
2196
|
-
match: boundMatch
|
|
2197
2243
|
});
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2244
|
+
try {
|
|
2245
|
+
checkAborted(signal);
|
|
2246
|
+
const registered = registry.get(collection);
|
|
2247
|
+
if (registered === undefined) {
|
|
2248
|
+
throw new Error(`Unknown collection "${collection}"`);
|
|
2249
|
+
}
|
|
2250
|
+
const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
|
|
2251
|
+
let slotHandedOff = false;
|
|
2252
|
+
try {
|
|
2253
|
+
const typedOnDiff = onDiff;
|
|
2254
|
+
const subscribeSet = subsFor(collection);
|
|
2255
|
+
const wrapReturn = (sub) => {
|
|
2256
|
+
checkAborted(signal);
|
|
2257
|
+
const innerUnsubscribe = sub.unsubscribe;
|
|
2258
|
+
let released = false;
|
|
2259
|
+
const wrappedUnsubscribe = () => {
|
|
2260
|
+
if (released)
|
|
2261
|
+
return;
|
|
2262
|
+
released = true;
|
|
2263
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2264
|
+
innerUnsubscribe();
|
|
2265
|
+
};
|
|
2266
|
+
const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
|
|
2267
|
+
linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
|
|
2268
|
+
slotHandedOff = true;
|
|
2269
|
+
return wrapped;
|
|
2270
|
+
};
|
|
2271
|
+
const registeredKind = registered.kind;
|
|
2272
|
+
if (registeredKind === "join") {
|
|
2273
|
+
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2274
|
+
return wrapReturn(joined);
|
|
2275
|
+
}
|
|
2276
|
+
if (registeredKind === "graph") {
|
|
2277
|
+
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2278
|
+
return wrapReturn(graphed);
|
|
2279
|
+
}
|
|
2280
|
+
if (registeredKind === "reactive") {
|
|
2281
|
+
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2282
|
+
return wrapReturn(reactived);
|
|
2283
|
+
}
|
|
2284
|
+
if (registeredKind === "search") {
|
|
2285
|
+
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2286
|
+
return wrapReturn(searched);
|
|
2287
|
+
}
|
|
2288
|
+
const definition = registered;
|
|
2289
|
+
if (definition.authorize !== undefined) {
|
|
2290
|
+
const allowed = await definition.authorize(params, ctx);
|
|
2291
|
+
if (!allowed) {
|
|
2292
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const key = definition.key ?? defaultKey;
|
|
2296
|
+
const match = definition.match;
|
|
2297
|
+
const tables = definition.tables ?? [collection];
|
|
2298
|
+
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2299
|
+
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2300
|
+
const rehydrate = async () => {
|
|
2301
|
+
const raw = [...await definition.hydrate(params, ctx)];
|
|
2302
|
+
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2303
|
+
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2304
|
+
};
|
|
2305
|
+
const incremental = match !== undefined && tables.length === 1;
|
|
2306
|
+
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2307
|
+
const view = createMaterializedView({
|
|
2308
|
+
key,
|
|
2309
|
+
match: boundMatch
|
|
2310
|
+
});
|
|
2311
|
+
const resuming = since !== undefined && canResume(since, incremental);
|
|
2312
|
+
view.hydrate([...await rehydrate()]);
|
|
2313
|
+
const atVersion = version;
|
|
2314
|
+
const subscription = {
|
|
2315
|
+
kind: "view",
|
|
2316
|
+
collection,
|
|
2317
|
+
view,
|
|
2318
|
+
incremental,
|
|
2319
|
+
rehydrate,
|
|
2320
|
+
key,
|
|
2321
|
+
onDiff: typedOnDiff
|
|
2322
|
+
};
|
|
2323
|
+
subscribeSet.add(subscription);
|
|
2324
|
+
const unsubscribe = () => {
|
|
2325
|
+
subscribeSet.delete(subscription);
|
|
2326
|
+
};
|
|
2327
|
+
if (resuming) {
|
|
2328
|
+
return wrapReturn({
|
|
2329
|
+
initial: [],
|
|
2330
|
+
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2331
|
+
cursor: currentCursor(),
|
|
2332
|
+
version: atVersion,
|
|
2333
|
+
unsubscribe
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
return wrapReturn({
|
|
2337
|
+
initial: view.rows(),
|
|
2338
|
+
cursor: currentCursor(),
|
|
2339
|
+
version: atVersion,
|
|
2340
|
+
unsubscribe
|
|
2341
|
+
});
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
if (!slotHandedOff)
|
|
2344
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2345
|
+
throw error;
|
|
2346
|
+
}
|
|
2347
|
+
} catch (spanError) {
|
|
2348
|
+
subscribeSpan.recordException(spanError);
|
|
2349
|
+
subscribeSpan.setStatus({
|
|
2350
|
+
code: 2,
|
|
2351
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
2221
2352
|
});
|
|
2353
|
+
throw spanError;
|
|
2354
|
+
} finally {
|
|
2355
|
+
subscribeSpan.end();
|
|
2222
2356
|
}
|
|
2223
|
-
return wrapReturn({
|
|
2224
|
-
initial: view.rows(),
|
|
2225
|
-
cursor: currentCursor(),
|
|
2226
|
-
version: atVersion,
|
|
2227
|
-
unsubscribe
|
|
2228
|
-
});
|
|
2229
2357
|
},
|
|
2230
2358
|
hydrate: async (collection, params, ctx, options2) => {
|
|
2231
2359
|
const signal = options2?.signal;
|
|
@@ -2328,85 +2456,102 @@ var createSyncEngine = (options = {}) => {
|
|
|
2328
2456
|
},
|
|
2329
2457
|
migrate: (table, row) => migrateRow(table, row),
|
|
2330
2458
|
runMutation: async (name, args, ctx) => {
|
|
2331
|
-
const
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
if (mutation.authorize !== undefined) {
|
|
2336
|
-
const allowed = await mutation.authorize(args, ctx);
|
|
2337
|
-
if (!allowed) {
|
|
2338
|
-
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2459
|
+
const span = tracer.startSpan("sync.runMutation", {
|
|
2460
|
+
attributes: {
|
|
2461
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
2462
|
+
[ABS_ATTRS.mutation]: name
|
|
2339
2463
|
}
|
|
2340
|
-
}
|
|
2341
|
-
await acquireMutationSlot();
|
|
2342
|
-
const sandboxRunner = sandboxRunners.get(name);
|
|
2343
|
-
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2344
|
-
const runHandler = async (tx) => {
|
|
2345
|
-
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2346
|
-
const result = await invokeHandler(args, ctx, actions);
|
|
2347
|
-
return { buffered, result };
|
|
2348
|
-
};
|
|
2349
|
-
const retry = mutation.retry;
|
|
2350
|
-
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2351
|
-
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2352
|
-
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2353
|
-
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2354
|
-
const startedAt = Date.now();
|
|
2355
|
-
let lastError;
|
|
2356
|
-
let attemptsMade = 0;
|
|
2464
|
+
});
|
|
2357
2465
|
try {
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2466
|
+
const mutation = mutations.get(name);
|
|
2467
|
+
if (mutation === undefined) {
|
|
2468
|
+
throw new Error(`Unknown mutation "${name}"`);
|
|
2469
|
+
}
|
|
2470
|
+
if (mutation.authorize !== undefined) {
|
|
2471
|
+
const allowed = await mutation.authorize(args, ctx);
|
|
2472
|
+
if (!allowed) {
|
|
2473
|
+
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
await acquireMutationSlot();
|
|
2477
|
+
const sandboxRunner = sandboxRunners.get(name);
|
|
2478
|
+
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2479
|
+
const runHandler = async (tx) => {
|
|
2480
|
+
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2481
|
+
const result = await invokeHandler(args, ctx, actions);
|
|
2482
|
+
return { buffered, result };
|
|
2483
|
+
};
|
|
2484
|
+
const retry = mutation.retry;
|
|
2485
|
+
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2486
|
+
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2487
|
+
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2488
|
+
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2489
|
+
const startedAt = Date.now();
|
|
2490
|
+
let lastError;
|
|
2491
|
+
let attemptsMade = 0;
|
|
2492
|
+
try {
|
|
2493
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2494
|
+
attemptsMade = attempt;
|
|
2495
|
+
try {
|
|
2496
|
+
const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
|
|
2497
|
+
await applyChangeBatch(buffered);
|
|
2498
|
+
mutationsCompleted += 1;
|
|
2499
|
+
emitActivity({
|
|
2500
|
+
type: "mutation",
|
|
2501
|
+
at: Date.now(),
|
|
2502
|
+
name,
|
|
2503
|
+
status: "ok"
|
|
2504
|
+
});
|
|
2505
|
+
return result;
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
lastError = error;
|
|
2508
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2509
|
+
const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
|
|
2510
|
+
if (!canRetry)
|
|
2511
|
+
break;
|
|
2512
|
+
mutationsRetried += 1;
|
|
2513
|
+
const rawDelay = computeDelay(attempt);
|
|
2514
|
+
const remaining = maxElapsedMs - elapsedMs;
|
|
2515
|
+
if (remaining <= 0)
|
|
2516
|
+
break;
|
|
2517
|
+
const delayMs = Math.max(0, Math.min(rawDelay, remaining));
|
|
2518
|
+
emitActivity({
|
|
2519
|
+
type: "mutationRetry",
|
|
2520
|
+
at: Date.now(),
|
|
2521
|
+
name,
|
|
2522
|
+
attempt,
|
|
2523
|
+
delayMs,
|
|
2524
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
2525
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2526
|
+
});
|
|
2527
|
+
if (delayMs > 0) {
|
|
2528
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2529
|
+
}
|
|
2394
2530
|
}
|
|
2395
2531
|
}
|
|
2532
|
+
mutationsFailed += 1;
|
|
2533
|
+
emitActivity({
|
|
2534
|
+
type: "mutation",
|
|
2535
|
+
at: Date.now(),
|
|
2536
|
+
name,
|
|
2537
|
+
status: "error"
|
|
2538
|
+
});
|
|
2539
|
+
if (attemptsMade > 1) {
|
|
2540
|
+
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2541
|
+
}
|
|
2542
|
+
throw lastError;
|
|
2543
|
+
} finally {
|
|
2544
|
+
releaseMutationSlot();
|
|
2396
2545
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
status: "error"
|
|
2546
|
+
} catch (spanError) {
|
|
2547
|
+
span.recordException(spanError);
|
|
2548
|
+
span.setStatus({
|
|
2549
|
+
code: 2,
|
|
2550
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
2403
2551
|
});
|
|
2404
|
-
|
|
2405
|
-
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2406
|
-
}
|
|
2407
|
-
throw lastError;
|
|
2552
|
+
throw spanError;
|
|
2408
2553
|
} finally {
|
|
2409
|
-
|
|
2554
|
+
span.end();
|
|
2410
2555
|
}
|
|
2411
2556
|
},
|
|
2412
2557
|
runMutations: async (specs, ctx) => {
|
|
@@ -2690,6 +2835,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2690
2835
|
},
|
|
2691
2836
|
subscriptions: {
|
|
2692
2837
|
byCollection,
|
|
2838
|
+
byTenant: Object.fromEntries(subscriptionsByTenant),
|
|
2693
2839
|
total: totalSubscriptions
|
|
2694
2840
|
},
|
|
2695
2841
|
uptimeMs: now - engineStartedAt,
|
|
@@ -3072,5 +3218,5 @@ export {
|
|
|
3072
3218
|
createPresenceHub
|
|
3073
3219
|
};
|
|
3074
3220
|
|
|
3075
|
-
//# debugId=
|
|
3221
|
+
//# debugId=06ECBF0C02EA0B8464756E2164756E21
|
|
3076
3222
|
//# sourceMappingURL=index.js.map
|