@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.
@@ -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;
@@ -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';
@@ -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
- checkAborted(signal);
2405
- const registered = registry.get(collection);
2406
- if (registered === undefined) {
2407
- throw new Error(`Unknown collection "${collection}"`);
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
- const resuming = since !== undefined && canResume(since, incremental);
2457
- view.hydrate([...await rehydrate()]);
2458
- const atVersion = version;
2459
- const subscription = {
2460
- kind: "view",
2461
- collection,
2462
- view,
2463
- incremental,
2464
- rehydrate,
2465
- key,
2466
- onDiff: typedOnDiff
2467
- };
2468
- subscribeSet.add(subscription);
2469
- const unsubscribe = () => {
2470
- subscribeSet.delete(subscription);
2471
- };
2472
- if (resuming) {
2473
- return wrapReturn({
2474
- initial: [],
2475
- catchup: buildCatchup(since, tables, key, boundMatch),
2476
- cursor: currentCursor(),
2477
- version: atVersion,
2478
- unsubscribe
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 mutation = mutations.get(name);
2590
- if (mutation === undefined) {
2591
- throw new Error(`Unknown mutation "${name}"`);
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
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2617
- attemptsMade = attempt;
2618
- try {
2619
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2620
- await applyChangeBatch(buffered);
2621
- mutationsCompleted += 1;
2622
- emitActivity({
2623
- type: "mutation",
2624
- at: Date.now(),
2625
- name,
2626
- status: "ok"
2627
- });
2628
- return result;
2629
- } catch (error) {
2630
- lastError = error;
2631
- const elapsedMs = Date.now() - startedAt;
2632
- const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2633
- if (!canRetry)
2634
- break;
2635
- mutationsRetried += 1;
2636
- const rawDelay = computeDelay(attempt);
2637
- const remaining = maxElapsedMs - elapsedMs;
2638
- if (remaining <= 0)
2639
- break;
2640
- const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2641
- emitActivity({
2642
- type: "mutationRetry",
2643
- at: Date.now(),
2644
- name,
2645
- attempt,
2646
- delayMs,
2647
- errorName: error instanceof Error ? error.name : "Error",
2648
- errorMessage: error instanceof Error ? error.message : String(error)
2649
- });
2650
- if (delayMs > 0) {
2651
- await new Promise((resolve) => setTimeout(resolve, delayMs));
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
- mutationsFailed += 1;
2656
- emitActivity({
2657
- type: "mutation",
2658
- at: Date.now(),
2659
- name,
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
- if (attemptsMade > 1) {
2663
- throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2664
- }
2665
- throw lastError;
2810
+ throw spanError;
2666
2811
  } finally {
2667
- releaseMutationSlot();
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=E104F5FFDBD631C764756E2164756E21
3636
+ //# debugId=774BCA75D2571A6764756E2164756E21
3490
3637
  //# sourceMappingURL=index.js.map