@iworkflow/temporal 0.3.0 → 0.4.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/dist/connection.d.ts +20 -0
- package/dist/failure.d.ts +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +229 -0
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +160 -0
- package/package.json +21 -6
- package/src/index.ts +15 -0
- package/src/connection.test.ts +0 -34
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TLSConfig } from "@temporalio/client";
|
|
2
|
+
/** Подключение к Temporal; не задано — TEMPORAL_URL и TEMPORAL_NAMESPACE из окружения. */
|
|
3
|
+
export interface TemporalOptions {
|
|
4
|
+
address?: string;
|
|
5
|
+
namespace?: string;
|
|
6
|
+
/** TLSConfig SDK (serverNameOverride, свой CA, mTLS). С apiKey SDK включает TLS сам — без TLS только `false`. */
|
|
7
|
+
tls?: boolean | TLSConfig;
|
|
8
|
+
/** Источник токена; соединение берёт свежий каждые apiKeyRenewMs (по умолчанию 60 с). */
|
|
9
|
+
apiKey?: () => Promise<string>;
|
|
10
|
+
apiKeyRenewMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const address: (o: TemporalOptions) => string;
|
|
13
|
+
export declare const namespace: (o: TemporalOptions) => string;
|
|
14
|
+
/** Токен меняется на месте: иначе воркер однажды молча перестанет брать задачи. */
|
|
15
|
+
export declare function renewApiKey(o: TemporalOptions, set: (token: string) => unknown): () => void;
|
|
16
|
+
export declare const connectOptions: (o: TemporalOptions) => Promise<{
|
|
17
|
+
address: string;
|
|
18
|
+
tls?: boolean | TLSConfig | undefined;
|
|
19
|
+
apiKey?: string | undefined;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FlowError } from "@iworkflow/core";
|
|
2
|
+
/** Отказ шага, дочернего процесса или процесса целиком — FlowError; прочее (отмена) — как есть. */
|
|
3
|
+
export declare function fromTemporal(e: unknown): unknown;
|
|
4
|
+
/** Причина отказа процесса или шага — FlowError; undefined — причина не наша (отмена и т. п.). */
|
|
5
|
+
export declare function cause(c: Error | undefined, exhausted?: boolean): FlowError | undefined;
|
|
6
|
+
/** Отказ из кода порта — ApplicationFailure: обычная ошибка в песочнице не роняет процесс, а повторяет задачу. */
|
|
7
|
+
export declare function toTemporal(e: unknown, nonRetryable: boolean): unknown;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type EngineClient, type EngineRuntime } from "@iworkflow/core";
|
|
2
|
+
import { type TemporalOptions } from "./connection";
|
|
3
|
+
export type { TemporalOptions };
|
|
4
|
+
/**
|
|
5
|
+
* workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса.
|
|
6
|
+
* workflowBundlePath — готовый бандл того же входа (прод-сборка); с ним воркер не собирает код сам.
|
|
7
|
+
*/
|
|
8
|
+
export declare function temporalRuntime(workflowsPath: string, opts?: TemporalOptions & {
|
|
9
|
+
workflowBundlePath?: string;
|
|
10
|
+
}): EngineRuntime;
|
|
11
|
+
export declare function temporalClient(opts?: TemporalOptions): Promise<EngineClient>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Client, Connection, WorkflowFailedError, WorkflowNotFoundError } from "@temporalio/client";
|
|
3
|
+
import { Context } from "@temporalio/activity";
|
|
4
|
+
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
5
|
+
import {
|
|
6
|
+
bindStepRuntime,
|
|
7
|
+
exitSignal,
|
|
8
|
+
FlowError as FlowError2
|
|
9
|
+
} from "@iworkflow/core";
|
|
10
|
+
|
|
11
|
+
// src/connection.ts
|
|
12
|
+
var address = (o) => o.address ?? process.env.TEMPORAL_URL ?? "127.0.0.1:17233";
|
|
13
|
+
var namespace = (o) => o.namespace ?? process.env.TEMPORAL_NAMESPACE ?? "default";
|
|
14
|
+
function renewApiKey(o, set) {
|
|
15
|
+
if (!o.apiKey)
|
|
16
|
+
return () => {};
|
|
17
|
+
const apiKey = o.apiKey;
|
|
18
|
+
let busy = false;
|
|
19
|
+
let stopped = false;
|
|
20
|
+
const timer = setInterval(() => {
|
|
21
|
+
if (busy)
|
|
22
|
+
return;
|
|
23
|
+
busy = true;
|
|
24
|
+
apiKey().then((t) => stopped ? undefined : set(t)).catch((e) => stopped || console.error("токен Temporal не обновлён:", e)).finally(() => busy = false);
|
|
25
|
+
}, o.apiKeyRenewMs ?? 60000);
|
|
26
|
+
timer.unref?.();
|
|
27
|
+
return () => {
|
|
28
|
+
stopped = true;
|
|
29
|
+
clearInterval(timer);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
var connectOptions = async (o) => ({
|
|
33
|
+
address: address(o),
|
|
34
|
+
...o.tls !== undefined ? { tls: o.tls } : {},
|
|
35
|
+
...o.apiKey ? { apiKey: await o.apiKey() } : {}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// src/failure.ts
|
|
39
|
+
import {
|
|
40
|
+
ActivityFailure,
|
|
41
|
+
ApplicationFailure,
|
|
42
|
+
ChildWorkflowFailure,
|
|
43
|
+
TerminatedFailure,
|
|
44
|
+
TimeoutFailure,
|
|
45
|
+
WorkflowExecutionAlreadyStartedError
|
|
46
|
+
} from "@temporalio/common";
|
|
47
|
+
import { Failure, FlowError, isFlowError } from "@iworkflow/core";
|
|
48
|
+
function fromTemporal(e) {
|
|
49
|
+
if (e instanceof WorkflowExecutionAlreadyStartedError)
|
|
50
|
+
return new FlowError(`процесс ${e.workflowId} уже есть`, { reason: "already-started" });
|
|
51
|
+
if (e instanceof ActivityFailure) {
|
|
52
|
+
const exhausted = e.retryState === "MAXIMUM_ATTEMPTS_REACHED";
|
|
53
|
+
const f = cause(e.cause, exhausted);
|
|
54
|
+
if (f && e.retryState === "TIMEOUT")
|
|
55
|
+
return new FlowError(f.message, { reason: "timeout", type: f.type, details: f.details });
|
|
56
|
+
return f ?? e;
|
|
57
|
+
}
|
|
58
|
+
if (e instanceof ChildWorkflowFailure)
|
|
59
|
+
return cause(e.cause, false) ?? e;
|
|
60
|
+
return e;
|
|
61
|
+
}
|
|
62
|
+
function cause(c, exhausted = false) {
|
|
63
|
+
if (c instanceof TimeoutFailure)
|
|
64
|
+
return new FlowError(c.message || "срок вышел", { reason: "timeout", exhausted });
|
|
65
|
+
if (c instanceof TerminatedFailure)
|
|
66
|
+
return new FlowError(c.message || "процесс прерван", { reason: "terminated" });
|
|
67
|
+
if (c instanceof ApplicationFailure)
|
|
68
|
+
return new FlowError(c.message, { reason: "failed", type: c.type ?? undefined, details: c.details?.[0], exhausted });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
function toTemporal(e, nonRetryable) {
|
|
72
|
+
if (e instanceof Failure)
|
|
73
|
+
return ApplicationFailure.create({
|
|
74
|
+
message: e.message,
|
|
75
|
+
type: e.type,
|
|
76
|
+
details: [e.details],
|
|
77
|
+
nonRetryable: nonRetryable || e.nonRetryable
|
|
78
|
+
});
|
|
79
|
+
if (isFlowError(e))
|
|
80
|
+
return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
|
|
81
|
+
return e;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/index.ts
|
|
85
|
+
var SIGNAL = "iwf.signal";
|
|
86
|
+
var QUERY = "iwf.query";
|
|
87
|
+
var poolQueue = (app, pool) => `${app}@${pool}`;
|
|
88
|
+
var STATUS = {
|
|
89
|
+
RUNNING: "running",
|
|
90
|
+
COMPLETED: "completed",
|
|
91
|
+
FAILED: "failed",
|
|
92
|
+
CANCELLED: "terminated",
|
|
93
|
+
TERMINATED: "terminated",
|
|
94
|
+
TIMED_OUT: "timed-out",
|
|
95
|
+
CONTINUED_AS_NEW: "running"
|
|
96
|
+
};
|
|
97
|
+
var notFound = (id) => (e) => {
|
|
98
|
+
if (e instanceof WorkflowNotFoundError)
|
|
99
|
+
throw new FlowError2(`процесса ${id} нет`, { reason: "not-found" });
|
|
100
|
+
throw e;
|
|
101
|
+
};
|
|
102
|
+
var wrap = (h) => ({
|
|
103
|
+
id: h.workflowId,
|
|
104
|
+
result: () => h.result().catch((e) => {
|
|
105
|
+
throw e instanceof WorkflowFailedError && cause(e.cause) || e;
|
|
106
|
+
}),
|
|
107
|
+
signal: (topic, value) => h.signal(SIGNAL, { topic, value }),
|
|
108
|
+
query: async (key) => await h.query(QUERY, key) ?? undefined,
|
|
109
|
+
async describe() {
|
|
110
|
+
const d = await h.describe().catch(notFound(h.workflowId));
|
|
111
|
+
return { id: h.workflowId, status: STATUS[d.status.name] ?? "running", startedAt: d.startTime };
|
|
112
|
+
},
|
|
113
|
+
terminate: async (reason) => void await h.terminate(reason).catch(notFound(h.workflowId))
|
|
114
|
+
});
|
|
115
|
+
var withFailures = (activities) => Object.fromEntries(Object.entries(activities).map(([name, fn]) => [
|
|
116
|
+
name,
|
|
117
|
+
async (...args) => {
|
|
118
|
+
try {
|
|
119
|
+
return await fn(...args);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
throw toTemporal(e, false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]));
|
|
125
|
+
function temporalRuntime(workflowsPath, opts = {}) {
|
|
126
|
+
let workers = [];
|
|
127
|
+
let running = [];
|
|
128
|
+
let conn;
|
|
129
|
+
let starting = false;
|
|
130
|
+
let stopping = false;
|
|
131
|
+
const exit = exitSignal();
|
|
132
|
+
let stopRenew = () => {};
|
|
133
|
+
return {
|
|
134
|
+
async start(def) {
|
|
135
|
+
if (starting)
|
|
136
|
+
throw new Error("воркер уже запущен");
|
|
137
|
+
starting = true;
|
|
138
|
+
exit.arm();
|
|
139
|
+
stopping = false;
|
|
140
|
+
bindStepRuntime({
|
|
141
|
+
heartbeat: async (details) => Context.current().heartbeat(details),
|
|
142
|
+
lastHeartbeat: async () => Context.current().info.heartbeatDetails
|
|
143
|
+
});
|
|
144
|
+
let c;
|
|
145
|
+
try {
|
|
146
|
+
c = conn = await NativeConnection.connect(await connectOptions(opts));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
starting = false;
|
|
149
|
+
exit.reject(e);
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
|
|
153
|
+
try {
|
|
154
|
+
const activities = withFailures(def.activities);
|
|
155
|
+
const base = { connection: c, namespace: namespace(opts) };
|
|
156
|
+
const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
|
|
157
|
+
workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
|
|
158
|
+
for (const [pool, limit] of Object.entries(def.pools ?? {}))
|
|
159
|
+
workers.push(await Worker.create({
|
|
160
|
+
...base,
|
|
161
|
+
taskQueue: poolQueue(def.app, pool),
|
|
162
|
+
activities,
|
|
163
|
+
maxConcurrentActivityTaskExecutions: limit
|
|
164
|
+
}));
|
|
165
|
+
} catch (e) {
|
|
166
|
+
await Promise.allSettled(workers.map(async (w) => {
|
|
167
|
+
const r = w.run();
|
|
168
|
+
w.shutdown();
|
|
169
|
+
await r;
|
|
170
|
+
}));
|
|
171
|
+
stopRenew();
|
|
172
|
+
await c.close().catch(() => {});
|
|
173
|
+
conn = undefined;
|
|
174
|
+
workers = [];
|
|
175
|
+
starting = false;
|
|
176
|
+
exit.reject(e);
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
running = workers.map((w) => w.run());
|
|
180
|
+
Promise.all(running).then(() => stopping || exit.resolve(), () => {});
|
|
181
|
+
for (const r of running)
|
|
182
|
+
r.catch((e) => stopping || exit.reject(e));
|
|
183
|
+
},
|
|
184
|
+
done: () => exit.promise(),
|
|
185
|
+
async stop() {
|
|
186
|
+
stopping = true;
|
|
187
|
+
for (const w of workers)
|
|
188
|
+
if (w.getState() === "RUNNING")
|
|
189
|
+
w.shutdown();
|
|
190
|
+
const done = await Promise.allSettled(running);
|
|
191
|
+
stopRenew();
|
|
192
|
+
try {
|
|
193
|
+
await conn?.close();
|
|
194
|
+
} finally {
|
|
195
|
+
conn = undefined;
|
|
196
|
+
workers = [];
|
|
197
|
+
running = [];
|
|
198
|
+
starting = false;
|
|
199
|
+
exit.resolve();
|
|
200
|
+
}
|
|
201
|
+
const failed = done.find((r) => r.status === "rejected");
|
|
202
|
+
if (failed)
|
|
203
|
+
throw failed.reason;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async function temporalClient(opts = {}) {
|
|
208
|
+
const connection = await Connection.connect(await connectOptions(opts));
|
|
209
|
+
const stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
|
|
210
|
+
const client = new Client({ connection, namespace: namespace(opts) });
|
|
211
|
+
return {
|
|
212
|
+
async start(target, args, opts) {
|
|
213
|
+
const h = await client.workflow.start(target.flow, {
|
|
214
|
+
taskQueue: target.app,
|
|
215
|
+
workflowId: opts.id,
|
|
216
|
+
args,
|
|
217
|
+
workflowIdConflictPolicy: "USE_EXISTING",
|
|
218
|
+
...opts.timeoutMs ? { workflowExecutionTimeout: opts.timeoutMs } : {}
|
|
219
|
+
});
|
|
220
|
+
return wrap(h);
|
|
221
|
+
},
|
|
222
|
+
handle: (id) => wrap(client.workflow.getHandle(id)),
|
|
223
|
+
close: () => (stopRenew(), connection.close())
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export {
|
|
227
|
+
temporalClient,
|
|
228
|
+
temporalRuntime
|
|
229
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Flow } from "@iworkflow/core";
|
|
2
|
+
export declare const SIGNAL = "iwf.signal";
|
|
3
|
+
export declare const QUERY = "iwf.query";
|
|
4
|
+
/** Очередь пула шагов модуля; воркер пула поднимает temporalRuntime. */
|
|
5
|
+
export declare const poolQueue: (app: string, pool: string) => string;
|
|
6
|
+
/** patches — как AppDefinition.patches: необъявленный id — ошибка, как у DBOS. */
|
|
7
|
+
export declare function temporalFlow(flow: Flow, patches?: string[]): (...args: unknown[]) => Promise<unknown>;
|
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/workflow.ts
|
|
2
|
+
import {
|
|
3
|
+
condition,
|
|
4
|
+
defineQuery,
|
|
5
|
+
defineSignal,
|
|
6
|
+
deprecatePatch,
|
|
7
|
+
executeChild,
|
|
8
|
+
getExternalWorkflowHandle,
|
|
9
|
+
ParentClosePolicy,
|
|
10
|
+
patched,
|
|
11
|
+
proxyActivities,
|
|
12
|
+
setHandler,
|
|
13
|
+
sleep,
|
|
14
|
+
startChild,
|
|
15
|
+
TemporalFailure,
|
|
16
|
+
workflowInfo
|
|
17
|
+
} from "@temporalio/workflow";
|
|
18
|
+
import { FlowError as FlowError2 } from "@iworkflow/core";
|
|
19
|
+
|
|
20
|
+
// src/failure.ts
|
|
21
|
+
import {
|
|
22
|
+
ActivityFailure,
|
|
23
|
+
ApplicationFailure,
|
|
24
|
+
ChildWorkflowFailure,
|
|
25
|
+
TerminatedFailure,
|
|
26
|
+
TimeoutFailure,
|
|
27
|
+
WorkflowExecutionAlreadyStartedError
|
|
28
|
+
} from "@temporalio/common";
|
|
29
|
+
import { Failure, FlowError, isFlowError } from "@iworkflow/core";
|
|
30
|
+
function fromTemporal(e) {
|
|
31
|
+
if (e instanceof WorkflowExecutionAlreadyStartedError)
|
|
32
|
+
return new FlowError(`процесс ${e.workflowId} уже есть`, { reason: "already-started" });
|
|
33
|
+
if (e instanceof ActivityFailure) {
|
|
34
|
+
const exhausted = e.retryState === "MAXIMUM_ATTEMPTS_REACHED";
|
|
35
|
+
const f = cause(e.cause, exhausted);
|
|
36
|
+
if (f && e.retryState === "TIMEOUT")
|
|
37
|
+
return new FlowError(f.message, { reason: "timeout", type: f.type, details: f.details });
|
|
38
|
+
return f ?? e;
|
|
39
|
+
}
|
|
40
|
+
if (e instanceof ChildWorkflowFailure)
|
|
41
|
+
return cause(e.cause, false) ?? e;
|
|
42
|
+
return e;
|
|
43
|
+
}
|
|
44
|
+
function cause(c, exhausted = false) {
|
|
45
|
+
if (c instanceof TimeoutFailure)
|
|
46
|
+
return new FlowError(c.message || "срок вышел", { reason: "timeout", exhausted });
|
|
47
|
+
if (c instanceof TerminatedFailure)
|
|
48
|
+
return new FlowError(c.message || "процесс прерван", { reason: "terminated" });
|
|
49
|
+
if (c instanceof ApplicationFailure)
|
|
50
|
+
return new FlowError(c.message, { reason: "failed", type: c.type ?? undefined, details: c.details?.[0], exhausted });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
function toTemporal(e, nonRetryable) {
|
|
54
|
+
if (e instanceof Failure)
|
|
55
|
+
return ApplicationFailure.create({
|
|
56
|
+
message: e.message,
|
|
57
|
+
type: e.type,
|
|
58
|
+
details: [e.details],
|
|
59
|
+
nonRetryable: nonRetryable || e.nonRetryable
|
|
60
|
+
});
|
|
61
|
+
if (isFlowError(e))
|
|
62
|
+
return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
|
|
63
|
+
return e;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/workflow.ts
|
|
67
|
+
var SIGNAL = "iwf.signal";
|
|
68
|
+
var QUERY = "iwf.query";
|
|
69
|
+
var signalDef = defineSignal(SIGNAL);
|
|
70
|
+
var queryDef = defineQuery(QUERY);
|
|
71
|
+
var poolQueue = (app, pool) => `${app}@${pool}`;
|
|
72
|
+
var proxies = new Map;
|
|
73
|
+
function activitiesWith(opts = {}) {
|
|
74
|
+
const key = JSON.stringify(opts);
|
|
75
|
+
let p = proxies.get(key);
|
|
76
|
+
if (!p) {
|
|
77
|
+
p = proxyActivities({
|
|
78
|
+
...opts.pool ? { taskQueue: poolQueue(workflowInfo().taskQueue, opts.pool) } : {},
|
|
79
|
+
startToCloseTimeout: opts.timeoutMs ?? 60000,
|
|
80
|
+
...opts.totalTimeoutMs ? { scheduleToCloseTimeout: opts.totalTimeoutMs } : {},
|
|
81
|
+
...opts.heartbeatTimeoutMs ? { heartbeatTimeout: opts.heartbeatTimeoutMs } : {},
|
|
82
|
+
retry: {
|
|
83
|
+
maximumAttempts: opts.attempts ?? 3,
|
|
84
|
+
initialInterval: opts.retryIntervalMs ?? 200,
|
|
85
|
+
backoffCoefficient: opts.backoff ?? 1,
|
|
86
|
+
...opts.maxRetryIntervalMs ? { maximumInterval: opts.maxRetryIntervalMs } : {},
|
|
87
|
+
...opts.nonRetryableTypes ? { nonRetryableErrorTypes: opts.nonRetryableTypes } : {}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
proxies.set(key, p);
|
|
91
|
+
}
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
var childOptions = (app, args, opts) => ({
|
|
95
|
+
taskQueue: app,
|
|
96
|
+
workflowId: opts.id,
|
|
97
|
+
args,
|
|
98
|
+
...opts.timeoutMs ? { workflowExecutionTimeout: opts.timeoutMs } : {},
|
|
99
|
+
...opts.onExisting === "reject" ? { workflowIdReusePolicy: "REJECT_DUPLICATE" } : {}
|
|
100
|
+
});
|
|
101
|
+
var rethrow = (e) => {
|
|
102
|
+
throw fromTemporal(e);
|
|
103
|
+
};
|
|
104
|
+
function temporalFlow(flow, patches = []) {
|
|
105
|
+
return async (...args) => {
|
|
106
|
+
const inbox = new Map;
|
|
107
|
+
const state = new Map;
|
|
108
|
+
setHandler(signalDef, ({ topic, value }) => {
|
|
109
|
+
const q = inbox.get(topic) ?? [];
|
|
110
|
+
q.push(value);
|
|
111
|
+
inbox.set(topic, q);
|
|
112
|
+
});
|
|
113
|
+
setHandler(queryDef, (key) => state.get(key));
|
|
114
|
+
const info = workflowInfo();
|
|
115
|
+
const ctx = {
|
|
116
|
+
workflowId: info.workflowId,
|
|
117
|
+
parentId: info.parent?.workflowId,
|
|
118
|
+
startedAt: info.startTime,
|
|
119
|
+
step: (name, stepArgs, opts) => activitiesWith(opts)[name](...stepArgs).catch(rethrow),
|
|
120
|
+
child: (target, childArgs, opts) => executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow),
|
|
121
|
+
async startChild(target, childArgs, opts) {
|
|
122
|
+
const h = await startChild(target.flow, {
|
|
123
|
+
...childOptions(target.app, childArgs, { ...opts, onExisting: "reject" }),
|
|
124
|
+
parentClosePolicy: ParentClosePolicy.ABANDON
|
|
125
|
+
}).catch(rethrow);
|
|
126
|
+
return { id: opts.id, result: () => h.result().catch(rethrow) };
|
|
127
|
+
},
|
|
128
|
+
sleep: (ms) => sleep(ms),
|
|
129
|
+
async recv(topic, timeoutMs) {
|
|
130
|
+
const ready = await condition(() => (inbox.get(topic)?.length ?? 0) > 0, timeoutMs);
|
|
131
|
+
return ready ? inbox.get(topic).shift() : null;
|
|
132
|
+
},
|
|
133
|
+
signal: (workflowId, topic, value) => getExternalWorkflowHandle(workflowId).signal(signalDef, { topic, value }).catch((e) => {
|
|
134
|
+
if (!(e instanceof TemporalFailure))
|
|
135
|
+
throw e;
|
|
136
|
+
throw new FlowError2(`процесса ${workflowId} нет или он завершён`, { reason: "not-found" });
|
|
137
|
+
}),
|
|
138
|
+
async setState(key, value) {
|
|
139
|
+
state.set(key, value);
|
|
140
|
+
},
|
|
141
|
+
patched: async (id) => {
|
|
142
|
+
if (!patches.includes(id))
|
|
143
|
+
throw new Error(`патч "${id}" не объявлен в patches процесса`);
|
|
144
|
+
return patched(id);
|
|
145
|
+
},
|
|
146
|
+
deprecatePatch: async (id) => deprecatePatch(id)
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
return await flow(ctx, ...args);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
throw toTemporal(e, true);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
QUERY,
|
|
157
|
+
SIGNAL,
|
|
158
|
+
poolQueue,
|
|
159
|
+
temporalFlow
|
|
160
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iworkflow/temporal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Temporal adapter for @iworkflow/core",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"workflow",
|
|
@@ -13,23 +13,35 @@
|
|
|
13
13
|
"directory": "packages/temporal"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
-
"src"
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"dist"
|
|
17
19
|
],
|
|
18
20
|
"type": "module",
|
|
19
21
|
"sideEffects": false,
|
|
20
22
|
"exports": {
|
|
21
|
-
".":
|
|
22
|
-
|
|
23
|
+
".": {
|
|
24
|
+
"bun": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./workflow": {
|
|
29
|
+
"bun": "./src/workflow.ts",
|
|
30
|
+
"types": "./dist/workflow.d.ts",
|
|
31
|
+
"default": "./dist/workflow.js"
|
|
32
|
+
}
|
|
23
33
|
},
|
|
24
34
|
"publishConfig": {
|
|
25
35
|
"access": "public"
|
|
26
36
|
},
|
|
27
37
|
"scripts": {
|
|
38
|
+
"build": "rm -rf dist && bun build src/index.ts src/workflow.ts --outdir dist --target node --format esm --packages external && tsc -p tsconfig.build.json",
|
|
28
39
|
"type-check": "tsc --noEmit -p tsconfig.json",
|
|
29
|
-
"test": "bun test"
|
|
40
|
+
"test": "bun test",
|
|
41
|
+
"prepack": "bun run build"
|
|
30
42
|
},
|
|
31
43
|
"dependencies": {
|
|
32
|
-
"@iworkflow/core": "^0.
|
|
44
|
+
"@iworkflow/core": "^0.4.0"
|
|
33
45
|
},
|
|
34
46
|
"peerDependencies": {
|
|
35
47
|
"@temporalio/activity": "^1.24.0",
|
|
@@ -44,5 +56,8 @@
|
|
|
44
56
|
"@temporalio/common": "^1.24.0",
|
|
45
57
|
"@temporalio/worker": "^1.24.0",
|
|
46
58
|
"@temporalio/workflow": "^1.24.0"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=22"
|
|
47
62
|
}
|
|
48
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Context } from "@temporalio/activity";
|
|
|
4
4
|
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
5
5
|
import {
|
|
6
6
|
bindStepRuntime,
|
|
7
|
+
exitSignal,
|
|
7
8
|
FlowError,
|
|
8
9
|
type Activity,
|
|
9
10
|
type AppDefinition,
|
|
@@ -79,11 +80,15 @@ export function temporalRuntime(
|
|
|
79
80
|
let running: Promise<void>[] = [];
|
|
80
81
|
let conn: NativeConnection | undefined;
|
|
81
82
|
let starting = false;
|
|
83
|
+
let stopping = false;
|
|
84
|
+
const exit = exitSignal();
|
|
82
85
|
let stopRenew = () => {};
|
|
83
86
|
return {
|
|
84
87
|
async start(def: AppDefinition) {
|
|
85
88
|
if (starting) throw new Error("воркер уже запущен");
|
|
86
89
|
starting = true;
|
|
90
|
+
exit.arm();
|
|
91
|
+
stopping = false;
|
|
87
92
|
bindStepRuntime({
|
|
88
93
|
heartbeat: async (details) => Context.current().heartbeat(details),
|
|
89
94
|
lastHeartbeat: async () => Context.current().info.heartbeatDetails,
|
|
@@ -93,6 +98,7 @@ export function temporalRuntime(
|
|
|
93
98
|
c = conn = await NativeConnection.connect(await connectOptions(opts));
|
|
94
99
|
} catch (e) {
|
|
95
100
|
starting = false;
|
|
101
|
+
exit.reject(e);
|
|
96
102
|
throw e;
|
|
97
103
|
}
|
|
98
104
|
stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
|
|
@@ -124,11 +130,19 @@ export function temporalRuntime(
|
|
|
124
130
|
conn = undefined;
|
|
125
131
|
workers = [];
|
|
126
132
|
starting = false;
|
|
133
|
+
exit.reject(e);
|
|
127
134
|
throw e;
|
|
128
135
|
}
|
|
129
136
|
running = workers.map((w) => w.run());
|
|
137
|
+
// Воркер упал сам — done() отклоняется сразу, не дожидаясь stop.
|
|
138
|
+
// Отказ run() во время stop (срок дренажа) — не падение: done() решает stop.
|
|
139
|
+
// Воркеры погасил сам Runtime (SIGTERM) без stop — тоже конец цикла.
|
|
140
|
+
void Promise.all(running).then(() => stopping || exit.resolve(), () => {});
|
|
141
|
+
for (const r of running) r.catch((e) => stopping || exit.reject(e));
|
|
130
142
|
},
|
|
143
|
+
done: () => exit.promise(),
|
|
131
144
|
async stop() {
|
|
145
|
+
stopping = true;
|
|
132
146
|
for (const w of workers) if (w.getState() === "RUNNING") w.shutdown();
|
|
133
147
|
// Упавший воркер не мешает закрыть соединение и таймер токена.
|
|
134
148
|
const done = await Promise.allSettled(running);
|
|
@@ -140,6 +154,7 @@ export function temporalRuntime(
|
|
|
140
154
|
workers = [];
|
|
141
155
|
running = [];
|
|
142
156
|
starting = false;
|
|
157
|
+
exit.resolve();
|
|
143
158
|
}
|
|
144
159
|
const failed = done.find((r) => r.status === "rejected");
|
|
145
160
|
if (failed) throw (failed as PromiseRejectedResult).reason;
|
package/src/connection.test.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { connectOptions, renewApiKey } from "./connection";
|
|
3
|
-
|
|
4
|
-
test("токен берётся при подключении и меняется по таймеру", async () => {
|
|
5
|
-
let n = 0;
|
|
6
|
-
const apiKey = async () => `t${++n}`;
|
|
7
|
-
expect(await connectOptions({ address: "a:1", tls: true, apiKey })).toEqual({ address: "a:1", tls: true, apiKey: "t1" });
|
|
8
|
-
const got: string[] = [];
|
|
9
|
-
const stop = renewApiKey({ apiKey, apiKeyRenewMs: 10 }, (t) => got.push(t));
|
|
10
|
-
while (got.length < 2) await Bun.sleep(5);
|
|
11
|
-
stop();
|
|
12
|
-
const seen = got.length;
|
|
13
|
-
await Bun.sleep(25);
|
|
14
|
-
expect(seen).toBeGreaterThanOrEqual(2);
|
|
15
|
-
expect(got.length).toBe(seen);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("без токена и TLS — только адрес", async () => {
|
|
19
|
-
expect(await connectOptions({ address: "a:1" })).toEqual({ address: "a:1" });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("медленный запрос не затирает свежий токен, после остановки токен не ставится", async () => {
|
|
23
|
-
let release!: (t: string) => void;
|
|
24
|
-
let calls = 0;
|
|
25
|
-
const apiKey = () => (++calls === 1 ? new Promise<string>((r) => (release = r)) : Promise.resolve(`t${calls}`));
|
|
26
|
-
const got: string[] = [];
|
|
27
|
-
const stop = renewApiKey({ apiKey, apiKeyRenewMs: 5 }, (t) => got.push(t));
|
|
28
|
-
await Bun.sleep(40);
|
|
29
|
-
expect(calls).toBe(1);
|
|
30
|
-
stop();
|
|
31
|
-
release("old");
|
|
32
|
-
await Bun.sleep(5);
|
|
33
|
-
expect(got).toEqual([]);
|
|
34
|
-
});
|