@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.cjs CHANGED
@@ -2123,6 +2123,29 @@ var subscribe = (streams) => store2().subscribe(streams);
2123
2123
 
2124
2124
  // src/internal/event-sourcing.ts
2125
2125
  var import_act_patch = require("@rotorsoft/act-patch");
2126
+
2127
+ // src/internal/backoff.ts
2128
+ function computeBackoffDelay(retry, opts) {
2129
+ if (!opts || opts.baseMs <= 0) return 0;
2130
+ const r = Math.max(0, retry);
2131
+ let delay;
2132
+ switch (opts.strategy) {
2133
+ case "fixed":
2134
+ delay = opts.baseMs;
2135
+ break;
2136
+ case "linear":
2137
+ delay = opts.baseMs * (r + 1);
2138
+ break;
2139
+ case "exponential":
2140
+ delay = opts.baseMs * 2 ** r;
2141
+ if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
2142
+ break;
2143
+ }
2144
+ if (opts.jitter) delay = delay * (0.5 + Math.random());
2145
+ return Math.max(0, Math.floor(delay));
2146
+ }
2147
+
2148
+ // src/internal/event-sourcing.ts
2126
2149
  var DEFAULT_BATCH = 500;
2127
2150
  async function snap(snapshot) {
2128
2151
  try {
@@ -2330,113 +2353,126 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
2330
2353
  const { stream, expectedVersion, actor } = target;
2331
2354
  if (!stream) throw new Error("Missing target stream");
2332
2355
  const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
2333
- const snapshot = await load(me, stream);
2334
- if (snapshot.event?.name === TOMBSTONE_EVENT)
2335
- throw new StreamClosedError(stream);
2336
- const expected = expectedVersion ?? snapshot.event?.version;
2337
- if (me.given) {
2338
- const invariants = me.given[action2] || [];
2339
- invariants.forEach(({ valid, description }) => {
2340
- if (!valid(snapshot.state, actor))
2341
- throw new InvariantError(
2342
- action2,
2343
- validated,
2344
- target,
2345
- snapshot,
2346
- description
2347
- );
2348
- });
2349
- }
2350
- const result = me.on[action2](validated, snapshot, target);
2351
- if (!result) return [snapshot];
2352
- if (Array.isArray(result) && result.length === 0) {
2353
- return [snapshot];
2354
- }
2355
- const tuples = Array.isArray(result[0]) ? result : [result];
2356
- const deprecated = me._deprecated;
2357
- if (deprecated && deprecated.size > 0) {
2358
- const me_ = me;
2359
- const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
2360
- for (const [name] of tuples) {
2361
- const evt = name;
2362
- if (deprecated.has(evt) && !warned.has(evt)) {
2363
- warned.add(evt);
2364
- log().warn(
2365
- `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)`
2356
+ const opts = me.options?.[action2];
2357
+ const maxRetries = opts?.maxRetries ?? 0;
2358
+ for (let attempt = 0; ; attempt++) {
2359
+ try {
2360
+ const snapshot = await load(me, stream);
2361
+ if (snapshot.event?.name === TOMBSTONE_EVENT)
2362
+ throw new StreamClosedError(stream);
2363
+ const expected = expectedVersion ?? snapshot.event?.version;
2364
+ if (me.given) {
2365
+ const invariants = me.given[action2] || [];
2366
+ invariants.forEach(({ valid, description }) => {
2367
+ if (!valid(snapshot.state, actor))
2368
+ throw new InvariantError(
2369
+ action2,
2370
+ validated,
2371
+ target,
2372
+ snapshot,
2373
+ description
2374
+ );
2375
+ });
2376
+ }
2377
+ const result = me.on[action2](validated, snapshot, target);
2378
+ if (!result) return [snapshot];
2379
+ if (Array.isArray(result) && result.length === 0) {
2380
+ return [snapshot];
2381
+ }
2382
+ const tuples = Array.isArray(result[0]) ? result : [result];
2383
+ const deprecated = me._deprecated;
2384
+ if (deprecated && deprecated.size > 0) {
2385
+ const me_ = me;
2386
+ const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
2387
+ for (const [name] of tuples) {
2388
+ const evt = name;
2389
+ if (deprecated.has(evt) && !warned.has(evt)) {
2390
+ warned.add(evt);
2391
+ log().warn(
2392
+ `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)`
2393
+ );
2394
+ }
2395
+ }
2396
+ }
2397
+ const emitted = tuples.map(([name, data]) => ({
2398
+ name,
2399
+ data: skipValidation ? data : validate(name, data, me.events[name])
2400
+ }));
2401
+ const meta = {
2402
+ correlation: reactingTo?.meta.correlation || correlator({
2403
+ action: action2,
2404
+ state: me.name,
2405
+ stream,
2406
+ actor: target.actor
2407
+ }),
2408
+ causation: {
2409
+ action: {
2410
+ name: action2,
2411
+ ...target
2412
+ // payload intentionally omitted: it can be large or contain PII,
2413
+ // and callers correlate via the correlation id when they need it.
2414
+ },
2415
+ event: reactingTo ? {
2416
+ id: reactingTo.id,
2417
+ name: reactingTo.name,
2418
+ stream: reactingTo.stream
2419
+ } : void 0
2420
+ }
2421
+ };
2422
+ let committed;
2423
+ try {
2424
+ committed = await store2().commit(
2425
+ stream,
2426
+ emitted,
2427
+ meta,
2428
+ // Reactions skip optimistic concurrency: they always append against the
2429
+ // current head. Stream leasing already serializes concurrent reactions,
2430
+ // and forcing version checks here would turn ordinary catch-up into
2431
+ // spurious retries.
2432
+ reactingTo ? void 0 : expected
2366
2433
  );
2434
+ } catch (error) {
2435
+ if (error instanceof ConcurrencyError) {
2436
+ await cache2().invalidate(stream);
2437
+ }
2438
+ throw error;
2439
+ }
2440
+ let { state: state2, patches } = snapshot;
2441
+ const snapshots = committed.map((event) => {
2442
+ const p = me.patch[event.name](event, state2);
2443
+ state2 = (0, import_act_patch.patch)(state2, p);
2444
+ patches++;
2445
+ return {
2446
+ event,
2447
+ state: state2,
2448
+ version: event.version,
2449
+ patches,
2450
+ snaps: snapshot.snaps,
2451
+ patch: p,
2452
+ cache_hit: snapshot.cache_hit,
2453
+ replayed: snapshot.replayed
2454
+ };
2455
+ });
2456
+ const last = snapshots.at(-1);
2457
+ const snapped = me.snap?.(last);
2458
+ cache2().set(stream, {
2459
+ state: last.state,
2460
+ version: last.event.version,
2461
+ event_id: last.event.id,
2462
+ patches: snapped ? 0 : last.patches,
2463
+ snaps: snapped ? last.snaps + 1 : last.snaps
2464
+ }).catch((err) => log().error(err));
2465
+ if (snapped) void snap(last);
2466
+ return snapshots;
2467
+ } catch (error) {
2468
+ if (!(error instanceof ConcurrencyError)) throw error;
2469
+ if (attempt >= maxRetries) throw error;
2470
+ if (opts?.backoff) {
2471
+ const delayMs = computeBackoffDelay(attempt, opts.backoff);
2472
+ if (delayMs > 0) await sleep(delayMs);
2367
2473
  }
2368
2474
  }
2369
2475
  }
2370
- const emitted = tuples.map(([name, data]) => ({
2371
- name,
2372
- data: skipValidation ? data : validate(name, data, me.events[name])
2373
- }));
2374
- const meta = {
2375
- correlation: reactingTo?.meta.correlation || correlator({
2376
- action: action2,
2377
- state: me.name,
2378
- stream,
2379
- actor: target.actor
2380
- }),
2381
- causation: {
2382
- action: {
2383
- name: action2,
2384
- ...target
2385
- // payload intentionally omitted: it can be large or contain PII,
2386
- // and callers correlate via the correlation id when they need it.
2387
- },
2388
- event: reactingTo ? {
2389
- id: reactingTo.id,
2390
- name: reactingTo.name,
2391
- stream: reactingTo.stream
2392
- } : void 0
2393
- }
2394
- };
2395
- let committed;
2396
- try {
2397
- committed = await store2().commit(
2398
- stream,
2399
- emitted,
2400
- meta,
2401
- // Reactions skip optimistic concurrency: they always append against the
2402
- // current head. Stream leasing already serializes concurrent reactions,
2403
- // and forcing version checks here would turn ordinary catch-up into
2404
- // spurious retries.
2405
- reactingTo ? void 0 : expected
2406
- );
2407
- } catch (error) {
2408
- if (error instanceof ConcurrencyError) {
2409
- await cache2().invalidate(stream);
2410
- }
2411
- throw error;
2412
- }
2413
- let { state: state2, patches } = snapshot;
2414
- const snapshots = committed.map((event) => {
2415
- const p = me.patch[event.name](event, state2);
2416
- state2 = (0, import_act_patch.patch)(state2, p);
2417
- patches++;
2418
- return {
2419
- event,
2420
- state: state2,
2421
- version: event.version,
2422
- patches,
2423
- snaps: snapshot.snaps,
2424
- patch: p,
2425
- cache_hit: snapshot.cache_hit,
2426
- replayed: snapshot.replayed
2427
- };
2428
- });
2429
- const last = snapshots.at(-1);
2430
- const snapped = me.snap?.(last);
2431
- cache2().set(stream, {
2432
- state: last.state,
2433
- version: last.event.version,
2434
- event_id: last.event.id,
2435
- patches: snapped ? 0 : last.patches,
2436
- snaps: snapped ? last.snaps + 1 : last.snaps
2437
- }).catch((err) => log().error(err));
2438
- if (snapped) void snap(last);
2439
- return snapshots;
2440
2476
  }
2441
2477
 
2442
2478
  // src/internal/tracing.ts
@@ -2970,27 +3006,6 @@ var _this_ = ({ stream }) => ({
2970
3006
  target: stream
2971
3007
  });
2972
3008
 
2973
- // src/internal/backoff.ts
2974
- function computeBackoffDelay(retry, opts) {
2975
- if (!opts || opts.baseMs <= 0) return 0;
2976
- const r = Math.max(0, retry);
2977
- let delay;
2978
- switch (opts.strategy) {
2979
- case "fixed":
2980
- delay = opts.baseMs;
2981
- break;
2982
- case "linear":
2983
- delay = opts.baseMs * (r + 1);
2984
- break;
2985
- case "exponential":
2986
- delay = opts.baseMs * 2 ** r;
2987
- if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
2988
- break;
2989
- }
2990
- if (opts.jitter) delay = delay * (0.5 + Math.random());
2991
- return Math.max(0, Math.floor(delay));
2992
- }
2993
-
2994
3009
  // src/internal/reactions.ts
2995
3010
  function finalize(lease, handled, at, error, options, logger, failed_at) {
2996
3011
  if (!error) return { lease, handled, acked_at: at };
@@ -4522,7 +4537,7 @@ function state(entry) {
4522
4537
  function action_builder(state2) {
4523
4538
  const internal = state2;
4524
4539
  const builder = {
4525
- on(entry) {
4540
+ on(entry, options) {
4526
4541
  const keys = Object.keys(entry);
4527
4542
  if (keys.length !== 1) throw new Error(".on() requires exactly one key");
4528
4543
  const action2 = keys[0];
@@ -4530,6 +4545,10 @@ function action_builder(state2) {
4530
4545
  if (action2 in internal.actions)
4531
4546
  throw new Error(`Duplicate action "${action2}"`);
4532
4547
  internal.actions[action2] = schema;
4548
+ if (options) {
4549
+ internal.options ??= {};
4550
+ internal.options[action2] = options;
4551
+ }
4533
4552
  function given(rules) {
4534
4553
  internal.given ??= {};
4535
4554
  internal.given[action2] = rules;