@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/testing.js CHANGED
@@ -625,6 +625,28 @@ class CdcConsumerSlowError extends Error {
625
625
  this.lastDeliveredVersion = lastDeliveredVersion;
626
626
  }
627
627
  }
628
+
629
+ class MutationQueueOverflowError extends Error {
630
+ queueLimit;
631
+ constructor(queueLimit) {
632
+ 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.`);
633
+ this.name = "MutationQueueOverflowError";
634
+ this.queueLimit = queueLimit;
635
+ }
636
+ }
637
+
638
+ class SubscriptionLimitError extends Error {
639
+ tenantKey;
640
+ limit;
641
+ active;
642
+ constructor(tenantKey, limit, active) {
643
+ super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
644
+ this.name = "SubscriptionLimitError";
645
+ this.tenantKey = tenantKey;
646
+ this.limit = limit;
647
+ this.active = active;
648
+ }
649
+ }
628
650
  var defaultKey = (row) => row.id;
629
651
  var shallowEqual3 = (a, b) => {
630
652
  if (a === b) {
@@ -741,6 +763,65 @@ var createSyncEngine = (options = {}) => {
741
763
  let mutationsFailed = 0;
742
764
  let mutationsRetried = 0;
743
765
  let mutationsInFlight = 0;
766
+ const mutationWaiters = [];
767
+ let mutationsQueued = 0;
768
+ const acquireMutationSlot = async () => {
769
+ const limit = options.mutationConcurrency;
770
+ if (limit === undefined) {
771
+ mutationsInFlight += 1;
772
+ return;
773
+ }
774
+ if (mutationsInFlight < limit && mutationWaiters.length === 0) {
775
+ mutationsInFlight += 1;
776
+ return;
777
+ }
778
+ const queueLimit = options.mutationQueueLimit;
779
+ if (queueLimit !== undefined && mutationsQueued >= queueLimit) {
780
+ throw new MutationQueueOverflowError(queueLimit);
781
+ }
782
+ mutationsQueued += 1;
783
+ try {
784
+ await new Promise((resolve) => {
785
+ mutationWaiters.push(resolve);
786
+ });
787
+ } finally {
788
+ mutationsQueued -= 1;
789
+ }
790
+ mutationsInFlight += 1;
791
+ };
792
+ const releaseMutationSlot = () => {
793
+ mutationsInFlight -= 1;
794
+ if (options.mutationConcurrency === undefined)
795
+ return;
796
+ const next = mutationWaiters.shift();
797
+ if (next !== undefined)
798
+ next();
799
+ };
800
+ const subscriptionsByTenant = new Map;
801
+ const acquireSubscriptionSlot = (ctx, args) => {
802
+ const cap = options.subscriptionLimit;
803
+ if (cap === undefined)
804
+ return;
805
+ const tenantKey = cap.key(ctx, args);
806
+ if (tenantKey === undefined)
807
+ return;
808
+ const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
809
+ if (active2 >= cap.max) {
810
+ throw new SubscriptionLimitError(tenantKey, cap.max, active2);
811
+ }
812
+ subscriptionsByTenant.set(tenantKey, active2 + 1);
813
+ return tenantKey;
814
+ };
815
+ const releaseSubscriptionSlot = (tenantKey) => {
816
+ if (tenantKey === undefined)
817
+ return;
818
+ const active2 = subscriptionsByTenant.get(tenantKey);
819
+ if (active2 === undefined || active2 <= 1) {
820
+ subscriptionsByTenant.delete(tenantKey);
821
+ } else {
822
+ subscriptionsByTenant.set(tenantKey, active2 - 1);
823
+ }
824
+ };
744
825
  const reactiveCacheMax = options.reactiveCache?.max ?? 256;
745
826
  const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
746
827
  const cachedReruns = new Map;
@@ -1578,84 +1659,103 @@ var createSyncEngine = (options = {}) => {
1578
1659
  if (registered === undefined) {
1579
1660
  throw new Error(`Unknown collection "${collection}"`);
1580
1661
  }
1581
- const typedOnDiff = onDiff;
1582
- const subscribeSet = subsFor(collection);
1583
- const wrapReturn = (sub) => {
1584
- checkAborted(signal);
1585
- linkAbortToUnsubscribe(signal, sub.unsubscribe);
1586
- return sub;
1587
- };
1588
- const registeredKind = registered.kind;
1589
- if (registeredKind === "join") {
1590
- const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1591
- return wrapReturn(joined);
1592
- }
1593
- if (registeredKind === "graph") {
1594
- const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1595
- return wrapReturn(graphed);
1596
- }
1597
- if (registeredKind === "reactive") {
1598
- const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1599
- return wrapReturn(reactived);
1600
- }
1601
- if (registeredKind === "search") {
1602
- const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1603
- return wrapReturn(searched);
1604
- }
1605
- const definition = registered;
1606
- if (definition.authorize !== undefined) {
1607
- const allowed = await definition.authorize(params, ctx);
1608
- if (!allowed) {
1609
- throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1662
+ const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
1663
+ let slotHandedOff = false;
1664
+ try {
1665
+ const typedOnDiff = onDiff;
1666
+ const subscribeSet = subsFor(collection);
1667
+ const wrapReturn = (sub) => {
1668
+ checkAborted(signal);
1669
+ const innerUnsubscribe = sub.unsubscribe;
1670
+ let released = false;
1671
+ const wrappedUnsubscribe = () => {
1672
+ if (released)
1673
+ return;
1674
+ released = true;
1675
+ releaseSubscriptionSlot(tenantSlot);
1676
+ innerUnsubscribe();
1677
+ };
1678
+ const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
1679
+ linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
1680
+ slotHandedOff = true;
1681
+ return wrapped;
1682
+ };
1683
+ const registeredKind = registered.kind;
1684
+ if (registeredKind === "join") {
1685
+ const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1686
+ return wrapReturn(joined);
1687
+ }
1688
+ if (registeredKind === "graph") {
1689
+ const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1690
+ return wrapReturn(graphed);
1691
+ }
1692
+ if (registeredKind === "reactive") {
1693
+ const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1694
+ return wrapReturn(reactived);
1695
+ }
1696
+ if (registeredKind === "search") {
1697
+ const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1698
+ return wrapReturn(searched);
1699
+ }
1700
+ const definition = registered;
1701
+ if (definition.authorize !== undefined) {
1702
+ const allowed = await definition.authorize(params, ctx);
1703
+ if (!allowed) {
1704
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1705
+ }
1706
+ }
1707
+ const key = definition.key ?? defaultKey;
1708
+ const match = definition.match;
1709
+ const tables = definition.tables ?? [collection];
1710
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1711
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1712
+ const rehydrate = async () => {
1713
+ const raw = [...await definition.hydrate(params, ctx)];
1714
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1715
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1716
+ };
1717
+ const incremental = match !== undefined && tables.length === 1;
1718
+ const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1719
+ const view = createMaterializedView({
1720
+ key,
1721
+ match: boundMatch
1722
+ });
1723
+ const resuming = since !== undefined && canResume(since, incremental);
1724
+ view.hydrate([...await rehydrate()]);
1725
+ const atVersion = version;
1726
+ const subscription = {
1727
+ kind: "view",
1728
+ collection,
1729
+ view,
1730
+ incremental,
1731
+ rehydrate,
1732
+ key,
1733
+ onDiff: typedOnDiff
1734
+ };
1735
+ subscribeSet.add(subscription);
1736
+ const unsubscribe = () => {
1737
+ subscribeSet.delete(subscription);
1738
+ };
1739
+ if (resuming) {
1740
+ return wrapReturn({
1741
+ initial: [],
1742
+ catchup: buildCatchup(since, tables, key, boundMatch),
1743
+ cursor: currentCursor(),
1744
+ version: atVersion,
1745
+ unsubscribe
1746
+ });
1610
1747
  }
1611
- }
1612
- const key = definition.key ?? defaultKey;
1613
- const match = definition.match;
1614
- const tables = definition.tables ?? [collection];
1615
- const scopedTable = tables.length === 1 ? tables[0] : undefined;
1616
- const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1617
- const rehydrate = async () => {
1618
- const raw = [...await definition.hydrate(params, ctx)];
1619
- const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1620
- return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1621
- };
1622
- const incremental = match !== undefined && tables.length === 1;
1623
- const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1624
- const view = createMaterializedView({
1625
- key,
1626
- match: boundMatch
1627
- });
1628
- const resuming = since !== undefined && canResume(since, incremental);
1629
- view.hydrate([...await rehydrate()]);
1630
- const atVersion = version;
1631
- const subscription = {
1632
- kind: "view",
1633
- collection,
1634
- view,
1635
- incremental,
1636
- rehydrate,
1637
- key,
1638
- onDiff: typedOnDiff
1639
- };
1640
- subscribeSet.add(subscription);
1641
- const unsubscribe = () => {
1642
- subscribeSet.delete(subscription);
1643
- };
1644
- if (resuming) {
1645
1748
  return wrapReturn({
1646
- initial: [],
1647
- catchup: buildCatchup(since, tables, key, boundMatch),
1749
+ initial: view.rows(),
1648
1750
  cursor: currentCursor(),
1649
1751
  version: atVersion,
1650
1752
  unsubscribe
1651
1753
  });
1754
+ } catch (error) {
1755
+ if (!slotHandedOff)
1756
+ releaseSubscriptionSlot(tenantSlot);
1757
+ throw error;
1652
1758
  }
1653
- return wrapReturn({
1654
- initial: view.rows(),
1655
- cursor: currentCursor(),
1656
- version: atVersion,
1657
- unsubscribe
1658
- });
1659
1759
  },
1660
1760
  hydrate: async (collection, params, ctx, options2) => {
1661
1761
  const signal = options2?.signal;
@@ -1768,6 +1868,7 @@ var createSyncEngine = (options = {}) => {
1768
1868
  throw new UnauthorizedError(`run mutation "${name}"`);
1769
1869
  }
1770
1870
  }
1871
+ await acquireMutationSlot();
1771
1872
  const sandboxRunner = sandboxRunners.get(name);
1772
1873
  const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
1773
1874
  const runHandler = async (tx) => {
@@ -1783,7 +1884,6 @@ var createSyncEngine = (options = {}) => {
1783
1884
  const startedAt = Date.now();
1784
1885
  let lastError;
1785
1886
  let attemptsMade = 0;
1786
- mutationsInFlight += 1;
1787
1887
  try {
1788
1888
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1789
1889
  attemptsMade = attempt;
@@ -1836,7 +1936,7 @@ var createSyncEngine = (options = {}) => {
1836
1936
  }
1837
1937
  throw lastError;
1838
1938
  } finally {
1839
- mutationsInFlight -= 1;
1939
+ releaseMutationSlot();
1840
1940
  }
1841
1941
  },
1842
1942
  runMutations: async (specs, ctx) => {
@@ -1849,6 +1949,7 @@ var createSyncEngine = (options = {}) => {
1849
1949
  }
1850
1950
  return { args: spec.args, mutation, name: spec.name };
1851
1951
  });
1952
+ await acquireMutationSlot();
1852
1953
  const runBatch = async (tx) => {
1853
1954
  const results = [];
1854
1955
  const accumulated = [];
@@ -1886,6 +1987,8 @@ var createSyncEngine = (options = {}) => {
1886
1987
  status: "error"
1887
1988
  });
1888
1989
  throw error;
1990
+ } finally {
1991
+ releaseMutationSlot();
1889
1992
  }
1890
1993
  },
1891
1994
  registerSchedule: (schedule) => {
@@ -2105,6 +2208,7 @@ var createSyncEngine = (options = {}) => {
2105
2208
  completed: mutationsCompleted,
2106
2209
  failed: mutationsFailed,
2107
2210
  inFlight: mutationsInFlight,
2211
+ queued: mutationsQueued,
2108
2212
  retried: mutationsRetried
2109
2213
  },
2110
2214
  reactiveCache: {
@@ -2116,6 +2220,7 @@ var createSyncEngine = (options = {}) => {
2116
2220
  },
2117
2221
  subscriptions: {
2118
2222
  byCollection,
2223
+ byTenant: Object.fromEntries(subscriptionsByTenant),
2119
2224
  total: totalSubscriptions
2120
2225
  },
2121
2226
  uptimeMs: now - engineStartedAt,
@@ -2225,5 +2330,5 @@ export {
2225
2330
  createTestEngine
2226
2331
  };
2227
2332
 
2228
- //# debugId=D332A9D90117FA7264756E2164756E21
2333
+ //# debugId=5456F0E468513B7264756E2164756E21
2229
2334
  //# sourceMappingURL=testing.js.map