@iworkflow/temporal 0.1.0 → 0.3.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/README.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,worker,workflow,activity}` ^1.24.
4
4
 
5
- - `temporalRuntime(workflowsPath).start(def)` — воркер на task queue `def.app`.
6
- - `temporalClient()` — старт, сигнал, query по id.
5
+ - `temporalRuntime(workflowsPath, opts?).start(def)` — воркер на task queue `def.app`; `opts.workflowBundlePath` — готовый бандл прод-сборки.
6
+ - `temporalClient(opts?)` — старт, сигнал, query по id.
7
7
  - `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
8
8
  `export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
9
9
 
10
- Переменные: `TEMPORAL_URL` (по умолчанию `127.0.0.1:17233`), `TEMPORAL_NAMESPACE`.
10
+ `opts` (`TemporalOptions`): `address`, `namespace`, `tls`, `apiKey` — источник токена, меняется каждые `apiKeyRenewMs`.
11
+ Без них — переменные `TEMPORAL_URL` (по умолчанию `127.0.0.1:17233`), `TEMPORAL_NAMESPACE`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/temporal",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Temporal adapter for @iworkflow/core",
5
5
  "keywords": [
6
6
  "workflow",
@@ -29,17 +29,19 @@
29
29
  "test": "bun test"
30
30
  },
31
31
  "dependencies": {
32
- "@iworkflow/core": "^0.1.0"
32
+ "@iworkflow/core": "^0.3.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@temporalio/activity": "^1.24.0",
36
36
  "@temporalio/client": "^1.24.0",
37
+ "@temporalio/common": "^1.24.0",
37
38
  "@temporalio/worker": "^1.24.0",
38
39
  "@temporalio/workflow": "^1.24.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@temporalio/activity": "^1.24.0",
42
43
  "@temporalio/client": "^1.24.0",
44
+ "@temporalio/common": "^1.24.0",
43
45
  "@temporalio/worker": "^1.24.0",
44
46
  "@temporalio/workflow": "^1.24.0"
45
47
  }
@@ -0,0 +1,34 @@
1
+ import { expect, test } from "bun:test";
2
+ import { connectOptions, renewApiKey } from "./connection";
3
+
4
+ test("токен берётся при подключении и меняется по таймеру", async () => {
5
+ let n = 0;
6
+ const apiKey = async () => `t${++n}`;
7
+ expect(await connectOptions({ address: "a:1", tls: true, apiKey })).toEqual({ address: "a:1", tls: true, apiKey: "t1" });
8
+ const got: string[] = [];
9
+ const stop = renewApiKey({ apiKey, apiKeyRenewMs: 10 }, (t) => got.push(t));
10
+ while (got.length < 2) await Bun.sleep(5);
11
+ stop();
12
+ const seen = got.length;
13
+ await Bun.sleep(25);
14
+ expect(seen).toBeGreaterThanOrEqual(2);
15
+ expect(got.length).toBe(seen);
16
+ });
17
+
18
+ test("без токена и TLS — только адрес", async () => {
19
+ expect(await connectOptions({ address: "a:1" })).toEqual({ address: "a:1" });
20
+ });
21
+
22
+ test("медленный запрос не затирает свежий токен, после остановки токен не ставится", async () => {
23
+ let release!: (t: string) => void;
24
+ let calls = 0;
25
+ const apiKey = () => (++calls === 1 ? new Promise<string>((r) => (release = r)) : Promise.resolve(`t${calls}`));
26
+ const got: string[] = [];
27
+ const stop = renewApiKey({ apiKey, apiKeyRenewMs: 5 }, (t) => got.push(t));
28
+ await Bun.sleep(40);
29
+ expect(calls).toBe(1);
30
+ stop();
31
+ release("old");
32
+ await Bun.sleep(5);
33
+ expect(got).toEqual([]);
34
+ });
@@ -0,0 +1,45 @@
1
+ // Подключение к Temporal: адрес, TLS, токен с заменой на месте — общее для воркера и клиента.
2
+ import type { TLSConfig } from "@temporalio/client";
3
+
4
+ /** Подключение к Temporal; не задано — TEMPORAL_URL и TEMPORAL_NAMESPACE из окружения. */
5
+ export interface TemporalOptions {
6
+ address?: string;
7
+ namespace?: string;
8
+ /** TLSConfig SDK (serverNameOverride, свой CA, mTLS). С apiKey SDK включает TLS сам — без TLS только `false`. */
9
+ tls?: boolean | TLSConfig;
10
+ /** Источник токена; соединение берёт свежий каждые apiKeyRenewMs (по умолчанию 60 с). */
11
+ apiKey?: () => Promise<string>;
12
+ apiKeyRenewMs?: number;
13
+ }
14
+
15
+ export const address = (o: TemporalOptions) => o.address ?? process.env.TEMPORAL_URL ?? "127.0.0.1:17233";
16
+ export const namespace = (o: TemporalOptions) => o.namespace ?? process.env.TEMPORAL_NAMESPACE ?? "default";
17
+
18
+ /** Токен меняется на месте: иначе воркер однажды молча перестанет брать задачи. */
19
+ export function renewApiKey(o: TemporalOptions, set: (token: string) => unknown): () => void {
20
+ if (!o.apiKey) return () => {};
21
+ const apiKey = o.apiKey;
22
+ // По одному запросу за раз: поздний ответ не затрёт свежий токен, после остановки токен не ставится.
23
+ let busy = false;
24
+ let stopped = false;
25
+ const timer = setInterval(() => {
26
+ if (busy) return;
27
+ busy = true;
28
+ apiKey()
29
+ .then((t) => (stopped ? undefined : set(t)))
30
+ .catch((e: unknown) => stopped || console.error("токен Temporal не обновлён:", e))
31
+ .finally(() => (busy = false));
32
+ }, o.apiKeyRenewMs ?? 60_000);
33
+ timer.unref?.();
34
+ return () => {
35
+ stopped = true;
36
+ clearInterval(timer);
37
+ };
38
+ }
39
+
40
+ export const connectOptions = async (o: TemporalOptions) => ({
41
+ address: address(o),
42
+ ...(o.tls !== undefined ? { tls: o.tls } : {}),
43
+ ...(o.apiKey ? { apiKey: await o.apiKey() } : {}),
44
+ });
45
+
package/src/failure.ts ADDED
@@ -0,0 +1,48 @@
1
+ // Отказы Temporal ↔ ошибки порта. Общее для песочницы и клиента: только @temporalio/common.
2
+ import {
3
+ ActivityFailure,
4
+ ApplicationFailure,
5
+ ChildWorkflowFailure,
6
+ TerminatedFailure,
7
+ TimeoutFailure,
8
+ WorkflowExecutionAlreadyStartedError,
9
+ } from "@temporalio/common";
10
+ import { Failure, FlowError, isFlowError } from "@iworkflow/core";
11
+
12
+ /** Отказ шага, дочернего процесса или процесса целиком — FlowError; прочее (отмена) — как есть. */
13
+ export function fromTemporal(e: unknown): unknown {
14
+ if (e instanceof WorkflowExecutionAlreadyStartedError)
15
+ return new FlowError(`процесс ${e.workflowId} уже есть`, { reason: "already-started" });
16
+ if (e instanceof ActivityFailure) {
17
+ const exhausted = e.retryState === "MAXIMUM_ATTEMPTS_REACHED";
18
+ const f = cause(e.cause, exhausted);
19
+ // Срок шага целиком (scheduleToClose) Temporal отдаёт последней ошибкой попытки, а не TimeoutFailure.
20
+ if (f && e.retryState === "TIMEOUT") return new FlowError(f.message, { reason: "timeout", type: f.type, details: f.details });
21
+ return f ?? e;
22
+ }
23
+ if (e instanceof ChildWorkflowFailure) return cause(e.cause, false) ?? e;
24
+ return e;
25
+ }
26
+
27
+ /** Причина отказа процесса или шага — FlowError; undefined — причина не наша (отмена и т. п.). */
28
+ export function cause(c: Error | undefined, exhausted = false): FlowError | undefined {
29
+ if (c instanceof TimeoutFailure) return new FlowError(c.message || "срок вышел", { reason: "timeout", exhausted });
30
+ if (c instanceof TerminatedFailure) return new FlowError(c.message || "процесс прерван", { reason: "terminated" });
31
+ if (c instanceof ApplicationFailure)
32
+ return new FlowError(c.message, { reason: "failed", type: c.type ?? undefined, details: c.details?.[0], exhausted });
33
+ return undefined;
34
+ }
35
+
36
+ /** Отказ из кода порта — ApplicationFailure: обычная ошибка в песочнице не роняет процесс, а повторяет задачу. */
37
+ export function toTemporal(e: unknown, nonRetryable: boolean): unknown {
38
+ if (e instanceof Failure)
39
+ return ApplicationFailure.create({
40
+ message: e.message,
41
+ type: e.type,
42
+ details: [e.details],
43
+ nonRetryable: nonRetryable || e.nonRetryable,
44
+ });
45
+ if (isFlowError(e))
46
+ return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
47
+ return e;
48
+ }
package/src/index.ts CHANGED
@@ -1,55 +1,156 @@
1
1
  // Temporal: воркер на очереди модуля (task queue = имя модуля), клиент — по имени процесса.
2
- import { Client, Connection, type WorkflowHandle } from "@temporalio/client";
2
+ import { Client, Connection, WorkflowFailedError, WorkflowNotFoundError, type WorkflowHandle } from "@temporalio/client";
3
3
  import { Context } from "@temporalio/activity";
4
4
  import { NativeConnection, Worker } from "@temporalio/worker";
5
- import { bindStepRuntime, type AppDefinition, type EngineClient, type EngineRuntime, type FlowHandle } from "@iworkflow/core";
5
+ import {
6
+ bindStepRuntime,
7
+ FlowError,
8
+ type Activity,
9
+ type AppDefinition,
10
+ type EngineClient,
11
+ type EngineRuntime,
12
+ type FlowHandle,
13
+ type FlowStatus,
14
+ } from "@iworkflow/core";
15
+ import { address, connectOptions, namespace, renewApiKey, type TemporalOptions } from "./connection";
16
+ import { cause, toTemporal } from "./failure";
17
+
18
+ export type { TemporalOptions };
6
19
 
7
20
  // Те же имена, что в workflow.ts: его нельзя импортировать вне песочницы.
8
21
  const SIGNAL = "iwf.signal";
9
22
  const QUERY = "iwf.query";
23
+ const poolQueue = (app: string, pool: string) => `${app}@${pool}`;
24
+
25
+ const STATUS: Record<string, FlowStatus> = {
26
+ RUNNING: "running",
27
+ COMPLETED: "completed",
28
+ FAILED: "failed",
29
+ CANCELLED: "terminated",
30
+ TERMINATED: "terminated",
31
+ TIMED_OUT: "timed-out",
32
+ CONTINUED_AS_NEW: "running",
33
+ };
34
+
35
+ const notFound = (id: string) => (e: unknown): never => {
36
+ if (e instanceof WorkflowNotFoundError) throw new FlowError(`процесса ${id} нет`, { reason: "not-found" });
37
+ throw e;
38
+ };
10
39
 
11
40
  const wrap = <T>(h: WorkflowHandle): FlowHandle<T> => ({
12
41
  id: h.workflowId,
13
- result: () => h.result(),
42
+ result: () =>
43
+ h.result().catch((e) => {
44
+ throw (e instanceof WorkflowFailedError && cause(e.cause)) || e;
45
+ }),
14
46
  signal: (topic, value) => h.signal(SIGNAL, { topic, value }),
15
47
  query: async <V>(key: string) => ((await h.query(QUERY, key)) ?? undefined) as V | undefined,
48
+ async describe() {
49
+ const d = await h.describe().catch(notFound(h.workflowId));
50
+ return { id: h.workflowId, status: STATUS[d.status.name] ?? "running", startedAt: d.startTime };
51
+ },
52
+ terminate: async (reason) => void (await h.terminate(reason).catch(notFound(h.workflowId))),
16
53
  });
17
54
 
18
- const address = () => process.env.TEMPORAL_URL ?? "127.0.0.1:17233";
19
- const namespace = () => process.env.TEMPORAL_NAMESPACE ?? "default";
55
+ /** Шаги отдают Failure как ApplicationFailure: тип, детали и nonRetryable доезжают до процесса. */
56
+ const withFailures = (activities: Record<string, Activity>) =>
57
+ Object.fromEntries(
58
+ Object.entries(activities).map(([name, fn]) => [
59
+ name,
60
+ async (...args: unknown[]) => {
61
+ try {
62
+ return await fn(...args);
63
+ } catch (e) {
64
+ throw toTemporal(e, false);
65
+ }
66
+ },
67
+ ]),
68
+ );
20
69
 
21
- /** workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса. */
22
- export function temporalRuntime(workflowsPath: string): EngineRuntime {
23
- let worker: Worker | undefined;
24
- let running: Promise<void> | undefined;
70
+ /**
71
+ * workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса.
72
+ * workflowBundlePath — готовый бандл того же входа (прод-сборка); с ним воркер не собирает код сам.
73
+ */
74
+ export function temporalRuntime(
75
+ workflowsPath: string,
76
+ opts: TemporalOptions & { workflowBundlePath?: string } = {},
77
+ ): EngineRuntime {
78
+ let workers: Worker[] = [];
79
+ let running: Promise<void>[] = [];
25
80
  let conn: NativeConnection | undefined;
81
+ let starting = false;
82
+ let stopRenew = () => {};
26
83
  return {
27
84
  async start(def: AppDefinition) {
85
+ if (starting) throw new Error("воркер уже запущен");
86
+ starting = true;
28
87
  bindStepRuntime({
29
88
  heartbeat: async (details) => Context.current().heartbeat(details),
30
89
  lastHeartbeat: async () => Context.current().info.heartbeatDetails,
31
90
  });
32
- conn = await NativeConnection.connect({ address: address() });
33
- worker = await Worker.create({
34
- connection: conn,
35
- namespace: namespace(),
36
- taskQueue: def.app,
37
- workflowsPath,
38
- activities: def.activities,
39
- });
40
- running = worker.run();
91
+ let c: NativeConnection;
92
+ try {
93
+ c = conn = await NativeConnection.connect(await connectOptions(opts));
94
+ } catch (e) {
95
+ starting = false;
96
+ throw e;
97
+ }
98
+ stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
99
+ // Воркер не создался (нет бандла, namespace) — соединение и таймер токена закрываются.
100
+ try {
101
+ const activities = withFailures(def.activities);
102
+ const base = { connection: c, namespace: namespace(opts) };
103
+ const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
104
+ workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
105
+ // Пул — своя очередь с пределом одновременных шагов в этом процессе.
106
+ for (const [pool, limit] of Object.entries(def.pools ?? {}))
107
+ workers.push(
108
+ await Worker.create({
109
+ ...base,
110
+ taskQueue: poolQueue(def.app, pool),
111
+ activities,
112
+ maxConcurrentActivityTaskExecutions: limit,
113
+ }),
114
+ );
115
+ } catch (e) {
116
+ // Созданный, но не запущенный воркер держит соединение: запустить и сразу погасить.
117
+ await Promise.allSettled(workers.map(async (w) => {
118
+ const r = w.run();
119
+ w.shutdown();
120
+ await r;
121
+ }));
122
+ stopRenew();
123
+ await c.close().catch(() => {});
124
+ conn = undefined;
125
+ workers = [];
126
+ starting = false;
127
+ throw e;
128
+ }
129
+ running = workers.map((w) => w.run());
41
130
  },
42
131
  async stop() {
43
- worker?.shutdown();
44
- await running;
45
- await conn?.close();
132
+ for (const w of workers) if (w.getState() === "RUNNING") w.shutdown();
133
+ // Упавший воркер не мешает закрыть соединение и таймер токена.
134
+ const done = await Promise.allSettled(running);
135
+ stopRenew();
136
+ try {
137
+ await conn?.close();
138
+ } finally {
139
+ conn = undefined;
140
+ workers = [];
141
+ running = [];
142
+ starting = false;
143
+ }
144
+ const failed = done.find((r) => r.status === "rejected");
145
+ if (failed) throw (failed as PromiseRejectedResult).reason;
46
146
  },
47
147
  };
48
148
  }
49
149
 
50
- export async function temporalClient(): Promise<EngineClient> {
51
- const connection = await Connection.connect({ address: address() });
52
- const client = new Client({ connection, namespace: namespace() });
150
+ export async function temporalClient(opts: TemporalOptions = {}): Promise<EngineClient> {
151
+ const connection = await Connection.connect(await connectOptions(opts));
152
+ const stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
153
+ const client = new Client({ connection, namespace: namespace(opts) });
53
154
  return {
54
155
  async start(target, args, opts) {
55
156
  const h = await client.workflow.start(target.flow, {
@@ -57,10 +158,11 @@ export async function temporalClient(): Promise<EngineClient> {
57
158
  workflowId: opts.id,
58
159
  args,
59
160
  workflowIdConflictPolicy: "USE_EXISTING",
161
+ ...(opts.timeoutMs ? { workflowExecutionTimeout: opts.timeoutMs } : {}),
60
162
  });
61
163
  return wrap(h);
62
164
  },
63
165
  handle: (id) => wrap(client.workflow.getHandle(id)),
64
- close: () => connection.close(),
166
+ close: () => (stopRenew(), connection.close()),
65
167
  };
66
168
  }
package/src/workflow.ts CHANGED
@@ -8,36 +8,63 @@ import {
8
8
  deprecatePatch,
9
9
  executeChild,
10
10
  getExternalWorkflowHandle,
11
+ ParentClosePolicy,
11
12
  patched,
12
13
  proxyActivities,
13
14
  setHandler,
14
15
  sleep,
16
+ startChild,
17
+ TemporalFailure,
15
18
  workflowInfo,
16
19
  } from "@temporalio/workflow";
17
20
 
18
- import type { Flow, FlowContext, StepOptions } from "@iworkflow/core";
21
+ import { FlowError, type ChildOptions, type Flow, type FlowContext, type StepOptions } from "@iworkflow/core";
22
+ import { fromTemporal, toTemporal } from "./failure";
19
23
 
20
24
  export const SIGNAL = "iwf.signal";
21
25
  export const QUERY = "iwf.query";
22
26
  const signalDef = defineSignal<[{ topic: string; value: unknown }]>(SIGNAL);
23
27
  const queryDef = defineQuery<unknown, [string]>(QUERY);
24
28
 
29
+ /** Очередь пула шагов модуля; воркер пула поднимает temporalRuntime. */
30
+ export const poolQueue = (app: string, pool: string) => `${app}@${pool}`;
31
+
25
32
  const proxies = new Map<string, Record<string, (...a: unknown[]) => Promise<unknown>>>();
26
33
 
27
34
  function activitiesWith(opts: StepOptions = {}) {
28
- const key = `${opts.attempts ?? 3}/${opts.timeoutMs ?? 60_000}/${opts.heartbeatTimeoutMs ?? 0}`;
35
+ const key = JSON.stringify(opts);
29
36
  let p = proxies.get(key);
30
37
  if (!p) {
31
38
  p = proxyActivities({
39
+ ...(opts.pool ? { taskQueue: poolQueue(workflowInfo().taskQueue, opts.pool) } : {}),
32
40
  startToCloseTimeout: opts.timeoutMs ?? 60_000,
41
+ ...(opts.totalTimeoutMs ? { scheduleToCloseTimeout: opts.totalTimeoutMs } : {}),
33
42
  ...(opts.heartbeatTimeoutMs ? { heartbeatTimeout: opts.heartbeatTimeoutMs } : {}),
34
- retry: { maximumAttempts: opts.attempts ?? 3, initialInterval: "200 ms", backoffCoefficient: 1 },
43
+ retry: {
44
+ maximumAttempts: opts.attempts ?? 3,
45
+ initialInterval: opts.retryIntervalMs ?? 200,
46
+ backoffCoefficient: opts.backoff ?? 1,
47
+ ...(opts.maxRetryIntervalMs ? { maximumInterval: opts.maxRetryIntervalMs } : {}),
48
+ ...(opts.nonRetryableTypes ? { nonRetryableErrorTypes: opts.nonRetryableTypes } : {}),
49
+ },
35
50
  });
36
51
  proxies.set(key, p);
37
52
  }
38
53
  return p;
39
54
  }
40
55
 
56
+ const childOptions = (app: string, args: unknown[], opts: ChildOptions) => ({
57
+ taskQueue: app,
58
+ workflowId: opts.id,
59
+ args,
60
+ ...(opts.timeoutMs ? { workflowExecutionTimeout: opts.timeoutMs } : {}),
61
+ ...(opts.onExisting === "reject" ? { workflowIdReusePolicy: "REJECT_DUPLICATE" as const } : {}),
62
+ });
63
+
64
+ const rethrow = (e: unknown): never => {
65
+ throw fromTemporal(e);
66
+ };
67
+
41
68
  /** patches — как AppDefinition.patches: необъявленный id — ошибка, как у DBOS. */
42
69
  export function temporalFlow(flow: Flow, patches: string[] = []) {
43
70
  return async (...args: unknown[]) => {
@@ -51,17 +78,34 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
51
78
  });
52
79
  setHandler(queryDef, (key) => state.get(key));
53
80
 
81
+ const info = workflowInfo();
54
82
  const ctx: FlowContext = {
55
- workflowId: workflowInfo().workflowId,
56
- step: (name, stepArgs, opts) => activitiesWith(opts)[name]!(...stepArgs) as Promise<never>,
83
+ workflowId: info.workflowId,
84
+ parentId: info.parent?.workflowId,
85
+ startedAt: info.startTime,
86
+ step: (name, stepArgs, opts) => activitiesWith(opts)[name]!(...stepArgs).catch(rethrow) as Promise<never>,
57
87
  child: (target, childArgs, opts) =>
58
- executeChild(target.flow, { taskQueue: target.app, workflowId: opts.id, args: childArgs }),
88
+ executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow) as Promise<never>,
89
+ async startChild(target, childArgs, opts) {
90
+ // REJECT_DUPLICATE: завершённый id тоже занят — как у DBOS, где id не переиспользуется.
91
+ const h = await startChild(target.flow, {
92
+ ...childOptions(target.app, childArgs, { ...opts, onExisting: "reject" }),
93
+ parentClosePolicy: ParentClosePolicy.ABANDON,
94
+ }).catch(rethrow);
95
+ return { id: opts.id, result: () => h.result().catch(rethrow) as Promise<never> };
96
+ },
59
97
  sleep: (ms) => sleep(ms),
60
98
  async recv<T>(topic: string, timeoutMs: number) {
61
99
  const ready = await condition(() => (inbox.get(topic)?.length ?? 0) > 0, timeoutMs);
62
100
  return ready ? (inbox.get(topic)!.shift() as T) : null;
63
101
  },
64
- signal: (workflowId, topic, value) => getExternalWorkflowHandle(workflowId).signal(signalDef, { topic, value }),
102
+ signal: (workflowId, topic, value) =>
103
+ getExternalWorkflowHandle(workflowId)
104
+ .signal(signalDef, { topic, value })
105
+ .catch((e) => {
106
+ if (!(e instanceof TemporalFailure)) throw e;
107
+ throw new FlowError(`процесса ${workflowId} нет или он завершён`, { reason: "not-found" });
108
+ }),
65
109
  async setState(key, value) {
66
110
  state.set(key, value);
67
111
  },
@@ -71,6 +115,10 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
71
115
  },
72
116
  deprecatePatch: async (id) => deprecatePatch(id),
73
117
  };
74
- return flow(ctx, ...args);
118
+ try {
119
+ return await flow(ctx, ...args);
120
+ } catch (e) {
121
+ throw toTemporal(e, true);
122
+ }
75
123
  };
76
124
  }