@rotorsoft/act 0.25.2 → 0.26.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/index.js CHANGED
@@ -309,7 +309,7 @@ var InMemoryStream = class {
309
309
  _error = "";
310
310
  _leased_by = void 0;
311
311
  _leased_until = void 0;
312
- get is_avaliable() {
312
+ get is_available() {
313
313
  return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date());
314
314
  }
315
315
  get at() {
@@ -503,7 +503,7 @@ var InMemoryStore = class {
503
503
  async claim(lagging, leading, by, millis) {
504
504
  await sleep();
505
505
  const available = [...this._streams.values()].filter(
506
- (s) => s.is_avaliable && (s.at < 0 || this._events.some(
506
+ (s) => s.is_available && (s.at < 0 || this._events.some(
507
507
  (e) => e.id > s.at && e.name !== SNAP_EVENT && (!s.source || RegExp(s.source).test(e.stream))
508
508
  ))
509
509
  );
@@ -747,7 +747,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
747
747
  if (!stream) throw new Error("Missing target stream");
748
748
  payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
749
749
  const snapshot = await load(me, stream);
750
- const expected = expectedVersion || snapshot.event?.version;
750
+ const expected = expectedVersion ?? snapshot.event?.version;
751
751
  logger2.trace(
752
752
  payload,
753
753
  `\u{1F535} ${stream}.${action2}${typeof expected === "number" ? `.${expected}` : ""}`
@@ -806,7 +806,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
806
806
  );
807
807
  } catch (error) {
808
808
  if (error.name === "ERR_CONCURRENCY") {
809
- void cache().invalidate(stream);
809
+ await cache().invalidate(stream);
810
810
  }
811
811
  throw error;
812
812
  }
@@ -834,9 +834,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
834
834
  var logger3 = log();
835
835
  var tracer = build_tracer(config().logLevel);
836
836
  var Act = class {
837
- constructor(registry, _states = /* @__PURE__ */ new Map()) {
837
+ constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
838
838
  this.registry = registry;
839
839
  this._states = _states;
840
+ this._batch_handlers = batchHandlers;
840
841
  const statics = [];
841
842
  for (const [name, register] of Object.entries(this.registry.events)) {
842
843
  if (register.reactions.size > 0) {
@@ -894,6 +895,8 @@ var Act = class {
894
895
  */
895
896
  /** Static resolver targets collected at build time */
896
897
  _static_targets;
898
+ /** Batch handlers for static-target projections (target → handler) */
899
+ _batch_handlers;
897
900
  /**
898
901
  * Executes an action on a state instance, committing resulting events.
899
902
  *
@@ -1135,6 +1138,41 @@ var Act = class {
1135
1138
  }
1136
1139
  return { lease, handled, at };
1137
1140
  }
1141
+ /**
1142
+ * Handles a batch of events for a projection with a batch handler.
1143
+ *
1144
+ * Called by `drain()` when a leased stream is a static-target projection
1145
+ * with a registered batch handler. All events are passed to the handler
1146
+ * in a single call, enabling bulk DB operations.
1147
+ *
1148
+ * @internal
1149
+ * @param lease The lease to handle
1150
+ * @param payloads The reactions to handle
1151
+ * @param batchHandler The batch handler for this projection
1152
+ * @returns The lease with results
1153
+ */
1154
+ async handleBatch(lease, payloads, batchHandler) {
1155
+ const stream = lease.stream;
1156
+ const events = payloads.map((p) => p.event);
1157
+ const at = events.at(-1).id;
1158
+ lease.retry > 0 && logger3.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
1159
+ try {
1160
+ await batchHandler(events, stream);
1161
+ return { lease, handled: events.length, at };
1162
+ } catch (error) {
1163
+ logger3.error(error);
1164
+ const { options } = payloads[0];
1165
+ const block = lease.retry >= options.maxRetries && options.blockOnError;
1166
+ block && logger3.error(`Blocking ${stream} after ${lease.retry} retries.`);
1167
+ return {
1168
+ lease,
1169
+ handled: 0,
1170
+ at: lease.at,
1171
+ error: error.message,
1172
+ block
1173
+ };
1174
+ }
1175
+ }
1138
1176
  /**
1139
1177
  * Processes pending reactions by draining uncommitted events from the event store.
1140
1178
  *
@@ -1229,10 +1267,12 @@ var Act = class {
1229
1267
  leased.map((lease) => {
1230
1268
  const streamFetch = fetched.find((f) => f.stream === lease.stream);
1231
1269
  const at = streamFetch?.events.at(-1)?.id || fetch_window_at;
1232
- return this.handle(
1233
- { ...lease, at },
1234
- payloadsMap.get(lease.stream)
1235
- );
1270
+ const payloads = payloadsMap.get(lease.stream);
1271
+ const batchHandler = this._batch_handlers.get(lease.stream);
1272
+ if (batchHandler && payloads.length > 0) {
1273
+ return this.handleBatch({ ...lease, at }, payloads, batchHandler);
1274
+ }
1275
+ return this.handle({ ...lease, at }, payloads);
1236
1276
  })
1237
1277
  );
1238
1278
  const [lagging_handled, leading_handled] = handled.reduce(
@@ -1617,7 +1657,11 @@ function registerState(state2, states, actions, events) {
1617
1657
  patch: mergedPatch,
1618
1658
  on: { ...existing.on, ...state2.on },
1619
1659
  given: { ...existing.given, ...state2.given },
1620
- snap: state2.snap || existing.snap
1660
+ snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
1661
+ throw new Error(
1662
+ `Duplicate snap strategy for state "${state2.name}"`
1663
+ );
1664
+ })() : state2.snap || existing.snap
1621
1665
  };
1622
1666
  states.set(state2.name, merged);
1623
1667
  for (const name of Object.keys(merged.actions)) {
@@ -1673,14 +1717,15 @@ var _void_ = () => void 0;
1673
1717
  function act(states = /* @__PURE__ */ new Map(), registry = {
1674
1718
  actions: {},
1675
1719
  events: {}
1676
- }, pendingProjections = []) {
1720
+ }, pendingProjections = [], batchHandlers = /* @__PURE__ */ new Map()) {
1677
1721
  const builder = {
1678
1722
  withState: (state2) => {
1679
1723
  registerState(state2, states, registry.actions, registry.events);
1680
1724
  return act(
1681
1725
  states,
1682
1726
  registry,
1683
- pendingProjections
1727
+ pendingProjections,
1728
+ batchHandlers
1684
1729
  );
1685
1730
  },
1686
1731
  withSlice: (input) => {
@@ -1697,22 +1742,28 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1697
1742
  return act(
1698
1743
  states,
1699
1744
  registry,
1700
- pendingProjections
1745
+ pendingProjections,
1746
+ batchHandlers
1701
1747
  );
1702
1748
  },
1703
1749
  withProjection: (proj) => {
1704
1750
  mergeProjection(proj, registry.events);
1751
+ if (proj.batchHandler && proj.target) {
1752
+ batchHandlers.set(proj.target, proj.batchHandler);
1753
+ }
1705
1754
  return act(
1706
1755
  states,
1707
1756
  registry,
1708
- pendingProjections
1757
+ pendingProjections,
1758
+ batchHandlers
1709
1759
  );
1710
1760
  },
1711
1761
  withActor: () => {
1712
1762
  return act(
1713
1763
  states,
1714
1764
  registry,
1715
- pendingProjections
1765
+ pendingProjections,
1766
+ batchHandlers
1716
1767
  );
1717
1768
  },
1718
1769
  on: (event) => ({
@@ -1725,19 +1776,22 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1725
1776
  maxRetries: options?.maxRetries ?? 3
1726
1777
  }
1727
1778
  };
1728
- const name = handler.name || `${String(event)}_${registry.events[event].reactions.size}`;
1729
- registry.events[event].reactions.set(name, reaction);
1779
+ if (!handler.name)
1780
+ throw new Error(
1781
+ `Reaction handler for "${String(event)}" must be a named function`
1782
+ );
1783
+ registry.events[event].reactions.set(handler.name, reaction);
1730
1784
  return {
1731
1785
  ...builder,
1732
1786
  to(resolver) {
1733
- registry.events[event].reactions.set(name, {
1787
+ registry.events[event].reactions.set(handler.name, {
1734
1788
  ...reaction,
1735
1789
  resolver: typeof resolver === "string" ? { target: resolver } : resolver
1736
1790
  });
1737
1791
  return builder;
1738
1792
  },
1739
1793
  void() {
1740
- registry.events[event].reactions.set(name, {
1794
+ registry.events[event].reactions.set(handler.name, {
1741
1795
  ...reaction,
1742
1796
  resolver: _void_
1743
1797
  });
@@ -1749,10 +1803,14 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1749
1803
  build: () => {
1750
1804
  for (const proj of pendingProjections) {
1751
1805
  mergeProjection(proj, registry.events);
1806
+ if (proj.batchHandler && proj.target) {
1807
+ batchHandlers.set(proj.target, proj.batchHandler);
1808
+ }
1752
1809
  }
1753
1810
  return new Act(
1754
1811
  registry,
1755
- states
1812
+ states,
1813
+ batchHandlers
1756
1814
  );
1757
1815
  },
1758
1816
  events: registry.events
@@ -1761,9 +1819,9 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1761
1819
  }
1762
1820
 
1763
1821
  // src/projection-builder.ts
1764
- function projection(target, events = {}) {
1765
- const defaultResolver = target ? { target } : void 0;
1766
- const builder = {
1822
+ function _projection(target, events) {
1823
+ const defaultResolver = typeof target === "string" ? { target } : void 0;
1824
+ const base = {
1767
1825
  on: (entry) => {
1768
1826
  const keys = Object.keys(entry);
1769
1827
  if (keys.length !== 1) throw new Error(".on() requires exactly one key");
@@ -1786,23 +1844,23 @@ function projection(target, events = {}) {
1786
1844
  }
1787
1845
  };
1788
1846
  const register = events[event];
1789
- const name = handler.name || `${event}_${register.reactions.size}`;
1790
- register.reactions.set(name, reaction);
1791
- const nextBuilder = projection(
1792
- target,
1793
- events
1794
- );
1847
+ if (!handler.name)
1848
+ throw new Error(
1849
+ `Projection handler for "${event}" must be a named function`
1850
+ );
1851
+ register.reactions.set(handler.name, reaction);
1852
+ const nextBuilder = _projection(target, events);
1795
1853
  return {
1796
1854
  ...nextBuilder,
1797
1855
  to(resolver) {
1798
- register.reactions.set(name, {
1856
+ register.reactions.set(handler.name, {
1799
1857
  ...reaction,
1800
1858
  resolver: typeof resolver === "string" ? { target: resolver } : resolver
1801
1859
  });
1802
1860
  return nextBuilder;
1803
1861
  },
1804
1862
  void() {
1805
- register.reactions.set(name, {
1863
+ register.reactions.set(handler.name, {
1806
1864
  ...reaction,
1807
1865
  resolver: _void_
1808
1866
  });
@@ -1814,11 +1872,28 @@ function projection(target, events = {}) {
1814
1872
  },
1815
1873
  build: () => ({
1816
1874
  _tag: "Projection",
1817
- events
1875
+ events,
1876
+ ...target !== void 0 && { target }
1818
1877
  }),
1819
1878
  events
1820
1879
  };
1821
- return builder;
1880
+ if (typeof target === "string") {
1881
+ return {
1882
+ ...base,
1883
+ batch: (handler) => ({
1884
+ build: () => ({
1885
+ _tag: "Projection",
1886
+ events,
1887
+ target,
1888
+ batchHandler: handler
1889
+ })
1890
+ })
1891
+ };
1892
+ }
1893
+ return base;
1894
+ }
1895
+ function projection(target, events = {}) {
1896
+ return _projection(target, events);
1822
1897
  }
1823
1898
 
1824
1899
  // src/slice-builder.ts
@@ -1852,19 +1927,22 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
1852
1927
  maxRetries: options?.maxRetries ?? 3
1853
1928
  }
1854
1929
  };
1855
- const name = handler.name || `${String(event)}_${events[event].reactions.size}`;
1856
- events[event].reactions.set(name, reaction);
1930
+ if (!handler.name)
1931
+ throw new Error(
1932
+ `Reaction handler for "${String(event)}" must be a named function`
1933
+ );
1934
+ events[event].reactions.set(handler.name, reaction);
1857
1935
  return {
1858
1936
  ...builder,
1859
1937
  to(resolver) {
1860
- events[event].reactions.set(name, {
1938
+ events[event].reactions.set(handler.name, {
1861
1939
  ...reaction,
1862
1940
  resolver: typeof resolver === "string" ? { target: resolver } : resolver
1863
1941
  });
1864
1942
  return builder;
1865
1943
  },
1866
1944
  void() {
1867
- events[event].reactions.set(name, {
1945
+ events[event].reactions.set(handler.name, {
1868
1946
  ...reaction,
1869
1947
  resolver: _void_
1870
1948
  });