@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 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" ? { workflowIdConflictPolicy: "FAIL", workflowIdReusePolicy: "REJECT_DUPLICATE" } : { workflowIdConflictPolicy: "USE_EXISTING" }
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
  });
@@ -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
- /** patches — как AppDefinition.patches: необъявленный id — ошибка, как у DBOS. */
7
- export declare function temporalFlow(flow: Flow, patches?: string[]): (...args: unknown[]) => Promise<unknown>;
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
- function temporalFlow(flow, patches = []) {
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
- setHandler(signalDef, ({ topic, value }) => {
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.7.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.7.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
- ? { workflowIdConflictPolicy: "FAIL" as const, workflowIdReusePolicy: "REJECT_DUPLICATE" as const }
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
- /** patches — как AppDefinition.patches: необъявленный id — ошибка, как у DBOS. */
73
- export function temporalFlow(flow: Flow, patches: string[] = []) {
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
- setHandler(signalDef, ({ topic, value }) => {
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
  },