@absolutejs/sync 1.20.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.
@@ -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';
@@ -1419,6 +1419,19 @@ class MutationQueueOverflowError extends Error {
1419
1419
  this.queueLimit = queueLimit;
1420
1420
  }
1421
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
+ }
1422
1435
  var defaultKey = (row) => row.id;
1423
1436
  var shallowEqual4 = (a, b) => {
1424
1437
  if (a === b) {
@@ -1569,6 +1582,31 @@ var createSyncEngine = (options = {}) => {
1569
1582
  if (next !== undefined)
1570
1583
  next();
1571
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
+ };
1572
1610
  const reactiveCacheMax = options.reactiveCache?.max ?? 256;
1573
1611
  const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
1574
1612
  const cachedReruns = new Map;
@@ -2406,84 +2444,103 @@ var createSyncEngine = (options = {}) => {
2406
2444
  if (registered === undefined) {
2407
2445
  throw new Error(`Unknown collection "${collection}"`);
2408
2446
  }
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}"`);
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
+ });
2438
2532
  }
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
- });
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
2533
  return wrapReturn({
2474
- initial: [],
2475
- catchup: buildCatchup(since, tables, key, boundMatch),
2534
+ initial: view.rows(),
2476
2535
  cursor: currentCursor(),
2477
2536
  version: atVersion,
2478
2537
  unsubscribe
2479
2538
  });
2539
+ } catch (error) {
2540
+ if (!slotHandedOff)
2541
+ releaseSubscriptionSlot(tenantSlot);
2542
+ throw error;
2480
2543
  }
2481
- return wrapReturn({
2482
- initial: view.rows(),
2483
- cursor: currentCursor(),
2484
- version: atVersion,
2485
- unsubscribe
2486
- });
2487
2544
  },
2488
2545
  hydrate: async (collection, params, ctx, options2) => {
2489
2546
  const signal = options2?.signal;
@@ -2948,6 +3005,7 @@ var createSyncEngine = (options = {}) => {
2948
3005
  },
2949
3006
  subscriptions: {
2950
3007
  byCollection,
3008
+ byTenant: Object.fromEntries(subscriptionsByTenant),
2951
3009
  total: totalSubscriptions
2952
3010
  },
2953
3011
  uptimeMs: now - engineStartedAt,
@@ -3476,6 +3534,7 @@ export {
3476
3534
  chain,
3477
3535
  aggregateOp,
3478
3536
  UnauthorizedError,
3537
+ SubscriptionLimitError,
3479
3538
  SchemaError,
3480
3539
  SEARCH_SCORE_FIELD,
3481
3540
  RetriesExhaustedError,
@@ -3486,5 +3545,5 @@ export {
3486
3545
  CdcConsumerSlowError
3487
3546
  };
3488
3547
 
3489
- //# debugId=E104F5FFDBD631C764756E2164756E21
3548
+ //# debugId=251EC576E67A0CAF64756E2164756E21
3490
3549
  //# sourceMappingURL=index.js.map