@iworkflow/temporal 0.0.0-stage → 0.1.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 +9 -2
- package/package.json +44 -4
- package/src/index.ts +66 -0
- package/src/workflow.ts +76 -0
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @iworkflow/temporal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,worker,workflow,activity}` ^1.24.
|
|
4
|
+
|
|
5
|
+
- `temporalRuntime(workflowsPath).start(def)` — воркер на task queue `def.app`.
|
|
6
|
+
- `temporalClient()` — старт, сигнал, query по id.
|
|
7
|
+
- `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
|
|
8
|
+
`export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
|
|
9
|
+
|
|
10
|
+
Переменные: `TEMPORAL_URL` (по умолчанию `127.0.0.1:17233`), `TEMPORAL_NAMESPACE`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iworkflow/temporal",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Temporal adapter for @iworkflow/core",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"workflow",
|
|
7
|
+
"temporal"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/iconicompany/iworkflow.git",
|
|
13
|
+
"directory": "packages/temporal"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./workflow": "./src/workflow.ts"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"type-check": "tsc --noEmit -p tsconfig.json",
|
|
29
|
+
"test": "bun test"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@iworkflow/core": "^0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@temporalio/activity": "^1.24.0",
|
|
36
|
+
"@temporalio/client": "^1.24.0",
|
|
37
|
+
"@temporalio/worker": "^1.24.0",
|
|
38
|
+
"@temporalio/workflow": "^1.24.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@temporalio/activity": "^1.24.0",
|
|
42
|
+
"@temporalio/client": "^1.24.0",
|
|
43
|
+
"@temporalio/worker": "^1.24.0",
|
|
44
|
+
"@temporalio/workflow": "^1.24.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Temporal: воркер на очереди модуля (task queue = имя модуля), клиент — по имени процесса.
|
|
2
|
+
import { Client, Connection, type WorkflowHandle } from "@temporalio/client";
|
|
3
|
+
import { Context } from "@temporalio/activity";
|
|
4
|
+
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
5
|
+
import { bindStepRuntime, type AppDefinition, type EngineClient, type EngineRuntime, type FlowHandle } from "@iworkflow/core";
|
|
6
|
+
|
|
7
|
+
// Те же имена, что в workflow.ts: его нельзя импортировать вне песочницы.
|
|
8
|
+
const SIGNAL = "iwf.signal";
|
|
9
|
+
const QUERY = "iwf.query";
|
|
10
|
+
|
|
11
|
+
const wrap = <T>(h: WorkflowHandle): FlowHandle<T> => ({
|
|
12
|
+
id: h.workflowId,
|
|
13
|
+
result: () => h.result(),
|
|
14
|
+
signal: (topic, value) => h.signal(SIGNAL, { topic, value }),
|
|
15
|
+
query: async <V>(key: string) => ((await h.query(QUERY, key)) ?? undefined) as V | undefined,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const address = () => process.env.TEMPORAL_URL ?? "127.0.0.1:17233";
|
|
19
|
+
const namespace = () => process.env.TEMPORAL_NAMESPACE ?? "default";
|
|
20
|
+
|
|
21
|
+
/** workflowsPath — вход песочницы модуля: temporalFlow(...) на каждый процесс, имя экспорта = имя процесса. */
|
|
22
|
+
export function temporalRuntime(workflowsPath: string): EngineRuntime {
|
|
23
|
+
let worker: Worker | undefined;
|
|
24
|
+
let running: Promise<void> | undefined;
|
|
25
|
+
let conn: NativeConnection | undefined;
|
|
26
|
+
return {
|
|
27
|
+
async start(def: AppDefinition) {
|
|
28
|
+
bindStepRuntime({
|
|
29
|
+
heartbeat: async (details) => Context.current().heartbeat(details),
|
|
30
|
+
lastHeartbeat: async () => Context.current().info.heartbeatDetails,
|
|
31
|
+
});
|
|
32
|
+
conn = await NativeConnection.connect({ address: address() });
|
|
33
|
+
worker = await Worker.create({
|
|
34
|
+
connection: conn,
|
|
35
|
+
namespace: namespace(),
|
|
36
|
+
taskQueue: def.app,
|
|
37
|
+
workflowsPath,
|
|
38
|
+
activities: def.activities,
|
|
39
|
+
});
|
|
40
|
+
running = worker.run();
|
|
41
|
+
},
|
|
42
|
+
async stop() {
|
|
43
|
+
worker?.shutdown();
|
|
44
|
+
await running;
|
|
45
|
+
await conn?.close();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function temporalClient(): Promise<EngineClient> {
|
|
51
|
+
const connection = await Connection.connect({ address: address() });
|
|
52
|
+
const client = new Client({ connection, namespace: namespace() });
|
|
53
|
+
return {
|
|
54
|
+
async start(target, args, opts) {
|
|
55
|
+
const h = await client.workflow.start(target.flow, {
|
|
56
|
+
taskQueue: target.app,
|
|
57
|
+
workflowId: opts.id,
|
|
58
|
+
args,
|
|
59
|
+
workflowIdConflictPolicy: "USE_EXISTING",
|
|
60
|
+
});
|
|
61
|
+
return wrap(h);
|
|
62
|
+
},
|
|
63
|
+
handle: (id) => wrap(client.workflow.getHandle(id)),
|
|
64
|
+
close: () => connection.close(),
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Обёртка процесса для песочницы Temporal: FlowContext поверх proxyActivities и executeChild.
|
|
2
|
+
// Сигналы и query — по одному обработчику на всё: тема и ключ едут в данных.
|
|
3
|
+
// Импортируется только из входа песочницы модуля: `@iworkflow/temporal/workflow`.
|
|
4
|
+
import {
|
|
5
|
+
condition,
|
|
6
|
+
defineQuery,
|
|
7
|
+
defineSignal,
|
|
8
|
+
deprecatePatch,
|
|
9
|
+
executeChild,
|
|
10
|
+
getExternalWorkflowHandle,
|
|
11
|
+
patched,
|
|
12
|
+
proxyActivities,
|
|
13
|
+
setHandler,
|
|
14
|
+
sleep,
|
|
15
|
+
workflowInfo,
|
|
16
|
+
} from "@temporalio/workflow";
|
|
17
|
+
|
|
18
|
+
import type { Flow, FlowContext, StepOptions } from "@iworkflow/core";
|
|
19
|
+
|
|
20
|
+
export const SIGNAL = "iwf.signal";
|
|
21
|
+
export const QUERY = "iwf.query";
|
|
22
|
+
const signalDef = defineSignal<[{ topic: string; value: unknown }]>(SIGNAL);
|
|
23
|
+
const queryDef = defineQuery<unknown, [string]>(QUERY);
|
|
24
|
+
|
|
25
|
+
const proxies = new Map<string, Record<string, (...a: unknown[]) => Promise<unknown>>>();
|
|
26
|
+
|
|
27
|
+
function activitiesWith(opts: StepOptions = {}) {
|
|
28
|
+
const key = `${opts.attempts ?? 3}/${opts.timeoutMs ?? 60_000}/${opts.heartbeatTimeoutMs ?? 0}`;
|
|
29
|
+
let p = proxies.get(key);
|
|
30
|
+
if (!p) {
|
|
31
|
+
p = proxyActivities({
|
|
32
|
+
startToCloseTimeout: opts.timeoutMs ?? 60_000,
|
|
33
|
+
...(opts.heartbeatTimeoutMs ? { heartbeatTimeout: opts.heartbeatTimeoutMs } : {}),
|
|
34
|
+
retry: { maximumAttempts: opts.attempts ?? 3, initialInterval: "200 ms", backoffCoefficient: 1 },
|
|
35
|
+
});
|
|
36
|
+
proxies.set(key, p);
|
|
37
|
+
}
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** patches — как AppDefinition.patches: необъявленный id — ошибка, как у DBOS. */
|
|
42
|
+
export function temporalFlow(flow: Flow, patches: string[] = []) {
|
|
43
|
+
return async (...args: unknown[]) => {
|
|
44
|
+
// Буфер по темам: сигнал до recv ждёт в очереди, как в DBOS.
|
|
45
|
+
const inbox = new Map<string, unknown[]>();
|
|
46
|
+
const state = new Map<string, unknown>();
|
|
47
|
+
setHandler(signalDef, ({ topic, value }) => {
|
|
48
|
+
const q = inbox.get(topic) ?? [];
|
|
49
|
+
q.push(value);
|
|
50
|
+
inbox.set(topic, q);
|
|
51
|
+
});
|
|
52
|
+
setHandler(queryDef, (key) => state.get(key));
|
|
53
|
+
|
|
54
|
+
const ctx: FlowContext = {
|
|
55
|
+
workflowId: workflowInfo().workflowId,
|
|
56
|
+
step: (name, stepArgs, opts) => activitiesWith(opts)[name]!(...stepArgs) as Promise<never>,
|
|
57
|
+
child: (target, childArgs, opts) =>
|
|
58
|
+
executeChild(target.flow, { taskQueue: target.app, workflowId: opts.id, args: childArgs }),
|
|
59
|
+
sleep: (ms) => sleep(ms),
|
|
60
|
+
async recv<T>(topic: string, timeoutMs: number) {
|
|
61
|
+
const ready = await condition(() => (inbox.get(topic)?.length ?? 0) > 0, timeoutMs);
|
|
62
|
+
return ready ? (inbox.get(topic)!.shift() as T) : null;
|
|
63
|
+
},
|
|
64
|
+
signal: (workflowId, topic, value) => getExternalWorkflowHandle(workflowId).signal(signalDef, { topic, value }),
|
|
65
|
+
async setState(key, value) {
|
|
66
|
+
state.set(key, value);
|
|
67
|
+
},
|
|
68
|
+
patched: async (id) => {
|
|
69
|
+
if (!patches.includes(id)) throw new Error(`патч "${id}" не объявлен в patches процесса`);
|
|
70
|
+
return patched(id);
|
|
71
|
+
},
|
|
72
|
+
deprecatePatch: async (id) => deprecatePatch(id),
|
|
73
|
+
};
|
|
74
|
+
return flow(ctx, ...args);
|
|
75
|
+
};
|
|
76
|
+
}
|