@rotorsoft/act 1.6.0 → 1.8.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/index.js CHANGED
@@ -873,6 +873,29 @@ var subscribe = (streams) => store().subscribe(streams);
873
873
 
874
874
  // src/internal/event-sourcing.ts
875
875
  import { patch } from "@rotorsoft/act-patch";
876
+
877
+ // src/internal/backoff.ts
878
+ function computeBackoffDelay(retry, opts) {
879
+ if (!opts || opts.baseMs <= 0) return 0;
880
+ const r = Math.max(0, retry);
881
+ let delay;
882
+ switch (opts.strategy) {
883
+ case "fixed":
884
+ delay = opts.baseMs;
885
+ break;
886
+ case "linear":
887
+ delay = opts.baseMs * (r + 1);
888
+ break;
889
+ case "exponential":
890
+ delay = opts.baseMs * 2 ** r;
891
+ if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
892
+ break;
893
+ }
894
+ if (opts.jitter) delay = delay * (0.5 + Math.random());
895
+ return Math.max(0, Math.floor(delay));
896
+ }
897
+
898
+ // src/internal/event-sourcing.ts
876
899
  var DEFAULT_BATCH = 500;
877
900
  async function snap(snapshot) {
878
901
  try {
@@ -1080,113 +1103,126 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1080
1103
  const { stream, expectedVersion, actor } = target;
1081
1104
  if (!stream) throw new Error("Missing target stream");
1082
1105
  const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
1083
- const snapshot = await load(me, stream);
1084
- if (snapshot.event?.name === TOMBSTONE_EVENT)
1085
- throw new StreamClosedError(stream);
1086
- const expected = expectedVersion ?? snapshot.event?.version;
1087
- if (me.given) {
1088
- const invariants = me.given[action2] || [];
1089
- invariants.forEach(({ valid, description }) => {
1090
- if (!valid(snapshot.state, actor))
1091
- throw new InvariantError(
1092
- action2,
1093
- validated,
1094
- target,
1095
- snapshot,
1096
- description
1097
- );
1098
- });
1099
- }
1100
- const result = me.on[action2](validated, snapshot, target);
1101
- if (!result) return [snapshot];
1102
- if (Array.isArray(result) && result.length === 0) {
1103
- return [snapshot];
1104
- }
1105
- const tuples = Array.isArray(result[0]) ? result : [result];
1106
- const deprecated = me._deprecated;
1107
- if (deprecated && deprecated.size > 0) {
1108
- const me_ = me;
1109
- const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
1110
- for (const [name] of tuples) {
1111
- const evt = name;
1112
- if (deprecated.has(evt) && !warned.has(evt)) {
1113
- warned.add(evt);
1114
- log().warn(
1115
- `Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
1106
+ const opts = me.options?.[action2];
1107
+ const maxRetries = opts?.maxRetries ?? 0;
1108
+ for (let attempt = 0; ; attempt++) {
1109
+ try {
1110
+ const snapshot = await load(me, stream);
1111
+ if (snapshot.event?.name === TOMBSTONE_EVENT)
1112
+ throw new StreamClosedError(stream);
1113
+ const expected = expectedVersion ?? snapshot.event?.version;
1114
+ if (me.given) {
1115
+ const invariants = me.given[action2] || [];
1116
+ invariants.forEach(({ valid, description }) => {
1117
+ if (!valid(snapshot.state, actor))
1118
+ throw new InvariantError(
1119
+ action2,
1120
+ validated,
1121
+ target,
1122
+ snapshot,
1123
+ description
1124
+ );
1125
+ });
1126
+ }
1127
+ const result = me.on[action2](validated, snapshot, target);
1128
+ if (!result) return [snapshot];
1129
+ if (Array.isArray(result) && result.length === 0) {
1130
+ return [snapshot];
1131
+ }
1132
+ const tuples = Array.isArray(result[0]) ? result : [result];
1133
+ const deprecated = me._deprecated;
1134
+ if (deprecated && deprecated.size > 0) {
1135
+ const me_ = me;
1136
+ const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
1137
+ for (const [name] of tuples) {
1138
+ const evt = name;
1139
+ if (deprecated.has(evt) && !warned.has(evt)) {
1140
+ warned.add(evt);
1141
+ log().warn(
1142
+ `Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
1143
+ );
1144
+ }
1145
+ }
1146
+ }
1147
+ const emitted = tuples.map(([name, data]) => ({
1148
+ name,
1149
+ data: skipValidation ? data : validate(name, data, me.events[name])
1150
+ }));
1151
+ const meta = {
1152
+ correlation: reactingTo?.meta.correlation || correlator({
1153
+ action: action2,
1154
+ state: me.name,
1155
+ stream,
1156
+ actor: target.actor
1157
+ }),
1158
+ causation: {
1159
+ action: {
1160
+ name: action2,
1161
+ ...target
1162
+ // payload intentionally omitted: it can be large or contain PII,
1163
+ // and callers correlate via the correlation id when they need it.
1164
+ },
1165
+ event: reactingTo ? {
1166
+ id: reactingTo.id,
1167
+ name: reactingTo.name,
1168
+ stream: reactingTo.stream
1169
+ } : void 0
1170
+ }
1171
+ };
1172
+ let committed;
1173
+ try {
1174
+ committed = await store().commit(
1175
+ stream,
1176
+ emitted,
1177
+ meta,
1178
+ // Reactions skip optimistic concurrency: they always append against the
1179
+ // current head. Stream leasing already serializes concurrent reactions,
1180
+ // and forcing version checks here would turn ordinary catch-up into
1181
+ // spurious retries.
1182
+ reactingTo ? void 0 : expected
1116
1183
  );
1184
+ } catch (error) {
1185
+ if (error instanceof ConcurrencyError) {
1186
+ await cache().invalidate(stream);
1187
+ }
1188
+ throw error;
1189
+ }
1190
+ let { state: state2, patches } = snapshot;
1191
+ const snapshots = committed.map((event) => {
1192
+ const p = me.patch[event.name](event, state2);
1193
+ state2 = patch(state2, p);
1194
+ patches++;
1195
+ return {
1196
+ event,
1197
+ state: state2,
1198
+ version: event.version,
1199
+ patches,
1200
+ snaps: snapshot.snaps,
1201
+ patch: p,
1202
+ cache_hit: snapshot.cache_hit,
1203
+ replayed: snapshot.replayed
1204
+ };
1205
+ });
1206
+ const last = snapshots.at(-1);
1207
+ const snapped = me.snap?.(last);
1208
+ cache().set(stream, {
1209
+ state: last.state,
1210
+ version: last.event.version,
1211
+ event_id: last.event.id,
1212
+ patches: snapped ? 0 : last.patches,
1213
+ snaps: snapped ? last.snaps + 1 : last.snaps
1214
+ }).catch((err) => log().error(err));
1215
+ if (snapped) void snap(last);
1216
+ return snapshots;
1217
+ } catch (error) {
1218
+ if (!(error instanceof ConcurrencyError)) throw error;
1219
+ if (attempt >= maxRetries) throw error;
1220
+ if (opts?.backoff) {
1221
+ const delayMs = computeBackoffDelay(attempt, opts.backoff);
1222
+ if (delayMs > 0) await sleep(delayMs);
1117
1223
  }
1118
1224
  }
1119
1225
  }
