@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/testing.js CHANGED
@@ -15,6 +15,59 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __require = import.meta.require;
17
17
 
18
+ // node_modules/@absolutejs/telemetry/dist/index.js
19
+ var NOOP_SPAN_CONTEXT = {
20
+ spanId: "0000000000000000",
21
+ traceFlags: 0,
22
+ traceId: "00000000000000000000000000000000"
23
+ };
24
+ var noopSpan = {
25
+ addEvent: () => noopSpan,
26
+ end: () => {},
27
+ isRecording: () => false,
28
+ recordException: () => {},
29
+ setAttribute: () => noopSpan,
30
+ setAttributes: () => noopSpan,
31
+ setStatus: () => noopSpan,
32
+ spanContext: () => NOOP_SPAN_CONTEXT,
33
+ updateName: () => noopSpan
34
+ };
35
+ var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
36
+ const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
37
+ return fn(noopSpan);
38
+ };
39
+ var noopTracer = {
40
+ startActiveSpan: startActiveSpanNoop,
41
+ startSpan: () => noopSpan
42
+ };
43
+ var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
44
+ var ABS_ATTRS = {
45
+ tenant: "abs.tenant",
46
+ shardId: "abs.shard.id",
47
+ engineId: "abs.engine.id",
48
+ collection: "abs.collection",
49
+ mutation: "abs.mutation",
50
+ mutationAttempt: "abs.mutation.attempt",
51
+ subscriptionId: "abs.subscription.id",
52
+ batchSize: "abs.batch.size",
53
+ clusterMessageOrigin: "abs.cluster.origin",
54
+ jobId: "abs.job.id",
55
+ jobKind: "abs.job.kind",
56
+ jobAttempt: "abs.job.attempt",
57
+ jobMaxAttempts: "abs.job.max_attempts",
58
+ workerId: "abs.worker.id",
59
+ runtimeKey: "abs.runtime.key",
60
+ runtimePid: "abs.runtime.pid",
61
+ runtimePort: "abs.runtime.port",
62
+ runtimeExitReason: "abs.runtime.exit_reason",
63
+ runtimeReadinessMs: "abs.runtime.readiness_ms",
64
+ routeShard: "abs.route.shard",
65
+ routeDecision: "abs.route.decision",
66
+ secretName: "abs.secret.name",
67
+ secretFingerprint: "abs.secret.fingerprint",
68
+ auditKind: "abs.audit.kind"
69
+ };
70
+
18
71
  // src/engine/equiJoin.ts
