@absolutejs/sync 1.20.1 → 1.22.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) {
@@ -863,6 +916,7 @@ var createSyncEngine = (options = {}) => {
863
916
  const runInTransaction = options.transaction;
864
917
  const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
865
918
  let clusterBus;
919
+ const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
866
920
  const importChangeLog = (snapshot) => {
867
921
  if (version !== 0) {
868
922
  throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
@@ -1654,107 +1708,124 @@ var createSyncEngine = (options = {}) => {
1654
1708
  registry.set(collection.name, collection);
1655
1709
  },
1656
1710
  subscribe: async ({ collection, params, ctx, onDiff, since, signal }) => {
1657
- checkAborted(signal);
1658
- const registered = registry.get(collection);
1659
- if (registered === undefined) {
1660
- throw new Error(`Unknown collection "${collection}"`);
1661
- }
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);
1711
+ const subscribeSpan = tracer.startSpan("sync.subscribe", {
1712
+ attributes: {
1713
+ [ABS_ATTRS.engineId]: instanceId,
1714
+ [ABS_ATTRS.collection]: collection
1695
1715
  }
1696
- if (registeredKind === "search") {
1697
- const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1698
- return wrapReturn(searched);
1716
+ });
1717
+ try {
1718
+ checkAborted(signal);
1719
+ const registered = registry.get(collection);
1720
+ if (registered === undefined) {
1721
+ throw new Error(`Unknown collection "${collection}"`);
1699
1722
  }
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}"`);
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
+ });
1705
1808
  }
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
1809
  return wrapReturn({
1741
- initial: [],
1742
- catchup: buildCatchup(since, tables, key, boundMatch),
1810
+ initial: view.rows(),
1743
1811
  cursor: currentCursor(),
1744
1812
  version: atVersion,
1745
1813
  unsubscribe
1746
1814
  });
1815
+ } catch (error) {
1816
+ if (!slotHandedOff)
1817
+ releaseSubscriptionSlot(tenantSlot);
1818
+ throw error;
1747
1819
  }
1748
- return wrapReturn({
1749
- initial: view.rows(),
1750
- cursor: currentCursor(),
1751
- version: atVersion,
1752
- unsubscribe
1820
+ } catch (spanError) {
1821
+ subscribeSpan.recordException(spanError);
1822
+ subscribeSpan.setStatus({
1823
+ code: 2,
1824
+ message: spanError instanceof Error ? spanError.message : String(spanError)
1753
1825
  });
1754
- } catch (error) {
1755
- if (!slotHandedOff)
1756
- releaseSubscriptionSlot(tenantSlot);
1757
- throw error;
1826
+ throw spanError;
1827
+ } finally {
1828
+ subscribeSpan.end();
1758
1829
  }
1759
1830
  },
1760
1831
  hydrate: async (collection, params, ctx, options2) => {
@@ -1858,85 +1929,102 @@ var createSyncEngine = (options = {}) => {
1858
1929
  },
1859
1930
  migrate: (table, row) => migrateRow(table, row),
1860
1931
  runMutation: async (name, args, ctx) => {
1861
- const mutation = mutations.get(name);
1862
- if (mutation === undefined) {
1863
- throw new Error(`Unknown mutation "${name}"`);
1864
- }
1865
- if (mutation.authorize !== undefined) {
1866
- const allowed = await mutation.authorize(args, ctx);
1867
- if (!allowed) {
1868
- 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
1869
1936
  }
1870
- }
1871
- await acquireMutationSlot();
1872
- const sandboxRunner = sandboxRunners.get(name);
1873
- const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
1874
- const runHandler = async (tx) => {
1875
- const { actions, buffered } = makeActions(tx, ctx, true);
1876
- const result = await invokeHandler(args, ctx, actions);
1877
- return { buffered, result };
1878
- };
1879
- const retry = mutation.retry;
1880
- const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
1881
- const isRetryable = retry?.isRetryable ?? isSerializationFailure;
1882
- const computeDelay = retry?.backoff ?? exponentialBackoff();
1883
- const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
1884
- const startedAt = Date.now();
1885
- let lastError;
1886
- let attemptsMade = 0;
1937
+ });
1887
1938
  try {
1888
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1889
- attemptsMade = attempt;
1890
- try {
1891
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1892
- await applyChangeBatch(buffered);
1893
- mutationsCompleted += 1;
1894
- emitActivity({
1895
- type: "mutation",
1896
- at: Date.now(),
1897
- name,
1898
- status: "ok"
1899
- });
1900
- return result;
1901
- } catch (error) {
1902
- lastError = error;
1903
- const elapsedMs = Date.now() - startedAt;
1904
- const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
1905
- if (!canRetry)
1906
- break;
1907
- mutationsRetried += 1;
1908
- const rawDelay = computeDelay(attempt);
1909
- const remaining = maxElapsedMs - elapsedMs;
1910
- if (remaining <= 0)
1911
- break;
1912
- const delayMs = Math.max(0, Math.min(rawDelay, remaining));
1913
- emitActivity({
1914
- type: "mutationRetry",
1915
- at: Date.now(),
1916
- name,
1917
- attempt,
1918
- delayMs,
1919
- errorName: error instanceof Error ? error.name : "Error",
1920
- errorMessage: error instanceof Error ? error.message : String(error)
1921
- });
1922
- if (delayMs > 0) {
1923
- 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
+ }
1924
2003
  }
1925
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();
1926
2018
  }
1927
- mutationsFailed += 1;
1928
- emitActivity({
1929
- type: "mutation",
1930
- at: Date.now(),
1931
- name,
1932
- status: "error"
2019
+ } catch (spanError) {
2020
+ span.recordException(spanError);
2021
+ span.setStatus({
2022
+ code: 2,
2023
+ message: spanError instanceof Error ? spanError.message : String(spanError)
1933
2024
  });
1934
- if (attemptsMade > 1) {
1935
- throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
1936
- }
1937
- throw lastError;
2025
+ throw spanError;
1938
2026
  } finally {
1939
- releaseMutationSlot();
2027
+ span.end();
1940
2028
  }
1941
2029
  },
1942
2030
  runMutations: async (specs, ctx) => {
@@ -2186,6 +2274,43 @@ var createSyncEngine = (options = {}) => {
2186
2274
  version
2187
2275
  }),
2188
2276
  importChangeLog,
2277
+ replayTo: async ({ at, tables }) => {
2278
+ const filterTables = tables !== undefined ? new Set(tables) : undefined;
2279
+ const state = new Map;
2280
+ let asOfVersion = 0;
2281
+ let asOfAt = 0;
2282
+ const oldest = changeLog[0];
2283
+ const truncated = oldest !== undefined && oldest.version > 1 && oldest.at > at;
2284
+ for (const entry of changeLog) {
2285
+ if (entry.at > at)
2286
+ break;
2287
+ if (filterTables !== undefined && !filterTables.has(entry.table)) {
2288
+ continue;
2289
+ }
2290
+ let tableState = state.get(entry.table);
2291
+ if (tableState === undefined) {
2292
+ tableState = new Map;
2293
+ state.set(entry.table, tableState);
2294
+ }
2295
+ const reader = readers.get(entry.table);
2296
+ const key = reader?.key?.(entry.change.row) ?? entry.change.row?.id;
2297
+ if (key === undefined) {
2298
+ continue;
2299
+ }
2300
+ if (entry.change.op === "delete") {
2301
+ tableState.delete(key);
2302
+ } else {
2303
+ tableState.set(key, entry.change.row);
2304
+ }
2305
+ asOfVersion = entry.version;
2306
+ asOfAt = entry.at;
2307
+ }
2308
+ const rows = {};
2309
+ for (const [table, map] of state) {
2310
+ rows[table] = [...map.values()];
2311
+ }
2312
+ return { asOfAt, asOfVersion, rows, truncated };
2313
+ },
2189
2314
  metrics: () => {
2190
2315
  const now = Date.now();
2191
2316
  const byCollection = {};
@@ -2330,5 +2455,5 @@ export {
2330
2455
  createTestEngine
2331
2456
  };
2332
2457
 
2333
- //# debugId=5456F0E468513B7264756E2164756E21
2458
+ //# debugId=19605A03FFBA9EF964756E2164756E21
2334
2459
  //# sourceMappingURL=testing.js.map