@nwire/forge 0.8.17 → 0.9.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.
Files changed (38) hide show
  1. package/dist/__tests__/plugin-stress.test.js +7 -7
  2. package/dist/__tests__/plugin-stress.test.js.map +1 -1
  3. package/dist/__tests__/workflow-saga.test.js +26 -0
  4. package/dist/__tests__/workflow-saga.test.js.map +1 -1
  5. package/dist/actor-store.d.ts +20 -2
  6. package/dist/actor-store.d.ts.map +1 -1
  7. package/dist/actor-store.js +30 -2
  8. package/dist/actor-store.js.map +1 -1
  9. package/dist/create-app.d.ts +1 -1
  10. package/dist/create-app.d.ts.map +1 -1
  11. package/dist/create-app.js +7 -4
  12. package/dist/create-app.js.map +1 -1
  13. package/dist/define-action.d.ts +14 -2
  14. package/dist/define-action.d.ts.map +1 -1
  15. package/dist/define-action.js.map +1 -1
  16. package/dist/define-handler.d.ts +3 -3
  17. package/dist/define-handler.d.ts.map +1 -1
  18. package/dist/define-query.d.ts +4 -0
  19. package/dist/define-query.d.ts.map +1 -1
  20. package/dist/define-query.js +4 -2
  21. package/dist/define-query.js.map +1 -1
  22. package/dist/define-upcaster.d.ts +25 -0
  23. package/dist/define-upcaster.d.ts.map +1 -0
  24. package/dist/define-upcaster.js +31 -0
  25. package/dist/define-upcaster.js.map +1 -0
  26. package/dist/define-workflow.d.ts +4 -4
  27. package/dist/define-workflow.d.ts.map +1 -1
  28. package/dist/define-workflow.js +40 -10
  29. package/dist/define-workflow.js.map +1 -1
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/runtime.d.ts +45 -121
  35. package/dist/runtime.d.ts.map +1 -1
  36. package/dist/runtime.js +134 -276
  37. package/dist/runtime.js.map +1 -1
  38. package/package.json +11 -11
package/dist/runtime.js CHANGED
@@ -19,38 +19,20 @@
19
19
  * - Events fan out to actors first, then workflow reactions, then
20
20
  * projections.
21
21
  */
22
- import { rootContainer } from "@nwire/container";
23
22
  import { hook } from "@nwire/hooks";
23
+ import { Runtime as RuntimeBase, serializeError, } from "@nwire/app";
24
24
  import { isValidated, markValidated } from "@nwire/messages";
25
25
  import { seedEnvelope, deriveEnvelope } from "@nwire/envelope";
26
26
  import { InMemoryWorkflowTimerStore, timerEventName, } from "./workflow-timer-store.js";
27
27
  import { randomUUID } from "node:crypto";
28
- import { InMemoryActorStore, createInitialInstance, } from "./actor-store.js";
28
+ import { InMemoryActorStore, createInitialInstance, ActorVersionConflictError, } from "./actor-store.js";
29
29
  import { normalizeEventReturn } from "./event-message.js";
30
30
  import { InMemoryProjectionStore } from "./projection-store.js";
31
31
  import { InMemoryIdempotencyStore } from "./idempotency-store.js";
32
- import { NoopLogger, loggerForEnvelope } from "@nwire/logger";
32
+ import { loggerForEnvelope } from "@nwire/logger";
33
33
  import { InMemoryDeadLetterSink, buildDeadLetterEntry, } from "@nwire/dead-letter";
34
- import { FrameworkEventBus } from "./framework-event-bus.js";
35
34
  import { ActionDispatching, ActionCompleted, ActionFailed, builtInFrameworkEvents, } from "./framework-events.js";
