@iworkflow/temporal 0.7.0 → 0.9.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 +4 -1
- package/dist/index.js +13 -2
- package/dist/workflow.d.ts +12 -2
- package/dist/workflow.js +25 -4
- package/package.json +2 -2
- package/src/index.ts +13 -2
- package/src/workflow.ts +42 -6
package/README.md
CHANGED
|
@@ -14,7 +14,10 @@ Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,common,worker,work
|
|
|
14
14
|
- `temporalStep(fn)` — тест шага без воркера: `new MockActivityEnvironment().run(() => temporalStep(() => acts.send(x)))`;
|
|
15
15
|
`acts` — сырые шаги (не результат `temporalActivities`), отказ остаётся `Failure`; `@temporalio/testing` ставит сам модуль.
|
|
16
16
|
- `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
|
|
17
|
-
`export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
|
|
17
|
+
`export const outreach = temporalFlow(flows.outreach, ["send-twice"])`. Внешний контракт по именам Temporal (соседи
|
|
18
|
+
шлют сигнал и query без порта): `temporalFlow(flow, { patches, signals: ["interviewFinished"], queries: ["link"] })`.
|
|
19
|
+
Новое имя в `signals` у процесса, который уже идёт, меняет переигрывание (сигнал, лежавший без обработчика, дойдёт до recv) —
|
|
20
|
+
добавлять вместе с первым кодом процесса или когда идущих нет.
|
|
18
21
|
|
|
19
22
|
`opts` (`TemporalOptions`): `address`, `namespace`, `tls`, `apiKey` — источник токена, меняется каждые `apiKeyRenewMs`;
|
|
20
23
|
`plugins` — плагины SDK воркерам и клиенту; `temporalClient({ lazy: true })` — соединение при первом вызове.
|
package/dist/index.js
CHANGED
|
@@ -141,7 +141,15 @@ var wrap = (h, ready = async () => {}) => ({
|
|
|
141
141
|
var TEMPORAL_STEP = {
|
|
142
142
|
heartbeat: async (details) => Context.current().heartbeat(details),
|
|
143
143
|
lastHeartbeat: async () => Context.current().info.heartbeatDetails,
|
|
144
|
-
attempt: () => Context.current().info.attempt
|
|
144
|
+
attempt: () => Context.current().info.attempt,
|
|
145
|
+
info: () => {
|
|
146
|
+
const { workflowExecution, activityId } = Context.current().info;
|
|
147
|
+
const { workflowId, runId } = workflowExecution ?? {};
|
|
148
|
+
if (!workflowId || !runId)
|
|
149
|
+
throw new Error("stepInfo: шаг вне процесса Temporal");
|
|
150
|
+
return { workflowId, runId, stepId: activityId };
|
|
151
|
+
},
|
|
152
|
+
signal: () => Context.current().cancellationSignal
|
|
145
153
|
};
|
|
146
154
|
var temporalStep = (fn) => stepScope(alsScope).run(TEMPORAL_STEP, fn);
|
|
147
155
|
function temporalActivities(activities) {
|
|
@@ -284,7 +292,10 @@ async function temporalClient(opts = {}) {
|
|
|
284
292
|
await ready();
|
|
285
293
|
const h = await client.workflow.start(target.flow, {
|
|
286
294
|
...startOptions(target, args, o),
|
|
287
|
-
...o.onExisting === "reject"
|
|
295
|
+
...o.onExisting === "reject" || o.reuseClosedId ? {
|
|
296
|
+
workflowIdConflictPolicy: "FAIL",
|
|
297
|
+
workflowIdReusePolicy: o.reuseClosedId ? "ALLOW_DUPLICATE" : "REJECT_DUPLICATE"
|
|
298
|
+
} : { workflowIdConflictPolicy: "USE_EXISTING" }
|
|
288
299
|
}).catch((e) => {
|
|
289
300
|
throw fromTemporal(e);
|
|
290
301
|
});
|
package/dist/workflow.d.ts
CHANGED
|
@@ -3,5 +3,15 @@ export declare const SIGNAL = "iwf.signal";
|
|
|
3
3
|
export declare const QUERY = "iwf.query";
|
|
4
4
|
/** Очередь пула шагов модуля; воркер пула поднимает temporalRuntime. */
|
|
5
5
|
export declare const poolQueue: (app: string, pool: string) => string;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
export interface TemporalFlowOptions {
|
|
7
|
+
/** Как AppDefinition.patches: необъявленный id — ошибка, как у DBOS. */
|
|
8
|
+
patches?: string[];
|
|
9
|
+
/** Сигналы Temporal по имени (внешний контракт): тема X, значение recv — signalValue(аргументы). */
|
|
10
|
+
signals?: string[];
|
|
11
|
+
/** Query Temporal по имени: query X — setState(X); не публиковалось — null. */
|
|
12
|
+
queries?: string[];
|
|
13
|
+
}
|
|
14
|
+
/** Аргументы именованного сигнала — значение recv: нет или null — true (не спутать с null срока), несколько — массив. */
|
|
15
|
+
export declare const signalValue: (a: unknown[]) => unknown;
|
|
16
|
+
/** Второй аргумент — patches списком или TemporalFlowOptions. */
|
|
17
|
+
export declare function temporalFlow(flow: Flow, opts?: string[] | TemporalFlowOptions): (...args: unknown[]) => Promise<unknown>;
|
package/dist/workflow.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
deprecatePatch,
|
|
7
7
|
executeChild,
|
|
8
8
|
getExternalWorkflowHandle,
|
|
9
|
+
log,
|
|
9
10
|
ParentClosePolicy,
|
|
10
11
|
patched,
|
|
11
12
|
proxyActivities,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
TemporalFailure,
|
|
16
17
|
workflowInfo
|
|
17
18
|
} from "@temporalio/workflow";
|
|
18
|
-
import { checkPriority, DEFAULT_PRIORITY, FlowError as FlowError2 } from "@iworkflow/core";
|
|
19
|
+
import { checkPriority, DEFAULT_PRIORITY, FlowError as FlowError2, plainAttrs } from "@iworkflow/core";
|
|
19
20
|
|
|
20
21
|
// src/failure.ts
|
|
21
22
|
import {
|
|
@@ -106,16 +107,34 @@ var childOptions = (app, args, opts) => ({
|
|
|
106
107
|
var rethrow = (e) => {
|
|
107
108
|
throw fromTemporal(e);
|
|
108
109
|
};
|
|
109
|
-
|
|
110
|
+
var checkName = (name, seen) => {
|
|
111
|
+
if (!name.trim() || name !== name.trim() || name.startsWith("iwf.") || name.startsWith("__") || seen.has(name))
|
|
112
|
+
throw new Error(`имя "${name}" пустое, повторено или занято портом/Temporal`);
|
|
113
|
+
seen.add(name);
|
|
114
|
+
return name;
|
|
115
|
+
};
|
|
116
|
+
var signalValue = (a) => a.length === 0 || a.length === 1 && a[0] == null ? true : a.length === 1 ? a[0] : a;
|
|
117
|
+
function temporalFlow(flow, opts = []) {
|
|
118
|
+
const { patches = [], signals = [], queries = [] } = Array.isArray(opts) ? { patches: opts } : opts;
|
|
119
|
+
const [sigSeen, qSeen] = [new Set, new Set];
|
|
120
|
+
const named = {
|
|
121
|
+
signals: signals.map((n) => [n, defineSignal(checkName(n, sigSeen))]),
|
|
122
|
+
queries: queries.map((n) => [n, defineQuery(checkName(n, qSeen))])
|
|
123
|
+
};
|
|
110
124
|
return async (...args) => {
|
|
111
125
|
const inbox = new Map;
|
|
112
126
|
const state = new Map;
|
|
113
|
-
|
|
127
|
+
const push = (topic, value) => {
|
|
114
128
|
const q = inbox.get(topic) ?? [];
|
|
115
129
|
q.push(value);
|
|
116
130
|
inbox.set(topic, q);
|
|
117
|
-
}
|
|
131
|
+
};
|
|
132
|
+
setHandler(signalDef, ({ topic, value }) => push(topic, value));
|
|
118
133
|
setHandler(queryDef, (key) => state.get(key));
|
|
134
|
+
for (const [name, def] of named.signals)
|
|
135
|
+
setHandler(def, (...a) => push(name, signalValue(a)));
|
|
136
|
+
for (const [name, def] of named.queries)
|
|
137
|
+
setHandler(def, () => state.get(name) ?? null);
|
|
119
138
|
const info = workflowInfo();
|
|
120
139
|
const ctx = {
|
|
121
140
|
workflowId: info.workflowId,
|
|
@@ -144,6 +163,7 @@ function temporalFlow(flow, patches = []) {
|
|
|
144
163
|
throw e;
|
|
145
164
|
throw new FlowError2(`процесса ${workflowId} нет или он завершён`, { reason: "not-found" });
|
|
146
165
|
}),
|
|
166
|
+
log: async (level, message, attrs) => log[level](message, plainAttrs(attrs)),
|
|
147
167
|
async setState(key, value) {
|
|
148
168
|
state.set(key, value);
|
|
149
169
|
},
|
|
@@ -165,5 +185,6 @@ export {
|
|
|
165
185
|
QUERY,
|
|
166
186
|
SIGNAL,
|
|
167
187
|
poolQueue,
|
|
188
|
+
signalValue,
|
|
168
189
|
temporalFlow
|
|
169
190
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iworkflow/temporal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.
|
|
44
|
+
"@iworkflow/core": "^0.9.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@temporalio/activity": "^1.24.0",
|
package/src/index.ts
CHANGED
|
@@ -78,6 +78,14 @@ const TEMPORAL_STEP: StepRuntime = {
|
|
|
78
78
|
heartbeat: async (details) => Context.current().heartbeat(details),
|
|
79
79
|
lastHeartbeat: async () => Context.current().info.heartbeatDetails,
|
|
80
80
|
attempt: () => Context.current().info.attempt,
|
|
81
|
+
info: () => {
|
|
82
|
+
// Пустой id как ключ идемпотентности склеил бы записи разных процессов — отказ.
|
|
83
|
+
const { workflowExecution, activityId } = Context.current().info;
|
|
84
|
+
const { workflowId, runId } = workflowExecution ?? {};
|
|
85
|
+
if (!workflowId || !runId) throw new Error("stepInfo: шаг вне процесса Temporal");
|
|
86
|
+
return { workflowId, runId, stepId: activityId };
|
|
87
|
+
},
|
|
88
|
+
signal: () => Context.current().cancellationSignal,
|
|
81
89
|
};
|
|
82
90
|
|
|
83
91
|
/** Один вызов шага в рантайме Temporal без обёртки отказов — для тестов в MockActivityEnvironment. */
|
|
@@ -250,8 +258,11 @@ export async function temporalClient(opts: TemporalOptions & { lazy?: boolean }
|
|
|
250
258
|
const h = await client.workflow
|
|
251
259
|
.start(target.flow, {
|
|
252
260
|
...startOptions(target, args, o),
|
|
253
|
-
...(o.onExisting === "reject"
|
|
254
|
-
? {
|
|
261
|
+
...(o.onExisting === "reject" || o.reuseClosedId
|
|
262
|
+
? {
|
|
263
|
+
workflowIdConflictPolicy: "FAIL" as const,
|
|
264
|
+
workflowIdReusePolicy: o.reuseClosedId ? ("ALLOW_DUPLICATE" as const) : ("REJECT_DUPLICATE" as const),
|
|
265
|
+
}
|
|
255
266
|
: { workflowIdConflictPolicy: "USE_EXISTING" as const }),
|
|
256
267
|
})
|
|
257
268
|
.catch((e) => {
|
package/src/workflow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Обёртка процесса для песочницы Temporal: FlowContext поверх proxyActivities и executeChild.
|
|
2
|
-
// Сигналы и query — по одному обработчику
|
|
2
|
+
// Сигналы и query порта — по одному обработчику (тема и ключ в данных); внешний контракт — по именам (signals/queries).
|
|
3
3
|
// Импортируется только из входа песочницы модуля: `@iworkflow/temporal/workflow`.
|
|
4
4
|
import {
|
|
5
5
|
condition,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
deprecatePatch,
|
|
9
9
|
executeChild,
|
|
10
10
|
getExternalWorkflowHandle,
|
|
11
|
+
log,
|
|
11
12
|
ParentClosePolicy,
|
|
12
13
|
patched,
|
|
13
14
|
proxyActivities,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
workflowInfo,
|
|
19
20
|
} from "@temporalio/workflow";
|
|
20
21
|
|
|
21
|
-
import { checkPriority, DEFAULT_PRIORITY, FlowError, type ChildOptions, type Flow, type FlowContext, type StepOptions } from "@iworkflow/core";
|
|
22
|
+
import { checkPriority, DEFAULT_PRIORITY, FlowError, plainAttrs, type ChildOptions, type Flow, type FlowContext, type StepOptions } from "@iworkflow/core";
|
|
22
23
|
import { fromTemporal, toTemporal } from "./failure";
|
|
23
24
|
|
|
24
25
|
export const SIGNAL = "iwf.signal";
|
|
@@ -69,18 +70,49 @@ const rethrow = (e: unknown): never => {
|
|
|
69
70
|
throw fromTemporal(e);
|
|
70
71
|
};
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
export interface TemporalFlowOptions {
|
|
74
|
+
/** Как AppDefinition.patches: необъявленный id — ошибка, как у DBOS. */
|
|
75
|
+
patches?: string[];
|
|
76
|
+
/** Сигналы Temporal по имени (внешний контракт): тема X, значение recv — signalValue(аргументы). */
|
|
77
|
+
signals?: string[];
|
|
78
|
+
/** Query Temporal по имени: query X — setState(X); не публиковалось — null. */
|
|
79
|
+
queries?: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** iwf.* — имена порта, __* — служебные Temporal: заняли бы чужой обработчик — отказ при объявлении. */
|
|
83
|
+
const checkName = (name: string, seen: Set<string>) => {
|
|
84
|
+
if (!name.trim() || name !== name.trim() || name.startsWith("iwf.") || name.startsWith("__") || seen.has(name))
|
|
85
|
+
throw new Error(`имя "${name}" пустое, повторено или занято портом/Temporal`);
|
|
86
|
+
seen.add(name);
|
|
87
|
+
return name;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Аргументы именованного сигнала — значение recv: нет или null — true (не спутать с null срока), несколько — массив. */
|
|
91
|
+
export const signalValue = (a: unknown[]): unknown => (a.length === 0 || (a.length === 1 && a[0] == null) ? true : a.length === 1 ? a[0] : a);
|
|
92
|
+
|
|
93
|
+
/** Второй аргумент — patches списком или TemporalFlowOptions. */
|
|
94
|
+
export function temporalFlow(flow: Flow, opts: string[] | TemporalFlowOptions = []) {
|
|
95
|
+
const { patches = [], signals = [], queries = [] } = Array.isArray(opts) ? { patches: opts } : opts;
|
|
96
|
+
// Имена сигналов и query — разные пространства Temporal: повтор ищется в каждом своём.
|
|
97
|
+
const [sigSeen, qSeen] = [new Set<string>(), new Set<string>()];
|
|
98
|
+
const named = {
|
|
99
|
+
signals: signals.map((n) => [n, defineSignal<unknown[]>(checkName(n, sigSeen))] as const),
|
|
100
|
+
queries: queries.map((n) => [n, defineQuery<unknown>(checkName(n, qSeen))] as const),
|
|
101
|
+
};
|
|
74
102
|
return async (...args: unknown[]) => {
|
|
75
103
|
// Буфер по темам: сигнал до recv ждёт в очереди, как в DBOS.
|
|
76
104
|
const inbox = new Map<string, unknown[]>();
|
|
77
105
|
const state = new Map<string, unknown>();
|
|
78
|
-
|
|
106
|
+
const push = (topic: string, value: unknown) => {
|
|
79
107
|
const q = inbox.get(topic) ?? [];
|
|
80
108
|
q.push(value);
|
|
81
109
|
inbox.set(topic, q);
|
|
82
|
-
}
|
|
110
|
+
};
|
|
111
|
+
setHandler(signalDef, ({ topic, value }) => push(topic, value));
|
|
83
112
|
setHandler(queryDef, (key) => state.get(key));
|
|
113
|
+
for (const [name, def] of named.signals)
|
|
114
|
+
setHandler(def, (...a: unknown[]) => push(name, signalValue(a)));
|
|
115
|
+
for (const [name, def] of named.queries) setHandler(def, () => state.get(name) ?? null);
|
|
84
116
|
|
|
85
117
|
const info = workflowInfo();
|
|
86
118
|
const ctx: FlowContext = {
|
|
@@ -105,6 +137,7 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
|
|
|
105
137
|
},
|
|
106
138
|
sleep: (ms) => sleep(ms),
|
|
107
139
|
async recv<T>(topic: string, timeoutMs: number) {
|
|
140
|
+
// 0 и меньше — таймер 1 мс (так было и до 0.8): отдаёт активацию, цикл опроса не виснет.
|
|
108
141
|
const ready = await condition(() => (inbox.get(topic)?.length ?? 0) > 0, timeoutMs);
|
|
109
142
|
return ready ? (inbox.get(topic)!.shift() as T) : null;
|
|
110
143
|
},
|
|
@@ -115,6 +148,9 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
|
|
|
115
148
|
if (!(e instanceof TemporalFailure)) throw e;
|
|
116
149
|
throw new FlowError(`процесса ${workflowId} нет или он завершён`, { reason: "not-found" });
|
|
117
150
|
}),
|
|
151
|
+
// log песочницы молчит при переигрывании.
|
|
152
|
+
// Неклонируемые attrs (функция, BigInt) уронили бы активацию при отправке из песочницы.
|
|
153
|
+
log: async (level, message, attrs) => log[level](message, plainAttrs(attrs)),
|
|
118
154
|
async setState(key, value) {
|
|
119
155
|
state.set(key, value);
|
|
120
156
|
},
|