@iworkflow/temporal 0.5.0 → 0.6.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/README.md CHANGED
@@ -1,9 +1,18 @@
1
1
  # @iworkflow/temporal
2
2
 
3
- Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,worker,workflow,activity}` ^1.24.
3
+ Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,common,worker,workflow,activity}` ^1.24.
4
+
5
+ Модуль объявляет в своих зависимостях **все пять** peer, даже если сам импортирует не все: сборка
6
+ сервера с `external: ["@temporalio/*"]` их не вкладывает, и без `@temporalio/common` под падает
7
+ на старте с `Cannot find module`.
4
8
 
5
9
  - `temporalRuntime(workflowsPath, opts?).start(def)` — воркер на task queue `def.app`; `opts.workflowBundlePath` — готовый бандл прод-сборки.
6
10
  - `temporalClient(opts?)` — старт, сигнал, query по id.
11
+ - `temporalActivities(activities)` — шаги порта для своего `Worker.create` (слив старой очереди, тестовый
12
+ воркер): `Failure` → `ApplicationFailure`, работают `heartbeat` и `stepAttempt`. Шаги без неё
13
+ в своём воркере `heartbeat`/`stepAttempt` не видят — с 0.6.0 рантайм шага ставится на время шага.
14
+ - `temporalStep(fn)` — тест шага без воркера: `new MockActivityEnvironment().run(() => temporalStep(() => acts.send(x)))`;
15
+ `acts` — сырые шаги (не результат `temporalActivities`), отказ остаётся `Failure`; `@temporalio/testing` ставит сам модуль.
7
16
  - `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
8
17
  `export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
9
18
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,13 @@
1
- import { type EngineClient, type EngineRuntime } from "@iworkflow/core";
1
+ import { type Activity, type EngineClient, type EngineRuntime } from "@iworkflow/core";
2
2
  import { type TemporalOptions } from "./connection";
3
3
  export type { TemporalOptions };