36
- /** Serialize an unknown thrown value into the canonical SerializedError shape. */
37
- function serializeError(err) {
38
- if (err instanceof Error) {
39
- const out = {
40
- name: err.name,
41
- message: err.message,
42
- stack: err.stack,
43
- };
44
- for (const k of Object.keys(err)) {
45
- if (k === "name" || k === "message" || k === "stack")
46
- continue;
47
- out[k] = err[k];
48
- }
49
- return out;
50
- }
51
- return { name: "NonError", message: String(err) };
52
- }
53
- export class Runtime {
35
+ export class Runtime extends RuntimeBase {
54
36
  handlers = new Map();
55
37
  actors = new Map();
56
38
  /**
@@ -83,19 +65,9 @@ export class Runtime {
83
65
  externalCalls = new Map();
84
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
67
  externalCallExecutors = new Map();
86
- container;
87
68
  actorStore;
88
69
  projectionStore;
89
- logger;
90
70
  deadLetterSink;
91
- /**
92
- * Dispatch is a `@nwire/hooks` chain — user middlewares plus a permanent
93
- * "handler" step pinned to the inner edge. `runtime.use(mw)` adapts the
94
- * legacy `DispatchMiddleware` shape into a `ChainFn` and registers it.
95
- * Taps fire into `runtime.onTelemetry` under `kind: "hook.step"`, which is
96
- * what Studio + CLI + OTel consume.
97
- */
98
- dispatchHook;
99
71
  /**
100
72
  * Per-action `action.before:<name>` hooks. Pre-created at
101
73
  * `registerHandler()` so they show up in `listHooks()` + scan + Studio
@@ -118,8 +90,6 @@ export class Runtime {
118
90
  actorTransitionHooks = [];
119
91
  bus;
120
92
  publishToBus;
121
- appName;
122
- telemetryListeners = [];
123
93
  /** Known external events — set by createApp from modules' needs.externalEvents. */
124
94
  externalEventNames = new Set();
125
95
  /** Public-event names (visibility: 'public') — set by createApp from modules' events. */
@@ -128,82 +98,53 @@ export class Runtime {
128
98
  workflowTimerStore;
129
99
  /** Envelope-level dedup table — see RuntimeOptions.idempotencyStore. */
130
100
  idempotencyStore;
131
- /**
132
- * Framework event bus — lifecycle + dispatch hooks. Plugins and apps
133
- * subscribe with `runtime.onFramework(Event, handler)`; the runtime
134
- * fires events at the appropriate sites in `dispatch`, `start`, `stop`.
135
- */
136
- frameworkEvents;
137
101
  constructor(options = {}) {
138
- this.container = options.container ?? rootContainer;
102
+ // Container / logger / appName / frameworkEvents / dispatchHook / use /
103
+ // adoptHook / onTelemetry / offTelemetry / emit / getContainer are all
104
+ // owned by the base. Forge layers the CQRS engine on top.
105
+ super({
106
+ container: options.container,
107
+ logger: options.logger,
108
+ appName: options.appName,
109
+ events: builtInFrameworkEvents,
110
+ });
139
111
  this.actorStore = options.actorStore ?? new InMemoryActorStore();
140
112
  this.projectionStore = options.projectionStore ?? new InMemoryProjectionStore();
141
- this.logger = options.logger ?? new NoopLogger();
142
113
  this.deadLetterSink = options.deadLetterSink ?? new InMemoryDeadLetterSink();
143
114
  this.bus = options.bus;
144
115
  this.publishToBus = options.publishToBus ?? false;
145
- this.appName = options.appName ?? "app";
146
116
  this.workflowTimerStore = options.workflowTimerStore ?? new InMemoryWorkflowTimerStore();
147
117
  this.idempotencyStore = options.idempotencyStore ?? new InMemoryIdempotencyStore();
148
- // Forward bus's per-event hook adoptions through runtime.adoptHook so
149
- // every framework event becomes a registered hook (visible in
150
- // listHooks() / scan / Studio).
151
- this.frameworkEvents = new FrameworkEventBus(this.logger, {
152
- adoptHook: (h) => this.adoptHook(h),
153
- events: builtInFrameworkEvents,
154
- });
155
- // Bridge framework-event firings into the telemetry stream so every
156
- // existing telemetry consumer (dev logger, Studio Live SSE, OTel
157
- // exporter) sees lifecycle activity through the same channel as
158
- // domain events. `namespace` = the second dotted segment so dev
159
- // logger / Studio can group by phase (app / plugin / wire / …).
160
- this.frameworkEvents.onFire((rec) => {
161
- const parts = rec.eventName.split(".");
162
- const namespace = parts.length >= 2 ? parts[1] : "framework";
163
- this.emit({
164
- kind: "lifecycle",
165
- event: rec.eventName,
166
- namespace,
167
- payload: rec.payload,
168
- phase: rec.phase,
169
- appName: this.appName,
170
- ts: rec.ts,
171
- });
172
- });
173
- // Set up the dispatch hook + the innermost "handler" step (pinned at
174
- // -Infinity so user middleware always wraps it). Every step emits a
175
- // start/end/error observation through the tap into the canonical
176
- // telemetry stream — that's what powers Studio Live + CLI hook trace.
177
- this.dispatchHook = hook("forge.action.dispatch");
118
+ // Pin the innermost dispatch step that calls forge's per-action retry +
119
+ // handler-invocation + event-publishing closure. priority `-Infinity`
120
+ // keeps user `runtime.use()` middleware strictly outside it.
178
121
  this.dispatchHook.use(async (hctx, next) => {
179
122
  hctx.result = await hctx.coreFn();
180
123
  await next();
181
124
  }, { name: "handler", priority: -Infinity });
182
- this.dispatchHook.tap((obs) => {
183
- this.emit({
184
- kind: "hook.step",
185
- hookName: obs.hookName,
186
- hookId: obs.hookId,
187
- runId: obs.runId,
188
- parentRunId: obs.parentRunId,
189
- stepId: obs.stepId,
190
- stepKind: obs.stepKind,
191
- stepName: obs.stepName,
192
- phase: obs.phase,
193
- durationMs: obs.durationMs,
194
- error: obs.error ? serializeError(obs.error) : undefined,
195
- appName: this.appName,
196
- ts: new Date().toISOString(),
197
- });
198
- });
199
125
  }
200
126
  /**
201
- * Subscribe to a framework event. Sugar over `runtime.frameworkEvents.on`
202
- * so plugins can write `runtime.onFramework(ActionDispatching, ...)`.
203
- * Returns an unsubscribe function.
127
+ * Tap into the canonical telemetry stream narrowed to forge's widened
128
+ * `Telemetry` union. Delegates to the base `Runtime.onTelemetry` but
129
+ * locks the listener's record type to forge's CQRS-aware shape so
130
+ * `rec.kind === "event.published"` narrowing works for consumers.
204
131
  */
205
- onFramework(event, handler, priority = 0) {
206
- return this.frameworkEvents.on(event, handler, priority);
132
+ onTelemetry(listener) {
133
+ return super.onTelemetry(listener);
134
+ }
135
+ offTelemetry(listener) {
136
+ super.offTelemetry(listener);
137
+ }
138
+ /**
139
+ * Register a dispatch middleware. Narrows the base `use(middleware)` to
140
+ * forge's tightened `DispatchMiddleware` shape so middleware authors
141
+ * see typed `action: ActionDefinition` / `ctx: HandlerContext`. The base
142
+ * runtime sees its `unknown`-typed shape; the cast is safe — every call
143
+ * site we control hands the same `(action, input, ctx)` triple forge's
144
+ * dispatcher pushes through `dispatchHook.run(...)`.
145
+ */
146
+ use(middleware) {
147
+ super.use(middleware);
207
148
  }
208
149
  /** Internal — createApp registers known external event names. */
209
150
  registerExternalEvent(eventName) {
@@ -213,67 +154,10 @@ export class Runtime {
213
154
  registerPublicEvent(eventName) {
214
155
  this.publicEventNames.add(eventName);
215
156
  }
216
- /**
217
- * Wire a hook's per-step tap into the canonical telemetry stream. After
218
- * calling this, every `.use()` / `.on()` step on the hook emits a
219
- * `kind: "hook.step"` record through `runtime.onTelemetry`, the same way
220
- * the built-in `forge.action.dispatch` hook does.
221
- *
222
- * createApp uses this for every per-plugin and per-module lifecycle hook;
223
- * plugin authors can call it on their own hooks if they want their
224
- * extension points surfaced in Studio + dev-logger + OTel for free.
225
- *
226
- * Idempotent at the source — calling twice on the same hook attaches two
227
- * tap subscribers; the framework only calls this once per hook it owns.
228
- */
229
- adoptHook(hook) {
230
- hook.tap((obs) => {
231
- this.emit({
232
- kind: "hook.step",
233
- hookName: obs.hookName,
234
- hookId: obs.hookId,
235
- runId: obs.runId,
236
- parentRunId: obs.parentRunId,
237
- stepId: obs.stepId,
238
- stepKind: obs.stepKind,
239
- stepName: obs.stepName,
240
- phase: obs.phase,
241
- durationMs: obs.durationMs,
242
- error: obs.error ? serializeError(obs.error) : undefined,
243
- appName: this.appName,
244
- ts: new Date().toISOString(),
245
- });
246
- });
247
- }
248
157
  /** Internal — createApp registers actor-transition hooks from plugins. */
249
158
  registerActorTransitionHook(hook) {
250
159
  this.actorTransitionHooks.push(hook);
251
160
  }
252
- /**
253
- * Register a dispatch middleware. Outermost first — the order you call
254
- * `use()` is the order layers wrap (first `use` is the outermost layer).
255
- * Middlewares run once per dispatch, outside the retry loop.
256
- *
257
- * Internally this attaches to the `forge.action.dispatch` hook as a
258
- * chain step. The legacy `(next, action, input, ctx)` shape is adapted
259
- * to a `ChainFn<DispatchHookCtx>` without surface change for callers.
260
- */
261
- use(middleware) {
262
- // Each user middleware gets a distinct priority so the chain order
263
- // matches registration order. We start from 0 and DECREMENT — the
264
- // first registered middleware ends up with the highest priority
265
- // (= outermost), and the pinned "handler" step at -Infinity stays
266
- // strictly innermost.
267
- const priority = -this.userMiddlewareCount;
268
- this.userMiddlewareCount += 1;
269
- this.dispatchHook.use(async (hctx, next) => {
270
- hctx.result = await middleware(async () => {
271
- await next();
272
- return hctx.result;
273
- }, hctx.action, hctx.input, hctx.ctx);
274
- }, { name: middleware.name || `middleware#${this.userMiddlewareCount}`, priority });
275
- }
276
- userMiddlewareCount = 0;
277
161
  // ─── Registration ────────────────────────────────────────────────
278
162
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
163
  registerHandler(handler) {
@@ -524,9 +408,6 @@ export class Runtime {
524
408
  getProjectionStore() {
525
409
  return this.projectionStore;
526
410
  }
527
- getContainer() {
528
- return this.container;
529
- }
530
411
  listHandlers() {
531
412
  return [...this.handlers.keys()];
532
413
  }
@@ -925,41 +806,6 @@ export class Runtime {
925
806
  }
926
807
  }
927
808
  }
928
- /**
929
- * Tap into the canonical telemetry stream. One subscriber sees every kind:
930
- * `action.dispatched` / `.completed` / `.failed`, `event.published`,
931
- * `actor.transitioned`, `projection.folded`, `reaction.fired` / `.failed`,
932
- * `query.executed`, `timer.scheduled` / `.fired`, `dlq.recorded`.
933
- *
934
- * Listeners run AFTER the corresponding lifecycle action commits — they
935
- * observe what actually happened, not in-flight intent. Throwing in a
936
- * listener is caught and logged; it never breaks domain dispatch.
937
- *
938
- * Returns an unsubscribe function.
939
- */
940
- onTelemetry(listener) {
941
- this.telemetryListeners.push(listener);
942
- return () => this.offTelemetry(listener);
943
- }
944
- offTelemetry(listener) {
945
- const i = this.telemetryListeners.indexOf(listener);
946
- if (i >= 0)
947
- this.telemetryListeners.splice(i, 1);
948
- }
949
- emit(record) {
950
- if (this.telemetryListeners.length === 0)
951
- return;
952
- for (const listener of this.telemetryListeners) {
953
- try {
954
- listener(record);
955
- }
956
- catch (err) {
957
- // Best-effort — don't let a bad listener break dispatch.
958
- // eslint-disable-next-line no-console
959
- console.error("Runtime.onTelemetry listener threw:", err);
960
- }
961
- }
962
- }
963
809
  /**
964
810
  * Apply an event that arrived from the cross-service bus. Same pipeline
965
811
  * as `publish` (actors → projections → workflows) but does NOT re-publish
@@ -1084,96 +930,109 @@ export class Runtime {
1084
930
  }
1085
931
  }
1086
932
  async applyEventToActorLocked(actor, key, tenant, event, candidateReactions, envelope) {
1087
- const existing = await this.actorStore.load(actor.name, key, tenant);
1088
- const instance = existing ?? createInitialInstance(actor, key, tenant);
1089
- const matching = candidateReactions.find((c) => c.state === instance.state);
1090
- if (!matching) {
1091
- // Actor is in a state that doesn't react to this event — silently
1092
- // skip. (Future: dead-letter / log; surfacing here is too noisy for
1093
- // events that fan out to many actors, only some of which match.)
1094
- return;
1095
- }
1096
- const stateConfig = actor.states[instance.state];
1097
- if (stateConfig?.final) {
1098
- // Defensive: a final state shouldn't have entries in eventIndex,
1099
- // but guard anyway.
1100
- return;
1101
- }
1102
- const partial = matching.reaction.assign
1103
- ? matching.reaction.assign(instance.data, event.payload)
1104
- : {};
1105
- const nextData = { ...instance.data, ...partial };
1106
- const nextStateName = matching.reaction.target ?? instance.state;
1107
- const nextStateConfig = actor.states[nextStateName];
1108
- if (!nextStateConfig) {
1109
- throw new Error(`Runtime: actor "${actor.name}" reaction targets undeclared state "${nextStateName}" from "${instance.state}".`);
1110
- }
1111
- // Schema validation on save invalid partial throw, atomically
1112
- // skip persistence, re-raise to caller.
1113
- const validated = actor.schema.parse(nextData);
1114
- const stateChanged = nextStateName !== instance.state;
1115
- const isNewActor = !existing;
1116
- // Timers are owned by a state, not the actor. Compute them on:
1117
- // - state change (cancel old, schedule new), or
1118
- // - actor creation (born into a state schedule its timers).
1119
- const nextTimers = stateChanged || isNewActor
1120
- ? this.computeTimersForState(actor, nextStateName, key)
1121
- : instance.activeTimers;
1122
- const nextInstance = {
1123
- actorName: actor.name,
1124
- key,
1125
- tenant,
1126
- state: nextStateName,
1127
- data: validated,
1128
- activeTimers: nextTimers,
1129
- };
1130
- await this.actorStore.save(nextInstance);
1131
- if (stateChanged) {
1132
- this.emit({
1133
- kind: "actor.transitioned",
1134
- actor: actor.name,
933
+ const maxOccRetries = 3;
934
+ for (let occAttempt = 0; occAttempt < maxOccRetries; occAttempt++) {
935
+ const existing = await this.actorStore.load(actor.name, key, tenant);
936
+ const instance = existing ?? createInitialInstance(actor, key, tenant);
937
+ const matching = candidateReactions.find((c) => c.state === instance.state);
938
+ if (!matching) {
939
+ // Actor is in a state that doesn't react to this event — silently
940
+ // skip. (Future: dead-letter / log; surfacing here is too noisy for
941
+ // events that fan out to many actors, only some of which match.)
942
+ return;
943
+ }
944
+ const stateConfig = actor.states[instance.state];
945
+ if (stateConfig?.final) {
946
+ // Defensive: a final state shouldn't have entries in eventIndex,
947
+ // but guard anyway.
948
+ return;
949
+ }
950
+ const partial = matching.reaction.assign
951
+ ? matching.reaction.assign(instance.data, event.payload)
952
+ : {};
953
+ const nextData = { ...instance.data, ...partial };
954
+ const nextStateName = matching.reaction.target ?? instance.state;
955
+ const nextStateConfig = actor.states[nextStateName];
956
+ if (!nextStateConfig) {
957
+ throw new Error(`Runtime: actor "${actor.name}" reaction targets undeclared state "${nextStateName}" from "${instance.state}".`);
958
+ }
959
+ // Schema validation on save — invalid partial → throw, atomically
960
+ // skip persistence, re-raise to caller.
961
+ const validated = actor.schema.parse(nextData);
962
+ const stateChanged = nextStateName !== instance.state;
963
+ const isNewActor = !existing;
964
+ // Timers are owned by a state, not the actor. Compute them on:
965
+ // - state change (cancel old, schedule new), or
966
+ // - actor creation (born into a state — schedule its timers).
967
+ const nextTimers = stateChanged || isNewActor
968
+ ? this.computeTimersForState(actor, nextStateName, key)
969
+ : instance.activeTimers;
970
+ const nextInstance = {
971
+ actorName: actor.name,
1135
972
  key,
1136
973
  tenant,
1137
- from: instance.state,
1138
- to: nextStateName,
1139
- triggeringEvent: event.eventName,
1140
- envelope,
1141
- appName: this.appName,
1142
- ts: new Date().toISOString(),
1143
- });
1144
- }
1145
- // Fire actor-transition hooks (registered by plugins). Hooks run AFTER
1146
- // the save so they observe committed state. Errors propagate — plugins
1147
- // are infrastructure; we want loud failures, not silent skips.
1148
- if (this.actorTransitionHooks.length > 0 && stateChanged) {
1149
- for (const hook of this.actorTransitionHooks) {
1150
- await hook(actor, key, instance.state, nextStateName, event, envelope);
974
+ state: nextStateName,
975
+ data: validated,
976
+ activeTimers: nextTimers,
977
+ version: instance.version,
978
+ };
979
+ try {
980
+ await this.actorStore.save(nextInstance, { expectedVersion: instance.version });
1151
981
  }
1152
- }
1153
- // Per-actor `actor.transition:<name>` hook named, observable in
1154
- // listHooks(), tap-able by plugins via runtime.ensureActorTransitionHook.
1155
- // Runs only on actual transitions (state changed) so observers don't
1156
- // see no-op events.
1157
- if (stateChanged) {
1158
- const perActorHook = this.perActorHooks.get(actor.name);
1159
- if (perActorHook) {
1160
- try {
1161
- await perActorHook.run({
1162
- actor,
1163
- key,
1164
- fromState: instance.state,
1165
- toState: nextStateName,
1166
- triggeringEvent: event,
1167
- envelope,
1168
- });
982
+ catch (err) {
983
+ if (err instanceof ActorVersionConflictError && occAttempt < maxOccRetries - 1) {
984
+ continue;
1169
985
  }
1170
- catch (err) {
1171
- loggerForEnvelope(this.logger, envelope).error(`actor.transition hook threw`, {
1172
- actor: actor.name,
1173
- error: err?.message,
1174
- });
986
+ throw err;
987
+ }
988
+ if (stateChanged) {
989
+ this.emit({
990
+ kind: "actor.transitioned",
991
+ actor: actor.name,
992
+ key,
993
+ tenant,
994
+ from: instance.state,
995
+ to: nextStateName,
996
+ triggeringEvent: event.eventName,
997
+ envelope,
998
+ appName: this.appName,
999
+ ts: new Date().toISOString(),
1000
+ });
1001
+ }
1002
+ // Fire actor-transition hooks (registered by plugins). Hooks run AFTER
1003
+ // the save so they observe committed state. Errors propagate — plugins
1004
+ // are infrastructure; we want loud failures, not silent skips.
1005
+ if (this.actorTransitionHooks.length > 0 && stateChanged) {
1006
+ for (const hook of this.actorTransitionHooks) {
1007
+ await hook(actor, key, instance.state, nextStateName, event, envelope);
1175
1008
  }
1176
1009
  }
1010
+ // Per-actor `actor.transition:<name>` hook — named, observable in
1011
+ // listHooks(), tap-able by plugins via runtime.ensureActorTransitionHook.
1012
+ // Runs only on actual transitions (state changed) so observers don't
1013
+ // see no-op events.
1014
+ if (stateChanged) {
1015
+ const perActorHook = this.perActorHooks.get(actor.name);
1016
+ if (perActorHook) {
1017
+ try {
1018
+ await perActorHook.run({
1019
+ actor,
1020
+ key,
1021
+ fromState: instance.state,
1022
+ toState: nextStateName,
1023
+ triggeringEvent: event,
1024
+ envelope,
1025
+ });
1026
+ }
1027
+ catch (err) {
1028
+ loggerForEnvelope(this.logger, envelope).error(`actor.transition hook threw`, {
1029
+ actor: actor.name,
1030
+ error: err?.message,
1031
+ });
1032
+ }
1033
+ }
1034
+ }
1035
+ return;
1177
1036
  }
1178
1037
  }
1179
1038
  computeTimersForState(actor, stateName, actorKey) {
@@ -1421,8 +1280,7 @@ export class Runtime {
1421
1280
  return envelope.messageId;
1422
1281
  },
1423
1282
  async request(action, input) {
1424
- const result = await self.dispatch(action, input, envelope, { signal: ctxSignal });
1425
- return result;
1283
+ return self.dispatch(action, input, envelope, { signal: ctxSignal });
1426
1284
  },
1427
1285
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1428
1286
  async query(