@iworkflow/temporal 0.4.0 → 0.5.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 +2 -1
- package/dist/connection.d.ts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +69 -21
- package/dist/workflow.js +9 -4
- package/package.json +2 -2
- package/src/connection.ts +4 -1
- package/src/failure.ts +2 -0
- package/src/index.ts +75 -20
- package/src/workflow.ts +10 -3
package/README.md
CHANGED
|
@@ -7,5 +7,6 @@ Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,worker,workflow,ac
|
|
|
7
7
|
- `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
|
|
8
8
|
`export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
|
|
9
9
|
|
|
10
|
-
`opts` (`TemporalOptions`): `address`, `namespace`, `tls`, `apiKey` — источник токена, меняется каждые `apiKeyRenewMs
|
|
10
|
+
`opts` (`TemporalOptions`): `address`, `namespace`, `tls`, `apiKey` — источник токена, меняется каждые `apiKeyRenewMs`;
|
|
11
|
+
`plugins` — плагины SDK воркерам и клиенту; `temporalClient({ lazy: true })` — соединение при первом вызове.
|
|
11
12
|
Без них — переменные `TEMPORAL_URL` (по умолчанию `127.0.0.1:17233`), `TEMPORAL_NAMESPACE`.
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { TLSConfig } from "@temporalio/client";
|
|
1
|
+
import type { ConnectionPlugin, TLSConfig } from "@temporalio/client";
|
|
2
|
+
import type { WorkerPlugin } from "@temporalio/worker";
|
|
2
3
|
/** Подключение к Temporal; не задано — TEMPORAL_URL и TEMPORAL_NAMESPACE из окружения. */
|
|
3
4
|
export interface TemporalOptions {
|
|
4
5
|
address?: string;
|
|
@@ -8,6 +9,8 @@ export interface TemporalOptions {
|
|
|
8
9
|
/** Источник токена; соединение берёт свежий каждые apiKeyRenewMs (по умолчанию 60 с). */
|
|
9
10
|
apiKey?: () => Promise<string>;
|
|
10
11
|
apiKeyRenewMs?: number;
|
|
12
|
+
/** Плагины SDK (OpenTelemetry и т. п.) — воркерам и клиенту. */
|
|
13
|
+
plugins?: (ConnectionPlugin & WorkerPlugin)[];
|
|
11
14
|
}
|
|
12
15
|
export declare const address: (o: TemporalOptions) => string;
|
|
13
16
|
export declare const namespace: (o: TemporalOptions) => string;
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,7 @@ export type { TemporalOptions };
|
|
|
8
8
|
export declare function temporalRuntime(workflowsPath: string, opts?: TemporalOptions & {
|
|
9
9
|
workflowBundlePath?: string;
|
|
10
10
|
}): EngineRuntime;
|
|
11
|
-
|
|
11
|
+
/** lazy — соединение при первом вызове: модуль поднимается и без Temporal. */
|
|
12
|
+
export declare function temporalClient(opts?: TemporalOptions & {
|
|
13
|
+
lazy?: boolean;
|
|
14
|
+
}): Promise<EngineClient>;
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Context } from "@temporalio/activity";
|
|
|
4
4
|
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
5
5
|
import {
|
|
6
6
|
bindStepRuntime,
|
|
7
|
+
checkPriority,
|
|
8
|
+
DEFAULT_PRIORITY,
|
|
7
9
|
exitSignal,
|
|
8
10
|
FlowError as FlowError2
|
|
9
11
|
} from "@iworkflow/core";
|
|
@@ -74,7 +76,8 @@ function toTemporal(e, nonRetryable) {
|
|
|
74
76
|
message: e.message,
|
|
75
77
|
type: e.type,
|
|
76
78
|
details: [e.details],
|
|
77
|
-
nonRetryable: nonRetryable || e.nonRetryable
|
|
79
|
+
nonRetryable: nonRetryable || e.nonRetryable,
|
|
80
|
+
...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}
|
|
78
81
|
});
|
|
79
82
|
if (isFlowError(e))
|
|
80
83
|
return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
|
|
@@ -99,18 +102,31 @@ var notFound = (id) => (e) => {
|
|
|
99
102
|
throw new FlowError2(`процесса ${id} нет`, { reason: "not-found" });
|
|
100
103
|
throw e;
|
|
101
104
|
};
|
|
102
|
-
var wrap = (h) => ({
|
|
105
|
+
var wrap = (h, ready = async () => {}) => ({
|
|
103
106
|
id: h.workflowId,
|
|
104
|
-
result:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
result: async () => {
|
|
108
|
+
await ready();
|
|
109
|
+
return h.result().catch((e) => {
|
|
110
|
+
throw e instanceof WorkflowFailedError && cause(e.cause) || e;
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
signal: async (topic, value) => {
|
|
114
|
+
await ready();
|
|
115
|
+
await h.signal(SIGNAL, { topic, value });
|
|
116
|
+
},
|
|
117
|
+
query: async (key) => {
|
|
118
|
+
await ready();
|
|
119
|
+
return await h.query(QUERY, key) ?? undefined;
|
|
120
|
+
},
|
|
109
121
|
async describe() {
|
|
122
|
+
await ready();
|
|
110
123
|
const d = await h.describe().catch(notFound(h.workflowId));
|
|
111
124
|
return { id: h.workflowId, status: STATUS[d.status.name] ?? "running", startedAt: d.startTime };
|
|
112
125
|
},
|
|
113
|
-
|
|
126
|
+
async terminate(reason) {
|
|
127
|
+
await ready();
|
|
128
|
+
await h.terminate(reason).catch(notFound(h.workflowId));
|
|
129
|
+
}
|
|
114
130
|
});
|
|
115
131
|
var withFailures = (activities) => Object.fromEntries(Object.entries(activities).map(([name, fn]) => [
|
|
116
132
|
name,
|
|
@@ -139,7 +155,8 @@ function temporalRuntime(workflowsPath, opts = {}) {
|
|
|
139
155
|
stopping = false;
|
|
140
156
|
bindStepRuntime({
|
|
141
157
|
heartbeat: async (details) => Context.current().heartbeat(details),
|
|
142
|
-
lastHeartbeat: async () => Context.current().info.heartbeatDetails
|
|
158
|
+
lastHeartbeat: async () => Context.current().info.heartbeatDetails,
|
|
159
|
+
attempt: () => Context.current().info.attempt
|
|
143
160
|
});
|
|
144
161
|
let c;
|
|
145
162
|
try {
|
|
@@ -152,7 +169,7 @@ function temporalRuntime(workflowsPath, opts = {}) {
|
|
|
152
169
|
stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
|
|
153
170
|
try {
|
|
154
171
|
const activities = withFailures(def.activities);
|
|
155
|
-
const base = { connection: c, namespace: namespace(opts) };
|
|
172
|
+
const base = { connection: c, namespace: namespace(opts), ...opts.plugins ? { plugins: opts.plugins } : {} };
|
|
156
173
|
const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
|
|
157
174
|
workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
|
|
158
175
|
for (const [pool, limit] of Object.entries(def.pools ?? {}))
|
|
@@ -205,21 +222,52 @@ function temporalRuntime(workflowsPath, opts = {}) {
|
|
|
205
222
|
};
|
|
206
223
|
}
|
|
207
224
|
async function temporalClient(opts = {}) {
|
|
208
|
-
|
|
209
|
-
|
|
225
|
+
let connection;
|
|
226
|
+
let stopRenew;
|
|
227
|
+
let ready = async () => {
|
|
228
|
+
return;
|
|
229
|
+
};
|
|
230
|
+
const plugins = opts.plugins ? { plugins: opts.plugins } : {};
|
|
231
|
+
if (opts.lazy) {
|
|
232
|
+
let token = "";
|
|
233
|
+
const apiKey = opts.apiKey;
|
|
234
|
+
let pending;
|
|
235
|
+
if (apiKey)
|
|
236
|
+
ready = async () => token || (token = await (pending ??= apiKey().finally(() => pending = undefined)));
|
|
237
|
+
connection = Connection.lazy({
|
|
238
|
+
...await connectOptions({ ...opts, apiKey: undefined }),
|
|
239
|
+
...apiKey ? { apiKey: () => token } : {},
|
|
240
|
+
...plugins
|
|
241
|
+
});
|
|
242
|
+
stopRenew = renewApiKey(opts, (t) => token = t);
|
|
243
|
+
} else {
|
|
244
|
+
connection = await Connection.connect({ ...await connectOptions(opts), ...plugins });
|
|
245
|
+
stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
|
|
246
|
+
}
|
|
210
247
|
const client = new Client({ connection, namespace: namespace(opts) });
|
|
248
|
+
const startOptions = (target, args, o) => ({
|
|
249
|
+
taskQueue: target.app,
|
|
250
|
+
workflowId: o.id,
|
|
251
|
+
args,
|
|
252
|
+
...o.timeoutMs ? { workflowExecutionTimeout: o.timeoutMs } : {},
|
|
253
|
+
priority: { priorityKey: checkPriority(o.priority) ?? DEFAULT_PRIORITY }
|
|
254
|
+
});
|
|
211
255
|
return {
|
|
212
|
-
async start(target, args,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
256
|
+
async start(target, args, o) {
|
|
257
|
+
await ready();
|
|
258
|
+
const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
|
|
259
|
+
return wrap(h, ready);
|
|
260
|
+
},
|
|
261
|
+
async signalWithStart(target, args, o, topic, value) {
|
|
262
|
+
await ready();
|
|
263
|
+
const h = await client.workflow.signalWithStart(target.flow, {
|
|
264
|
+
...startOptions(target, args, o),
|
|
265
|
+
signal: SIGNAL,
|
|
266
|
+
signalArgs: [{ topic, value }]
|
|
219
267
|
});
|
|
220
|
-
return wrap(h);
|
|
268
|
+
return wrap(h, ready);
|
|
221
269
|
},
|
|
222
|
-
handle: (id) => wrap(client.workflow.getHandle(id)),
|
|
270
|
+
handle: (id) => wrap(client.workflow.getHandle(id), ready),
|
|
223
271
|
close: () => (stopRenew(), connection.close())
|
|
224
272
|
};
|
|
225
273
|
}
|
package/dist/workflow.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
TemporalFailure,
|
|
16
16
|
workflowInfo
|
|
17
17
|
} from "@temporalio/workflow";
|
|
18
|
-
import { FlowError as FlowError2 } from "@iworkflow/core";
|
|
18
|
+
import { checkPriority, DEFAULT_PRIORITY, FlowError as FlowError2 } from "@iworkflow/core";
|
|
19
19
|
|
|
20
20
|
// src/failure.ts
|
|
21
21
|
import {
|
|
@@ -56,7 +56,8 @@ function toTemporal(e, nonRetryable) {
|
|
|
56
56
|
message: e.message,
|
|
57
57
|
type: e.type,
|
|
58
58
|
details: [e.details],
|
|
59
|
-
nonRetryable: nonRetryable || e.nonRetryable
|
|
59
|
+
nonRetryable: nonRetryable || e.nonRetryable,
|
|
60
|
+
...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}
|
|
60
61
|
});
|
|
61
62
|
if (isFlowError(e))
|
|
62
63
|
return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
|
|
@@ -71,7 +72,8 @@ var queryDef = defineQuery(QUERY);
|
|
|
71
72
|
var poolQueue = (app, pool) => `${app}@${pool}`;
|
|
72
73
|
var proxies = new Map;
|
|
73
74
|
function activitiesWith(opts = {}) {
|
|
74
|
-
const
|
|
75
|
+
const priority = checkPriority(opts.priority) ?? workflowInfo().priority?.priorityKey;
|
|
76
|
+
const key = JSON.stringify({ ...opts, priority });
|
|
75
77
|
let p = proxies.get(key);
|
|
76
78
|
if (!p) {
|
|
77
79
|
p = proxyActivities({
|
|
@@ -79,9 +81,10 @@ function activitiesWith(opts = {}) {
|
|
|
79
81
|
startToCloseTimeout: opts.timeoutMs ?? 60000,
|
|
80
82
|
...opts.totalTimeoutMs ? { scheduleToCloseTimeout: opts.totalTimeoutMs } : {},
|
|
81
83
|
...opts.heartbeatTimeoutMs ? { heartbeatTimeout: opts.heartbeatTimeoutMs } : {},
|
|
84
|
+
...priority ? { priority: { priorityKey: priority } } : {},
|
|
82
85
|
retry: {
|
|
83
86
|
maximumAttempts: opts.attempts ?? 3,
|
|
84
|
-
initialInterval: opts.retryIntervalMs ?? 200,
|
|
87
|
+
initialInterval: Math.max(opts.retryIntervalMs ?? 200, 1),
|
|
85
88
|
backoffCoefficient: opts.backoff ?? 1,
|
|
86
89
|
...opts.maxRetryIntervalMs ? { maximumInterval: opts.maxRetryIntervalMs } : {},
|
|
87
90
|
...opts.nonRetryableTypes ? { nonRetryableErrorTypes: opts.nonRetryableTypes } : {}
|
|
@@ -116,6 +119,8 @@ function temporalFlow(flow, patches = []) {
|
|
|
116
119
|
workflowId: info.workflowId,
|
|
117
120
|
parentId: info.parent?.workflowId,
|
|
118
121
|
startedAt: info.startTime,
|
|
122
|
+
priority: info.priority?.priorityKey || DEFAULT_PRIORITY,
|
|
123
|
+
now: async () => Date.now(),
|
|
119
124
|
step: (name, stepArgs, opts) => activitiesWith(opts)[name](...stepArgs).catch(rethrow),
|
|
120
125
|
child: (target, childArgs, opts) => executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow),
|
|
121
126
|
async startChild(target, childArgs, opts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iworkflow/temporal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@temporalio/activity": "^1.24.0",
|
package/src/connection.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Подключение к Temporal: адрес, TLS, токен с заменой на месте — общее для воркера и клиента.
|
|
2
|
-
import type { TLSConfig } from "@temporalio/client";
|
|
2
|
+
import type { ConnectionPlugin, TLSConfig } from "@temporalio/client";
|
|
3
|
+
import type { WorkerPlugin } from "@temporalio/worker";
|
|
3
4
|
|
|
4
5
|
/** Подключение к Temporal; не задано — TEMPORAL_URL и TEMPORAL_NAMESPACE из окружения. */
|
|
5
6
|
export interface TemporalOptions {
|
|
@@ -10,6 +11,8 @@ export interface TemporalOptions {
|
|
|
10
11
|
/** Источник токена; соединение берёт свежий каждые apiKeyRenewMs (по умолчанию 60 с). */
|
|
11
12
|
apiKey?: () => Promise<string>;
|
|
12
13
|
apiKeyRenewMs?: number;
|
|
14
|
+
/** Плагины SDK (OpenTelemetry и т. п.) — воркерам и клиенту. */
|
|
15
|
+
plugins?: (ConnectionPlugin & WorkerPlugin)[];
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export const address = (o: TemporalOptions) => o.address ?? process.env.TEMPORAL_URL ?? "127.0.0.1:17233";
|
package/src/failure.ts
CHANGED
|
@@ -41,6 +41,8 @@ export function toTemporal(e: unknown, nonRetryable: boolean): unknown {
|
|
|
41
41
|
type: e.type,
|
|
42
42
|
details: [e.details],
|
|
43
43
|
nonRetryable: nonRetryable || e.nonRetryable,
|
|
44
|
+
// 0 сервер считает «не задано» и берёт паузу политики; 1 мс — «сразу», как у DBOS.
|
|
45
|
+
...(e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}),
|
|
44
46
|
});
|
|
45
47
|
if (isFlowError(e))
|
|
46
48
|
return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { Context } from "@temporalio/activity";
|
|
|
4
4
|
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
5
5
|
import {
|
|
6
6
|
bindStepRuntime,
|
|
7
|
+
checkPriority,
|
|
8
|
+
DEFAULT_PRIORITY,
|
|
7
9
|
exitSignal,
|
|
8
10
|
FlowError,
|
|
9
11
|
type Activity,
|
|
@@ -11,7 +13,9 @@ import {
|
|
|
11
13
|
type EngineClient,
|
|
12
14
|
type EngineRuntime,
|
|
13
15
|
type FlowHandle,
|
|
16
|
+
type FlowRef,
|
|
14
17
|
type FlowStatus,
|
|
18
|
+
type StartOptions,
|
|
15
19
|
} from "@iworkflow/core";
|
|
16
20
|
import { address, connectOptions, namespace, renewApiKey, type TemporalOptions } from "./connection";
|
|
17
21
|
import { cause, toTemporal } from "./failure";
|
|
@@ -38,19 +42,32 @@ const notFound = (id: string) => (e: unknown): never => {
|
|
|
38
42
|
throw e;
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
/** ready — токен ленивого клиента: без него SDK уйдёт с пустым ключом. */
|
|
46
|
+
const wrap = <T>(h: WorkflowHandle, ready: () => Promise<unknown> = async () => {}): FlowHandle<T> => ({
|
|
42
47
|
id: h.workflowId,
|
|
43
|
-
result: () =>
|
|
44
|
-
|
|
48
|
+
result: async () => {
|
|
49
|
+
await ready();
|
|
50
|
+
return h.result().catch((e) => {
|
|
45
51
|
throw (e instanceof WorkflowFailedError && cause(e.cause)) || e;
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
signal: async (topic, value) => {
|
|
55
|
+
await ready();
|
|
56
|
+
await h.signal(SIGNAL, { topic, value });
|
|
57
|
+
},
|
|
58
|
+
query: async <V>(key: string) => {
|
|
59
|
+
await ready();
|
|
60
|
+
return ((await h.query(QUERY, key)) ?? undefined) as V | undefined;
|
|
61
|
+
},
|
|
49
62
|
async describe() {
|
|
63
|
+
await ready();
|
|
50
64
|
const d = await h.describe().catch(notFound(h.workflowId));
|
|
51
65
|
return { id: h.workflowId, status: STATUS[d.status.name] ?? "running", startedAt: d.startTime };
|
|
52
66
|
},
|
|
53
|
-
|
|
67
|
+
async terminate(reason) {
|
|
68
|
+
await ready();
|
|
69
|
+
await h.terminate(reason).catch(notFound(h.workflowId));
|
|
70
|
+
},
|
|
54
71
|
});
|
|
55
72
|
|
|
56
73
|
/** Шаги отдают Failure как ApplicationFailure: тип, детали и nonRetryable доезжают до процесса. */
|
|
@@ -92,6 +109,7 @@ export function temporalRuntime(
|
|
|
92
109
|
bindStepRuntime({
|
|
93
110
|
heartbeat: async (details) => Context.current().heartbeat(details),
|
|
94
111
|
lastHeartbeat: async () => Context.current().info.heartbeatDetails,
|
|
112
|
+
attempt: () => Context.current().info.attempt,
|
|
95
113
|
});
|
|
96
114
|
let c: NativeConnection;
|
|
97
115
|
try {
|
|
@@ -105,7 +123,7 @@ export function temporalRuntime(
|
|
|
105
123
|
// Воркер не создался (нет бандла, namespace) — соединение и таймер токена закрываются.
|
|
106
124
|
try {
|
|
107
125
|
const activities = withFailures(def.activities);
|
|
108
|
-
const base = { connection: c, namespace: namespace(opts) };
|
|
126
|
+
const base = { connection: c, namespace: namespace(opts), ...(opts.plugins ? { plugins: opts.plugins } : {}) };
|
|
109
127
|
const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
|
|
110
128
|
workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
|
|
111
129
|
// Пул — своя очередь с пределом одновременных шагов в этом процессе.
|
|
@@ -162,22 +180,59 @@ export function temporalRuntime(
|
|
|
162
180
|
};
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
/** lazy — соединение при первом вызове: модуль поднимается и без Temporal. */
|
|
184
|
+
export async function temporalClient(opts: TemporalOptions & { lazy?: boolean } = {}): Promise<EngineClient> {
|
|
185
|
+
let connection: Connection;
|
|
186
|
+
let stopRenew: () => void;
|
|
187
|
+
let ready = async (): Promise<unknown> => undefined;
|
|
188
|
+
const plugins = opts.plugins ? { plugins: opts.plugins } : {};
|
|
189
|
+
if (opts.lazy) {
|
|
190
|
+
// Ни Temporal, ни источник токена не нужны для старта: ключ — функцией, берётся при первом вызове.
|
|
191
|
+
let token = "";
|
|
192
|
+
const apiKey = opts.apiKey;
|
|
193
|
+
// Неудача не залипает: следующий вызов спросит токен снова.
|
|
194
|
+
// Один запрос на всех ждущих; неудача не залипает: следующий вызов спросит снова.
|
|
195
|
+
let pending: Promise<string> | undefined;
|
|
196
|
+
if (apiKey)
|
|
197
|
+
ready = async () =>
|
|
198
|
+
token ||
|
|
199
|
+
(token = await (pending ??= apiKey().finally(() => (pending = undefined))));
|
|
200
|
+
connection = Connection.lazy({
|
|
201
|
+
...(await connectOptions({ ...opts, apiKey: undefined })),
|
|
202
|
+
...(apiKey ? { apiKey: () => token } : {}),
|
|
203
|
+
...plugins,
|
|
204
|
+
});
|
|
205
|
+
stopRenew = renewApiKey(opts, (t) => (token = t));
|
|
206
|
+
} else {
|
|
207
|
+
connection = await Connection.connect({ ...(await connectOptions(opts)), ...plugins });
|
|
208
|
+
stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
|
|
209
|
+
}
|
|
210
|
+
// Плагины — у соединения: оно само передаёт их клиенту.
|
|
168
211
|
const client = new Client({ connection, namespace: namespace(opts) });
|
|
212
|
+
const startOptions = (target: FlowRef, args: unknown[], o: StartOptions) => ({
|
|
213
|
+
taskQueue: target.app,
|
|
214
|
+
workflowId: o.id,
|
|
215
|
+
args,
|
|
216
|
+
...(o.timeoutMs ? { workflowExecutionTimeout: o.timeoutMs } : {}),
|
|
217
|
+
// Явно и по умолчанию: ctx.priority и очередь видят одно и то же, как у DBOS.
|
|
218
|
+
priority: { priorityKey: checkPriority(o.priority) ?? DEFAULT_PRIORITY },
|
|
219
|
+
});
|
|
169
220
|
return {
|
|
170
|
-
async start(target, args,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
221
|
+
async start(target, args, o) {
|
|
222
|
+
await ready();
|
|
223
|
+
const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
|
|
224
|
+
return wrap(h, ready);
|
|
225
|
+
},
|
|
226
|
+
async signalWithStart(target, args, o, topic, value) {
|
|
227
|
+
await ready();
|
|
228
|
+
const h = await client.workflow.signalWithStart(target.flow, {
|
|
229
|
+
...startOptions(target, args, o),
|
|
230
|
+
signal: SIGNAL,
|
|
231
|
+
signalArgs: [{ topic, value }],
|
|
177
232
|
});
|
|
178
|
-
return wrap(h);
|
|
233
|
+
return wrap(h, ready);
|
|
179
234
|
},
|
|
180
|
-
handle: (id) => wrap(client.workflow.getHandle(id)),
|
|
235
|
+
handle: (id) => wrap(client.workflow.getHandle(id), ready),
|
|
181
236
|
close: () => (stopRenew(), connection.close()),
|
|
182
237
|
};
|
|
183
238
|
}
|
package/src/workflow.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
workflowInfo,
|
|
19
19
|
} from "@temporalio/workflow";
|
|
20
20
|
|
|
21
|
-
import { FlowError, type ChildOptions, type Flow, type FlowContext, type StepOptions } from "@iworkflow/core";
|
|
21
|
+
import { checkPriority, DEFAULT_PRIORITY, FlowError, type ChildOptions, type Flow, type FlowContext, type StepOptions } from "@iworkflow/core";
|
|
22
22
|
import { fromTemporal, toTemporal } from "./failure";
|
|
23
23
|
|
|
24
24
|
export const SIGNAL = "iwf.signal";
|
|
@@ -32,7 +32,9 @@ export const poolQueue = (app: string, pool: string) => `${app}@${pool}`;
|
|
|
32
32
|
const proxies = new Map<string, Record<string, (...a: unknown[]) => Promise<unknown>>>();
|
|
33
33
|
|
|
34
34
|
function activitiesWith(opts: StepOptions = {}) {
|
|
35
|
-
|
|
35
|
+
// Шаг без своего приоритета — с приоритетом процесса, как у Temporal по умолчанию.
|
|
36
|
+
const priority = checkPriority(opts.priority) ?? workflowInfo().priority?.priorityKey;
|
|
37
|
+
const key = JSON.stringify({ ...opts, priority });
|
|
36
38
|
let p = proxies.get(key);
|
|
37
39
|
if (!p) {
|
|
38
40
|
p = proxyActivities({
|
|
@@ -40,9 +42,11 @@ function activitiesWith(opts: StepOptions = {}) {
|
|
|
40
42
|
startToCloseTimeout: opts.timeoutMs ?? 60_000,
|
|
41
43
|
...(opts.totalTimeoutMs ? { scheduleToCloseTimeout: opts.totalTimeoutMs } : {}),
|
|
42
44
|
...(opts.heartbeatTimeoutMs ? { heartbeatTimeout: opts.heartbeatTimeoutMs } : {}),
|
|
45
|
+
...(priority ? { priority: { priorityKey: priority } } : {}),
|
|
43
46
|
retry: {
|
|
44
47
|
maximumAttempts: opts.attempts ?? 3,
|
|
45
|
-
|
|
48
|
+
// 0 Temporal не принимает (ValueError — вечный повтор задачи процесса): 1 мс — «сразу», как у DBOS.
|
|
49
|
+
initialInterval: Math.max(opts.retryIntervalMs ?? 200, 1),
|
|
46
50
|
backoffCoefficient: opts.backoff ?? 1,
|
|
47
51
|
...(opts.maxRetryIntervalMs ? { maximumInterval: opts.maxRetryIntervalMs } : {}),
|
|
48
52
|
...(opts.nonRetryableTypes ? { nonRetryableErrorTypes: opts.nonRetryableTypes } : {}),
|
|
@@ -83,6 +87,9 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
|
|
|
83
87
|
workflowId: info.workflowId,
|
|
84
88
|
parentId: info.parent?.workflowId,
|
|
85
89
|
startedAt: info.startTime,
|
|
90
|
+
priority: info.priority?.priorityKey || DEFAULT_PRIORITY,
|
|
91
|
+
// Date.now() в песочнице Temporal детерминирован: время задачи процесса.
|
|
92
|
+
now: async () => Date.now(),
|
|
86
93
|
step: (name, stepArgs, opts) => activitiesWith(opts)[name]!(...stepArgs).catch(rethrow) as Promise<never>,
|
|
87
94
|
child: (target, childArgs, opts) =>
|
|
88
95
|
executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow) as Promise<never>,
|