4
+ /** Один вызов шага в рантайме Temporal без обёртки отказов — для тестов в MockActivityEnvironment. */
5
+ export declare const temporalStep: <T>(fn: () => T) => T;
6
+ /**
7
+ * Шаги порта для своего Worker.create (слив старой очереди, тестовый воркер): Failure доезжает
8
+ * как ApplicationFailure, heartbeat и stepAttempt работают. temporalRuntime делает это сам.
9
+ */
10
+ export declare function temporalActivities<T extends Record<string, Activity>>(activities: T): T;
4
11
  /**
5
12
  * workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса.
6
13
  * workflowBundlePath — готовый бандл того же входа (прод-сборка); с ним воркер не собирает код сам.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Client, Connection, WorkflowFailedError, WorkflowNotFoundError } from "
3
3
  import { Context } from "@temporalio/activity";
4
4
  import { NativeConnection, Worker } from "@temporalio/worker";
5
5
  import {
6
- bindStepRuntime,
6
+ stepScope,
7
7
  checkPriority,
8
8
  DEFAULT_PRIORITY,
9
9
  exitSignal,
@@ -41,6 +41,7 @@ var connectOptions = async (o) => ({
41
41
  import {
42
42
  ActivityFailure,
43
43
  ApplicationFailure,
44
+ ApplicationFailureCategory,
44
45
  ChildWorkflowFailure,
45
46
  TerminatedFailure,
46
47
  TimeoutFailure,
@@ -77,13 +78,21 @@ function toTemporal(e, nonRetryable) {
77
78
  type: e.type,
78
79
  details: [e.details],
79
80
  nonRetryable: nonRetryable || e.nonRetryable,
80
- ...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}
81
+ ...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {},
82
+ ...e.benign && !nonRetryable ? { category: ApplicationFailureCategory.BENIGN } : {}
81
83
  });
82
84
  if (isFlowError(e))
83
85
  return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
84
86
  return e;
85
87
  }
86
88
 
89
+ // src/scope.ts
90
+ import { AsyncLocalStorage } from "node:async_hooks";
91
+ var alsScope = () => {
92
+ const als = new AsyncLocalStorage;
93
+ return { run: (rt, fn) => als.run(rt, fn), current: () => als.getStore() };
94
+ };
95
+
87
96
  // src/index.ts
88
97
  var SIGNAL = "iwf.signal";
89
98
  var QUERY = "iwf.query";
@@ -128,16 +137,24 @@ var wrap = (h, ready = async () => {}) => ({
128
137
  await h.terminate(reason).catch(notFound(h.workflowId));
129
138
  }
130
139
  });
131
- var withFailures = (activities) => Object.fromEntries(Object.entries(activities).map(([name, fn]) => [
132
- name,
133
- async (...args) => {
134
- try {
135
- return await fn(...args);
136
- } catch (e) {
137
- throw toTemporal(e, false);
140
+ var TEMPORAL_STEP = {
141
+ heartbeat: async (details) => Context.current().heartbeat(details),
142
+ lastHeartbeat: async () => Context.current().info.heartbeatDetails,
143
+ attempt: () => Context.current().info.attempt
144
+ };
145
+ var temporalStep = (fn) => stepScope(alsScope).run(TEMPORAL_STEP, fn);
146
+ function temporalActivities(activities) {
147
+ return Object.fromEntries(Object.entries(activities).map(([name, fn]) => [
148
+ name,
149
+ async (...args) => {
150
+ try {
151
+ return await temporalStep(() => fn(...args));
152
+ } catch (e) {
153
+ throw toTemporal(e, false);
154
+ }
138
155
  }
139
- }
140
- ]));
156
+ ]));
157
+ }
141
158
  function temporalRuntime(workflowsPath, opts = {}) {
142
159
  let workers = [];
143
160
  let running = [];
@@ -153,11 +170,6 @@ function temporalRuntime(workflowsPath, opts = {}) {
153
170
  starting = true;
154
171
  exit.arm();
155
172
  stopping = false;
156
- bindStepRuntime({
157
- heartbeat: async (details) => Context.current().heartbeat(details),
158
- lastHeartbeat: async () => Context.current().info.heartbeatDetails,
159
- attempt: () => Context.current().info.attempt
160
- });
161
173
  let c;
162
174
  try {
163
175
  c = conn = await NativeConnection.connect(await connectOptions(opts));
@@ -168,7 +180,7 @@ function temporalRuntime(workflowsPath, opts = {}) {
168
180
  }
169
181
  stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
170
182
  try {
171
- const activities = withFailures(def.activities);
183
+ const activities = temporalActivities(def.activities);
172
184
  const base = { connection: c, namespace: namespace(opts), ...opts.plugins ? { plugins: opts.plugins } : {} };
173
185
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
174
186
  workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
@@ -272,6 +284,8 @@ async function temporalClient(opts = {}) {
272
284
  };
273
285
  }
274
286
  export {
287
+ temporalActivities,
275
288
  temporalClient,
276
- temporalRuntime
289
+ temporalRuntime,
290
+ temporalStep
277
291
  };
@@ -0,0 +1,2 @@
1
+ import type { StepScope } from "@iworkflow/core";
2
+ export declare const alsScope: () => StepScope;
package/dist/workflow.js CHANGED
@@ -21,6 +21,7 @@ import { checkPriority, DEFAULT_PRIORITY, FlowError as FlowError2 } from "@iwork
21
21
  import {
22
22
  ActivityFailure,
23
23
  ApplicationFailure,
24
+ ApplicationFailureCategory,
24
25
  ChildWorkflowFailure,
25
26
  TerminatedFailure,
26
27
  TimeoutFailure,
@@ -57,7 +58,8 @@ function toTemporal(e, nonRetryable) {
57
58
  type: e.type,
58
59
  details: [e.details],
59
60
  nonRetryable: nonRetryable || e.nonRetryable,
60
- ...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}
61
+ ...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {},
62
+ ...e.benign && !nonRetryable ? { category: ApplicationFailureCategory.BENIGN } : {}
61
63
  });
62
64
  if (isFlowError(e))
63
65
  return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/temporal",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Temporal adapter for @iworkflow/core",
5
5
  "keywords": [
6
6
  "workflow",
@@ -41,7 +41,7 @@
41
41
  "prepack": "bun run build"
42
42
  },
43
43
  "dependencies": {
44
- "@iworkflow/core": "^0.5.0"
44
+ "@iworkflow/core": "^0.6.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@temporalio/activity": "^1.24.0",
@@ -54,6 +54,7 @@
54
54
  "@temporalio/activity": "^1.24.0",
55
55
  "@temporalio/client": "^1.24.0",
56
56
  "@temporalio/common": "^1.24.0",
57
+ "@temporalio/testing": "^1.24.0",
57
58
  "@temporalio/worker": "^1.24.0",
58
59
  "@temporalio/workflow": "^1.24.0"
59
60
  },
package/src/failure.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  ActivityFailure,
4
4
  ApplicationFailure,
5
+ ApplicationFailureCategory,
5
6
  ChildWorkflowFailure,
6
7
  TerminatedFailure,
7
8
  TimeoutFailure,
@@ -43,6 +44,8 @@ export function toTemporal(e: unknown, nonRetryable: boolean): unknown {
43
44
  nonRetryable: nonRetryable || e.nonRetryable,
44
45
  // 0 сервер считает «не задано» и берёт паузу политики; 1 мс — «сразу», как у DBOS.
45
46
  ...(e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}),
47
+ // BENIGN — только отказу шага: отказ процесса целиком штатным не бывает.
48
+ ...(e.benign && !nonRetryable ? { category: ApplicationFailureCategory.BENIGN } : {}),
46
49
  });
47
50
  if (isFlowError(e))
48
51
  return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
package/src/index.ts CHANGED
@@ -3,7 +3,8 @@ import { Client, Connection, WorkflowFailedError, WorkflowNotFoundError, type Wo
3
3
  import { Context } from "@temporalio/activity";
4
4
  import { NativeConnection, Worker } from "@temporalio/worker";
5
5
  import {
6
- bindStepRuntime,
6
+ stepScope,
7
+ type StepRuntime,
7
8
  checkPriority,
8
9
  DEFAULT_PRIORITY,
9
10
  exitSignal,
@@ -19,6 +20,7 @@ import {
19
20
  } from "@iworkflow/core";
20
21
  import { address, connectOptions, namespace, renewApiKey, type TemporalOptions } from "./connection";
21
22
  import { cause, toTemporal } from "./failure";
23
+ import { alsScope } from "./scope";
22
24
 
23
25
  export type { TemporalOptions };
24
26
 
@@ -70,20 +72,35 @@ const wrap = <T>(h: WorkflowHandle, ready: () => Promise<unknown> = async () =>
70
72
  },
71
73
  });
72
74
 
73
- /** Шаги отдают Failure как ApplicationFailure: тип, детали и nonRetryable доезжают до процесса. */
74
- const withFailures = (activities: Record<string, Activity>) =>
75
- Object.fromEntries(
75
+ /** Рантайм шага Temporal: всё из контекста активности. */
76
+ const TEMPORAL_STEP: StepRuntime = {
77
+ heartbeat: async (details) => Context.current().heartbeat(details),
78
+ lastHeartbeat: async () => Context.current().info.heartbeatDetails,
79
+ attempt: () => Context.current().info.attempt,
80
+ };
81
+
82
+ /** Один вызов шага в рантайме Temporal без обёртки отказов — для тестов в MockActivityEnvironment. */
83
+ // Область берётся при вызове, а не при импорте: пакет sideEffects: false.
84
+ export const temporalStep = <T>(fn: () => T): T => stepScope(alsScope).run(TEMPORAL_STEP, fn);
85
+
86
+ /**
87
+ * Шаги порта для своего Worker.create (слив старой очереди, тестовый воркер): Failure доезжает
88
+ * как ApplicationFailure, heartbeat и stepAttempt работают. temporalRuntime делает это сам.
89
+ */
90
+ export function temporalActivities<T extends Record<string, Activity>>(activities: T): T {
91
+ return Object.fromEntries(
76
92
  Object.entries(activities).map(([name, fn]) => [
77
93
  name,
78
94
  async (...args: unknown[]) => {
79
95
  try {
80
- return await fn(...args);
96
+ return await temporalStep(() => fn(...args));
81
97
  } catch (e) {
82
98
  throw toTemporal(e, false);
83
99
  }
84
100
  },
85
101
  ]),
86
- );
102
+ ) as T;
103
+ }
87
104
 
