@iworkflow/temporal 0.5.0 → 0.6.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
@@ -1,9 +1,16 @@
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 рантайм шага ставится на время шага.
7
14
  - `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
8
15
  `export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
9
16
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
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
+ /**
5
+ * Шаги порта для своего Worker.create (слив старой очереди, тестовый воркер): Failure доезжает
6
+ * как ApplicationFailure, heartbeat и stepAttempt работают. temporalRuntime делает это сам.
7
+ */
8
+ export declare function temporalActivities<T extends Record<string, Activity>>(activities: T): T;
4
9
  /**
5
10
  * workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса.
6
11
  * 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
+ function temporalActivities(activities) {
146
+ const scope = stepScope(alsScope);
147
+ return Object.fromEntries(Object.entries(activities).map(([name, fn]) => [
148
+ name,
149
+ async (...args) => {
150
+ try {
151
+ return await scope.run(TEMPORAL_STEP, () => 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,7 @@ async function temporalClient(opts = {}) {
272
284
  };
273
285
  }
274
286
  export {
287
+ temporalActivities,
275
288
  temporalClient,
276
289
  temporalRuntime
277
290
  };
@@ -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.0",
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.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@temporalio/activity": "^1.24.0",
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,32 @@ 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
+ /**
83
+ * Шаги порта для своего Worker.create (слив старой очереди, тестовый воркер): Failure доезжает
84
+ * как ApplicationFailure, heartbeat и stepAttempt работают. temporalRuntime делает это сам.
85
+ */
86
+ export function temporalActivities<T extends Record<string, Activity>>(activities: T): T {
87
+ const scope = stepScope(alsScope);
88
+ return Object.fromEntries(
76
89
  Object.entries(activities).map(([name, fn]) => [
77
90
  name,
78
91
  async (...args: unknown[]) => {
79
92
  try {
80
- return await fn(...args);
93
+ return await scope.run(TEMPORAL_STEP, () => fn(...args));
81
94
  } catch (e) {
82
95
  throw toTemporal(e, false);
83
96
  }
84
97
  },
85
98
  ]),
86
- );
99
+ ) as T;
100
+ }
87
101
 
88
102
  /**
89
103
  * workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса.
@@ -106,11 +120,6 @@ export function temporalRuntime(
106
120
  starting = true;
107
121
  exit.arm();
108
122
  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
123
  let c: NativeConnection;
115
124
  try {
116
125
  c = conn = await NativeConnection.connect(await connectOptions(opts));
@@ -122,7 +131,7 @@ export function temporalRuntime(
122
131
  stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
123
132
  // Воркер не создался (нет бандла, namespace) — соединение и таймер токена закрываются.
124
133
  try {
125
- const activities = withFailures(def.activities);
134
+ const activities = temporalActivities(def.activities);
126
135
  const base = { connection: c, namespace: namespace(opts), ...(opts.plugins ? { plugins: opts.plugins } : {}) };
127
136
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
128
137
  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
+ };