1120
- const emitted = tuples.map(([name, data]) => ({
1121
- name,
1122
- data: skipValidation ? data : validate(name, data, me.events[name])
1123
- }));
1124
- const meta = {
1125
- correlation: reactingTo?.meta.correlation || correlator({
1126
- action: action2,
1127
- state: me.name,
1128
- stream,
1129
- actor: target.actor
1130
- }),
1131
- causation: {
1132
- action: {
1133
- name: action2,
1134
- ...target
1135
- // payload intentionally omitted: it can be large or contain PII,
1136
- // and callers correlate via the correlation id when they need it.
1137
- },
1138
- event: reactingTo ? {
1139
- id: reactingTo.id,
1140
- name: reactingTo.name,
1141
- stream: reactingTo.stream
1142
- } : void 0
1143
- }
1144
- };
1145
- let committed;
1146
- try {
1147
- committed = await store().commit(
1148
- stream,
1149
- emitted,
1150
- meta,
1151
- // Reactions skip optimistic concurrency: they always append against the
1152
- // current head. Stream leasing already serializes concurrent reactions,
1153
- // and forcing version checks here would turn ordinary catch-up into
1154
- // spurious retries.
1155
- reactingTo ? void 0 : expected
1156
- );
1157
- } catch (error) {
1158
- if (error instanceof ConcurrencyError) {
1159
- await cache().invalidate(stream);
1160
- }
1161
- throw error;
1162
- }
1163
- let { state: state2, patches } = snapshot;
1164
- const snapshots = committed.map((event) => {
1165
- const p = me.patch[event.name](event, state2);
1166
- state2 = patch(state2, p);
1167
- patches++;
1168
- return {
1169
- event,
1170
- state: state2,
1171
- version: event.version,
1172
- patches,
1173
- snaps: snapshot.snaps,
1174
- patch: p,
1175
- cache_hit: snapshot.cache_hit,
1176
- replayed: snapshot.replayed
1177
- };
1178
- });
1179
- const last = snapshots.at(-1);
1180
- const snapped = me.snap?.(last);
1181
- cache().set(stream, {
1182
- state: last.state,
1183
- version: last.event.version,
1184
- event_id: last.event.id,
1185
- patches: snapped ? 0 : last.patches,
1186
- snaps: snapped ? last.snaps + 1 : last.snaps
1187
- }).catch((err) => log().error(err));
1188
- if (snapped) void snap(last);
1189
- return snapshots;
1190
1226
  }