88
105
  /**
89
106
  * workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса.
@@ -106,11 +123,6 @@ export function temporalRuntime(
106
123
  starting = true;
107
124
  exit.arm();
108
125
  stopping = false;
109
- bindStepRuntime({
110
- heartbeat: async (details) => Context.current().heartbeat(details),
111
- lastHeartbeat: async () => Context.current().info.heartbeatDetails,
112
- attempt: () => Context.current().info.attempt,
113
- });
114
126
  let c: NativeConnection;
115
127
  try {
116
128
  c = conn = await NativeConnection.connect(await connectOptions(opts));
@@ -122,7 +134,7 @@ export function temporalRuntime(
122
134
  stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
123
135
  // Воркер не создался (нет бандла, namespace) — соединение и таймер токена закрываются.
124
136
  try {
125
- const activities = withFailures(def.activities);
137
+ const activities = temporalActivities(def.activities);
126
138
  const base = { connection: c, namespace: namespace(opts), ...(opts.plugins ? { plugins: opts.plugins } : {}) };
127
139
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
128
140
  workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
package/src/scope.ts ADDED
@@ -0,0 +1,8 @@
1
+ // Область шага на AsyncLocalStorage: stepScope ядра берёт её у первого адаптера процесса.
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import type { StepRuntime, StepScope } from "@iworkflow/core";
4
+
5
+ export const alsScope = (): StepScope => {
6
+ const als = new AsyncLocalStorage<StepRuntime>();
7
+ return { run: (rt, fn) => als.run(rt, fn), current: () => als.getStore() };
8
+ };