@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 +10 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +32 -18
- package/dist/scope.d.ts +2 -0
- package/dist/workflow.js +3 -1
- package/package.json +3 -2
- package/src/failure.ts +3 -0
- package/src/index.ts +24 -12
- package/src/scope.ts +8 -0
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
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
async (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 =
|
|
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
|
};
|
package/dist/scope.d.ts
ADDED
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
74
|
-
const
|
|
75
|
-
|
|
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 =
|
|
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
|
+
};
|