1191
1227
 
1192
1228
  // src/internal/tracing.ts
@@ -1720,27 +1756,6 @@ var _this_ = ({ stream }) => ({
1720
1756
  target: stream
1721
1757
  });
1722
1758
 
1723
- // src/internal/backoff.ts
1724
- function computeBackoffDelay(retry, opts) {
1725
- if (!opts || opts.baseMs <= 0) return 0;
1726
- const r = Math.max(0, retry);
1727
- let delay;
1728
- switch (opts.strategy) {
1729
- case "fixed":
1730
- delay = opts.baseMs;
1731
- break;
1732
- case "linear":
1733
- delay = opts.baseMs * (r + 1);
1734
- break;
1735
- case "exponential":
1736
- delay = opts.baseMs * 2 ** r;
1737
- if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
1738
- break;
1739
- }
1740
- if (opts.jitter) delay = delay * (0.5 + Math.random());
1741
- return Math.max(0, Math.floor(delay));
1742
- }
1743
-
1744
1759
  // src/internal/reactions.ts
1745
1760
  function finalize(lease, handled, at, error, options, logger, failed_at) {
1746
1761
  if (!error) return { lease, handled, acked_at: at };
@@ -3272,7 +3287,7 @@ function state(entry) {
3272
3287
  function action_builder(state2) {
3273
3288
  const internal = state2;
3274
3289
  const builder = {
3275
- on(entry) {
3290
+ on(entry, options) {
3276
3291
  const keys = Object.keys(entry);
3277
3292
  if (keys.length !== 1) throw new Error(".on() requires exactly one key");
3278
3293
  const action2 = keys[0];
@@ -3280,6 +3295,10 @@ function action_builder(state2) {
3280
3295
  if (action2 in internal.actions)
3281
3296
  throw new Error(`Duplicate action "${action2}"`);
3282
3297
  internal.actions[action2] = schema;
3298
+ if (options) {
3299
+ internal.options ??= {};
3300
+ internal.options[action2] = options;
3301
+ }
3283
3302
  function given(rules) {
3284
3303
  internal.given ??= {};
3285
3304
  internal.given[action2] = rules;