19
72
  var shallowEqual = (a, b) => {
20
73
  if (a === b) {
@@ -634,6 +687,19 @@ class MutationQueueOverflowError extends Error {
634
687
  this.queueLimit = queueLimit;
635
688
  }
636
689
  }
690
+
691
+ class SubscriptionLimitError extends Error {
692
+ tenantKey;
693
+ limit;
694
+ active;
695
+ constructor(tenantKey, limit, active) {
696
+ super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
697
+ this.name = "SubscriptionLimitError";
698
+ this.tenantKey = tenantKey;
699
+ this.limit = limit;
700
+ this.active = active;
701
+ }
702
+ }
637
703
  var defaultKey = (row) => row.id;
638
704
  var shallowEqual3 = (a, b) => {
639
705
  if (a === b) {
@@ -784,6 +850,31 @@ var createSyncEngine = (options = {}) => {
784
850
  if (next !== undefined)
785
851
  next();
786
852
  };
853
+ const subscriptionsByTenant = new Map;
854
+ const acquireSubscriptionSlot = (ctx, args) => {
855
+ const cap = options.subscriptionLimit;
856
+ if (cap === undefined)
857
+ return;
858
+ const tenantKey = cap.key(ctx, args);
859
+ if (tenantKey === undefined)
860
+ return;
861
+ const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
862
+ if (active2 >= cap.max) {
863
+ throw new SubscriptionLimitError(tenantKey, cap.max, active2);
864
+ }
865
+ subscriptionsByTenant.set(tenantKey, active2 + 1);
866
+ return tenantKey;
867
+ };
868
+ const releaseSubscriptionSlot = (tenantKey) => {
869
+ if (tenantKey === undefined)
870
+ return;
871
+ const active2 = subscriptionsByTenant.get(tenantKey);
872
+ if (active2 === undefined || active2 <= 1) {
873
+ subscriptionsByTenant.delete(tenantKey);
874
+ } else {
875
+ subscriptionsByTenant.set(tenantKey, active2 - 1);
876
+ }
877
+ };
787
878
  const reactiveCacheMax = options.reactiveCache?.max ?? 256;
788
879
  const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
789
880
  const cachedReruns = new Map;
@@ -825,6 +916,7 @@ var createSyncEngine = (options = {}) => {
825
916
  const runInTransaction = options.transaction;
826
917
  const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
827
918
  let clusterBus;
919
+ const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
828
920
  const importChangeLog = (snapshot) => {
829
921
  if (version !== 0) {
830
922
  throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
@@ -1616,89 +1708,125 @@ var createSyncEngine = (options = {}) => {
1616
1708
  registry.set(collection.name, collection);
1617
1709
  },
1618
1710
  subscribe: async ({ collection, params, ctx, onDiff, since, signal }) => {
1619
- checkAborted(signal);
1620
- const registered = registry.get(collection);
1621
- if (registered === undefined) {
1622
- throw new Error(`Unknown collection "${collection}"`);
1623
- }
1624
- const typedOnDiff = onDiff;
1625
- const subscribeSet = subsFor(collection);
1626
- const wrapReturn = (sub) => {
1627
- checkAborted(signal);
1628
- linkAbortToUnsubscribe(signal, sub.unsubscribe);
1629
- return sub;
1630
- };
1631
- const registeredKind = registered.kind;
1632
- if (registeredKind === "join") {
1633
- const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1634
- return wrapReturn(joined);
1635
- }
1636
- if (registeredKind === "graph") {
1637
- const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1638
- return wrapReturn(graphed);
1639
- }
1640
- if (registeredKind === "reactive") {
1641
- const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1642
- return wrapReturn(reactived);
1643
- }
1644
- if (registeredKind === "search") {
1645
- const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1646
- return wrapReturn(searched);
1647
- }
1648
- const definition = registered;
1649
- if (definition.authorize !== undefined) {
1650
- const allowed = await definition.authorize(params, ctx);
1651
- if (!allowed) {
1652
- throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1711
+ const subscribeSpan = tracer.startSpan("sync.subscribe", {
1712
+ attributes: {
1713
+ [ABS_ATTRS.engineId]: instanceId,
1714
+ [ABS_ATTRS.collection]: collection
1653
1715
  }
1654
- }
1655
- const key = definition.key ?? defaultKey;
1656
- const match = definition.match;
1657
- const tables = definition.tables ?? [collection];
1658
- const scopedTable = tables.length === 1 ? tables[0] : undefined;
1659
- const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1660
- const rehydrate = async () => {
1661
- const raw = [...await definition.hydrate(params, ctx)];
1662
- const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1663
- return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1664
- };
1665
- const incremental = match !== undefined && tables.length === 1;
1666
- const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1667
- const view = createMaterializedView({
1668
- key,
1669
- match: boundMatch
1670
1716
  });
1671
- const resuming = since !== undefined && canResume(since, incremental);
1672
- view.hydrate([...await rehydrate()]);
1673
- const atVersion = version;
1674
- const subscription = {
1675
- kind: "view",
1676
- collection,
1677
- view,
1678
- incremental,
1679
- rehydrate,
1680
- key,
1681
- onDiff: typedOnDiff
1682
- };
1683
- subscribeSet.add(subscription);
1684
- const unsubscribe = () => {
1685
- subscribeSet.delete(subscription);
1686
- };
1687
- if (resuming) {
1688
- return wrapReturn({
1689
- initial: [],
1690
- catchup: buildCatchup(since, tables, key, boundMatch),
1691
- cursor: currentCursor(),
1692
- version: atVersion,
1693
- unsubscribe
1717
+ try {
1718
+ checkAborted(signal);
1719
+ const registered = registry.get(collection);
1720
+ if (registered === undefined) {
1721
+ throw new Error(`Unknown collection "${collection}"`);
1722
+ }
1723
+ const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
1724
+ let slotHandedOff = false;
1725
+ try {
1726
+ const typedOnDiff = onDiff;
1727
+ const subscribeSet = subsFor(collection);
1728
+ const wrapReturn = (sub) => {
1729
+ checkAborted(signal);
1730
+ const innerUnsubscribe = sub.unsubscribe;
1731
+ let released = false;
1732
+ const wrappedUnsubscribe = () => {
1733
+ if (released)
1734
+ return;
1735
+ released = true;
1736
+ releaseSubscriptionSlot(tenantSlot);
1737
+ innerUnsubscribe();
1738
+ };
1739
+ const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
1740
+ linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
1741
+ slotHandedOff = true;
1742
+ return wrapped;
1743
+ };
1744
+ const registeredKind = registered.kind;
1745
+ if (registeredKind === "join") {
1746
+ const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1747
+ return wrapReturn(joined);
1748
+ }
1749
+ if (registeredKind === "graph") {
1750
+ const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1751
+ return wrapReturn(graphed);
1752
+ }
1753
+ if (registeredKind === "reactive") {
1754
+ const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1755
+ return wrapReturn(reactived);
1756
+ }
1757
+ if (registeredKind === "search") {
1758
+ const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1759
+ return wrapReturn(searched);
1760
+ }
1761
+ const definition = registered;
1762
+ if (definition.authorize !== undefined) {
1763
+ const allowed = await definition.authorize(params, ctx);
1764
+ if (!allowed) {
1765
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1766
+ }
1767
+ }
1768
+ const key = definition.key ?? defaultKey;
1769
+ const match = definition.match;
1770
+ const tables = definition.tables ?? [collection];
1771
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1772
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1773
+ const rehydrate = async () => {
1774
+ const raw = [...await definition.hydrate(params, ctx)];
1775
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1776
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1777
+ };
1778
+ const incremental = match !== undefined && tables.length === 1;
1779
+ const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1780
+ const view = createMaterializedView({
1781
+ key,
1782
+ match: boundMatch
1783
+ });
1784
+ const resuming = since !== undefined && canResume(since, incremental);
1785
+ view.hydrate([...await rehydrate()]);
1786
+ const atVersion = version;
1787
+ const subscription = {
1788
+ kind: "view",
1789
+ collection,
1790
+ view,
1791
+ incremental,
1792
+ rehydrate,
1793
+ key,
1794
+ onDiff: typedOnDiff
1795
+ };
1796
+ subscribeSet.add(subscription);
1797
+ const unsubscribe = () => {
1798
+ subscribeSet.delete(subscription);
1799
+ };
1800
+ if (resuming) {
1801
+ return wrapReturn({
1802
+ initial: [],
1803
+ catchup: buildCatchup(since, tables, key, boundMatch),
1804
+ cursor: currentCursor(),
1805
+ version: atVersion,
1806
+ unsubscribe
1807
+ });
1808
+ }
1809
+ return wrapReturn({
1810
+ initial: view.rows(),
1811
+ cursor: currentCursor(),
1812
+ version: atVersion,
1813
+ unsubscribe
1814
+ });
1815
+ } catch (error) {
1816
+ if (!slotHandedOff)
1817
+ releaseSubscriptionSlot(tenantSlot);
1818
+ throw error;
1819
+ }
1820
+ } catch (spanError) {
1821
+ subscribeSpan.recordException(spanError);
1822
+ subscribeSpan.setStatus({
1823
+ code: 2,
1824
+ message: spanError instanceof Error ? spanError.message : String(spanError)
1694
1825
  });
1826
+ throw spanError;
1827
+ } finally {
1828
+ subscribeSpan.end();
1695
1829
  }
1696
- return wrapReturn({
1697
- initial: view.rows(),
1698
- cursor: currentCursor(),
1699
- version: atVersion,
1700
- unsubscribe
1701
- });
1702
1830
  },
1703
1831
  hydrate: async (collection, params, ctx, options2) => {
1704
1832
  const signal = options2?.signal;
@@ -1801,85 +1929,102 @@ var createSyncEngine = (options = {}) => {
1801
1929
  },
1802
1930
  migrate: (table, row) => migrateRow(table, row),
1803
1931
  runMutation: async (name, args, ctx) => {
1804
- const mutation = mutations.get(name);
1805
- if (mutation === undefined) {
1806
- throw new Error(`Unknown mutation "${name}"`);
1807
- }
1808
- if (mutation.authorize !== undefined) {
1809
- const allowed = await mutation.authorize(args, ctx);
1810
- if (!allowed) {
1811
- throw new UnauthorizedError(`run mutation "${name}"`);
1932
+ const span = tracer.startSpan("sync.runMutation", {
1933
+ attributes: {
1934
+ [ABS_ATTRS.engineId]: instanceId,
1935
+ [ABS_ATTRS.mutation]: name
1812
1936
  }
1813
- }
1814
- await acquireMutationSlot();
1815
- const sandboxRunner = sandboxRunners.get(name);
1816
- const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
1817
- const runHandler = async (tx) => {
1818
- const { actions, buffered } = makeActions(tx, ctx, true);
1819
- const result = await invokeHandler(args, ctx, actions);
1820
- return { buffered, result };
1821
- };
1822
- const retry = mutation.retry;
1823
- const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
1824
- const isRetryable = retry?.isRetryable ?? isSerializationFailure;
1825
- const computeDelay = retry?.backoff ?? exponentialBackoff();
1826
- const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
1827
- const startedAt = Date.now();
1828
- let lastError;
1829
- let attemptsMade = 0;
1937
+ });
1830
1938
  try {
1831
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1832
- attemptsMade = attempt;
1833
- try {
1834
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1835
- await applyChangeBatch(buffered);
1836
- mutationsCompleted += 1;
1837
- emitActivity({
1838
- type: "mutation",
1839
- at: Date.now(),
1840
- name,
1841
- status: "ok"
1842
- });
1843
- return result;
1844
- } catch (error) {
1845
- lastError = error;
1846
- const elapsedMs = Date.now() - startedAt;
1847
- const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
1848
- if (!canRetry)
1849
- break;
1850
- mutationsRetried += 1;
1851
- const rawDelay = computeDelay(attempt);
1852
- const remaining = maxElapsedMs - elapsedMs;
1853
- if (remaining <= 0)
1854
- break;
1855
- const delayMs = Math.max(0, Math.min(rawDelay, remaining));
1856
- emitActivity({
1857
- type: "mutationRetry",
1858
- at: Date.now(),
1859
- name,
1860
- attempt,
1861
- delayMs,
1862
- errorName: error instanceof Error ? error.name : "Error",
1863
- errorMessage: error instanceof Error ? error.message : String(error)
1864
- });
1865
- if (delayMs > 0) {
1866
- await new Promise((resolve) => setTimeout(resolve, delayMs));
1939
+ const mutation = mutations.get(name);
1940
+ if (mutation === undefined) {
1941
+ throw new Error(`Unknown mutation "${name}"`);
1942
+ }
1943
+ if (mutation.authorize !== undefined) {
1944
+ const allowed = await mutation.authorize(args, ctx);
1945
+ if (!allowed) {
1946
+ throw new UnauthorizedError(`run mutation "${name}"`);
1947
+ }
1948
+ }
1949
+ await acquireMutationSlot();
1950
+ const sandboxRunner = sandboxRunners.get(name);
1951
+ const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
1952
+ const runHandler = async (tx) => {
1953
+ const { actions, buffered } = makeActions(tx, ctx, true);
1954
+ const result = await invokeHandler(args, ctx, actions);
1955
+ return { buffered, result };
1956
+ };
1957
+ const retry = mutation.retry;
1958
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
1959
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
1960
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
1961
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
1962
+ const startedAt = Date.now();
1963
+ let lastError;
1964
+ let attemptsMade = 0;
1965
+ try {
1966
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1967
+ attemptsMade = attempt;
1968
+ try {
1969
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1970
+ await applyChangeBatch(buffered);
1971
+ mutationsCompleted += 1;
1972
+ emitActivity({
1973
+ type: "mutation",
1974
+ at: Date.now(),
1975
+ name,
1976
+ status: "ok"
1977
+ });
1978
+ return result;
1979
+ } catch (error) {
1980
+ lastError = error;
1981
+ const elapsedMs = Date.now() - startedAt;
1982
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
1983
+ if (!canRetry)
1984
+ break;
1985
+ mutationsRetried += 1;
1986
+ const rawDelay = computeDelay(attempt);
1987
+ const remaining = maxElapsedMs - elapsedMs;
1988
+ if (remaining <= 0)
1989
+ break;
1990
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
1991
+ emitActivity({
1992
+ type: "mutationRetry",
1993
+ at: Date.now(),
1994
+ name,
1995
+ attempt,
1996
+ delayMs,
1997
+ errorName: error instanceof Error ? error.name : "Error",
1998
+ errorMessage: error instanceof Error ? error.message : String(error)
1999
+ });
2000
+ if (delayMs > 0) {
2001
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2002
+ }
1867
2003
  }
1868
2004
  }
2005
+ mutationsFailed += 1;
2006
+ emitActivity({
2007
+ type: "mutation",
2008
+ at: Date.now(),
2009
+ name,
2010
+ status: "error"
2011
+ });
2012
+ if (attemptsMade > 1) {
2013
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2014
+ }
2015
+ throw lastError;
2016
+ } finally {
2017
+ releaseMutationSlot();
1869
2018
  }
1870
- mutationsFailed += 1;
1871
- emitActivity({
1872
- type: "mutation",
1873
- at: Date.now(),
1874
- name,
1875
- status: "error"
2019
+ } catch (spanError) {
2020
+ span.recordException(spanError);
2021
+ span.setStatus({
2022
+ code: 2,
2023
+ message: spanError instanceof Error ? spanError.message : String(spanError)
1876
2024
  });
1877
- if (attemptsMade > 1) {
1878
- throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
1879
- }
1880
- throw lastError;
2025
+ throw spanError;
1881
2026
  } finally {
1882
- releaseMutationSlot();
2027
+ span.end();
1883
2028
  }
1884
2029
  },
1885
2030
  runMutations: async (specs, ctx) => {
@@ -2163,6 +2308,7 @@ var createSyncEngine = (options = {}) => {
2163
2308
  },
2164
2309
  subscriptions: {
2165
2310
  byCollection,
2311
+ byTenant: Object.fromEntries(subscriptionsByTenant),
2166
2312
  total: totalSubscriptions
2167
2313
  },
2168
2314
  uptimeMs: now - engineStartedAt,
@@ -2272,5 +2418,5 @@ export {
2272
2418
  createTestEngine
2273
2419
  };
2274
2420
 
2275
- //# debugId=261DFADB673FBB5664756E2164756E21
2421
+ //# debugId=ED4D54755D5C43CD64756E2164756E21
2276
2422
  //# sourceMappingURL=testing.js.map