@iworkflow/temporal 0.6.0 → 0.7.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
@@ -11,6 +11,8 @@ Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,common,worker,work
11
11
  - `temporalActivities(activities)` — шаги порта для своего `Worker.create` (слив старой очереди, тестовый
12
12
  воркер): `Failure` → `ApplicationFailure`, работают `heartbeat` и `stepAttempt`. Шаги без неё
13
13
  в своём воркере `heartbeat`/`stepAttempt` не видят — с 0.6.0 рантайм шага ставится на время шага.
14
+ - `temporalStep(fn)` — тест шага без воркера: `new MockActivityEnvironment().run(() => temporalStep(() => acts.send(x)))`;
15
+ `acts` — сырые шаги (не результат `temporalActivities`), отказ остаётся `Failure`; `@temporalio/testing` ставит сам модуль.
14
16
  - `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
15
17
  `export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
16
18
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { type Activity, type EngineClient, type EngineRuntime } from "@iworkflow/core";
2
2
  import { type TemporalOptions } from "./connection";
3
3
  export type { TemporalOptions };
4
+ /** Один вызов шага в рантайме Temporal без обёртки отказов — для тестов в MockActivityEnvironment. */
5
+ export declare const temporalStep: <T>(fn: () => T) => T;
4
6
  /**
5
7
  * Шаги порта для своего Worker.create (слив старой очереди, тестовый воркер): Failure доезжает
6
8
  * как ApplicationFailure, heartbeat и stepAttempt работают. temporalRuntime делает это сам.
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { Context } from "@temporalio/activity";
4
4
  import { NativeConnection, Worker } from "@temporalio/worker";
5
5
  import {
6
6
  stepScope,
7
+ checkLimits,
7
8
  checkPriority,
8
9
  DEFAULT_PRIORITY,
9
10
  exitSignal,
@@ -142,13 +143,13 @@ var TEMPORAL_STEP = {
142
143
  lastHeartbeat: async () => Context.current().info.heartbeatDetails,
143
144
  attempt: () => Context.current().info.attempt
144
145
  };
146
+ var temporalStep = (fn) => stepScope(alsScope).run(TEMPORAL_STEP, fn);
145
147
  function temporalActivities(activities) {
146
- const scope = stepScope(alsScope);
147
148
  return Object.fromEntries(Object.entries(activities).map(([name, fn]) => [
148
149
  name,
149
150
  async (...args) => {
150
151
  try {
151
- return await scope.run(TEMPORAL_STEP, () => fn(...args));
152
+ return await temporalStep(() => fn(...args));
152
153
  } catch (e) {
153
154
  throw toTemporal(e, false);
154
155
  }
@@ -167,8 +168,14 @@ function temporalRuntime(workflowsPath, opts = {}) {
167
168
  async start(def) {
168
169
  if (starting)
169
170
  throw new Error("воркер уже запущен");
170
- starting = true;
171
171
  exit.arm();
172
+ try {
173
+ checkLimits(def);
174
+ } catch (e) {
175
+ exit.reject(e);
176
+ throw e;
177
+ }
178
+ starting = true;
172
179
  stopping = false;
173
180
  let c;
174
181
  try {
@@ -183,7 +190,15 @@ function temporalRuntime(workflowsPath, opts = {}) {
183
190
  const activities = temporalActivities(def.activities);
184
191
  const base = { connection: c, namespace: namespace(opts), ...opts.plugins ? { plugins: opts.plugins } : {} };
185
192
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
186
- workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
193
+ workers = [
194
+ await Worker.create({
195
+ ...base,
196
+ taskQueue: def.app,
197
+ ...code,
198
+ activities,
199
+ ...def.maxConcurrentSteps ? { maxConcurrentActivityTaskExecutions: def.maxConcurrentSteps } : {}
200
+ })
201
+ ];
187
202
  for (const [pool, limit] of Object.entries(def.pools ?? {}))
188
203
  workers.push(await Worker.create({
189
204
  ...base,
@@ -267,7 +282,12 @@ async function temporalClient(opts = {}) {
267
282
  return {
268
283
  async start(target, args, o) {
269
284
  await ready();
270
- const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
285
+ const h = await client.workflow.start(target.flow, {
286
+ ...startOptions(target, args, o),
287
+ ...o.onExisting === "reject" ? { workflowIdConflictPolicy: "FAIL", workflowIdReusePolicy: "REJECT_DUPLICATE" } : { workflowIdConflictPolicy: "USE_EXISTING" }
288
+ }).catch((e) => {
289
+ throw fromTemporal(e);
290
+ });
271
291
  return wrap(h, ready);
272
292
  },
273
293
  async signalWithStart(target, args, o, topic, value) {
@@ -286,5 +306,6 @@ async function temporalClient(opts = {}) {
286
306
  export {
287
307
  temporalActivities,
288
308
  temporalClient,
289
- temporalRuntime
309
+ temporalRuntime,
310
+ temporalStep
290
311
  };
package/dist/workflow.js CHANGED
@@ -126,8 +126,10 @@ function temporalFlow(flow, patches = []) {
126
126
  step: (name, stepArgs, opts) => activitiesWith(opts)[name](...stepArgs).catch(rethrow),
127
127
  child: (target, childArgs, opts) => executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow),
128
128
  async startChild(target, childArgs, opts) {
129
+ const { reuseClosedId, ...rest } = opts;
129
130
  const h = await startChild(target.flow, {
130
- ...childOptions(target.app, childArgs, { ...opts, onExisting: "reject" }),
131
+ ...childOptions(target.app, childArgs, { ...rest, onExisting: "reject" }),
132
+ ...reuseClosedId ? { workflowIdReusePolicy: "ALLOW_DUPLICATE" } : {},
131
133
  parentClosePolicy: ParentClosePolicy.ABANDON
132
134
  }).catch(rethrow);
133
135
  return { id: opts.id, result: () => h.result().catch(rethrow) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/temporal",
3
- "version": "0.6.0",
3
+ "version": "0.7.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.6.0"
44
+ "@iworkflow/core": "^0.7.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@temporalio/activity": "^1.24.0",
@@ -54,6 +54,7 @@
54
54
  "@temporalio/activity": "^1.24.0",
55
55
  "@temporalio/client": "^1.24.0",
56
56
  "@temporalio/common": "^1.24.0",
57
+ "@temporalio/testing": "^1.24.0",
57
58
  "@temporalio/worker": "^1.24.0",
58
59
  "@temporalio/workflow": "^1.24.0"
59
60
  },
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { NativeConnection, Worker } from "@temporalio/worker";
5
5
  import {
6
6
  stepScope,
7
7
  type StepRuntime,
8
+ checkLimits,
8
9
  checkPriority,
9
10
  DEFAULT_PRIORITY,
10
11
  exitSignal,
@@ -19,7 +20,7 @@ import {
19
20
  type StartOptions,
20
21
  } from "@iworkflow/core";
21
22
  import { address, connectOptions, namespace, renewApiKey, type TemporalOptions } from "./connection";
22
- import { cause, toTemporal } from "./failure";
23
+ import { cause, fromTemporal, toTemporal } from "./failure";
23
24
  import { alsScope } from "./scope";
24
25
 
25
26
  export type { TemporalOptions };
@@ -79,18 +80,21 @@ const TEMPORAL_STEP: StepRuntime = {
79
80
  attempt: () => Context.current().info.attempt,
80
81
  };
81
82
 
83
+ /** Один вызов шага в рантайме Temporal без обёртки отказов — для тестов в MockActivityEnvironment. */
84
+ // Область берётся при вызове, а не при импорте: пакет sideEffects: false.
85
+ export const temporalStep = <T>(fn: () => T): T => stepScope(alsScope).run(TEMPORAL_STEP, fn);
86
+
82
87
  /**
83
88
  * Шаги порта для своего Worker.create (слив старой очереди, тестовый воркер): Failure доезжает
84
89
  * как ApplicationFailure, heartbeat и stepAttempt работают. temporalRuntime делает это сам.
85
90
  */
86
91
  export function temporalActivities<T extends Record<string, Activity>>(activities: T): T {
87
- const scope = stepScope(alsScope);
88
92
  return Object.fromEntries(
89
93
  Object.entries(activities).map(([name, fn]) => [
90
94
  name,
91
95
  async (...args: unknown[]) => {
92
96
  try {
93
- return await scope.run(TEMPORAL_STEP, () => fn(...args));
97
+ return await temporalStep(() => fn(...args));
94
98
  } catch (e) {
95
99
  throw toTemporal(e, false);
96
100
  }
@@ -117,8 +121,14 @@ export function temporalRuntime(
117
121
  return {
118
122
  async start(def: AppDefinition) {
119
123
  if (starting) throw new Error("воркер уже запущен");
120
- starting = true;
121
124
  exit.arm();
125
+ try {
126
+ checkLimits(def);
127
+ } catch (e) {
128
+ exit.reject(e);
129
+ throw e;
130
+ }
131
+ starting = true;
122
132
  stopping = false;
123
133
  let c: NativeConnection;
124
134
  try {
@@ -134,7 +144,15 @@ export function temporalRuntime(
134
144
  const activities = temporalActivities(def.activities);
135
145
  const base = { connection: c, namespace: namespace(opts), ...(opts.plugins ? { plugins: opts.plugins } : {}) };
136
146
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
137
- workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
147
+ workers = [
148
+ await Worker.create({
149
+ ...base,
150
+ taskQueue: def.app,
151
+ ...code,
152
+ activities,
153
+ ...(def.maxConcurrentSteps ? { maxConcurrentActivityTaskExecutions: def.maxConcurrentSteps } : {}),
154
+ }),
155
+ ];
138
156
  // Пул — своя очередь с пределом одновременных шагов в этом процессе.
139
157
  for (const [pool, limit] of Object.entries(def.pools ?? {}))
140
158
  workers.push(
@@ -229,7 +247,16 @@ export async function temporalClient(opts: TemporalOptions & { lazy?: boolean }
229
247
  return {
230
248
  async start(target, args, o) {
231
249
  await ready();
232
- const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
250
+ const h = await client.workflow
251
+ .start(target.flow, {
252
+ ...startOptions(target, args, o),
253
+ ...(o.onExisting === "reject"
254
+ ? { workflowIdConflictPolicy: "FAIL" as const, workflowIdReusePolicy: "REJECT_DUPLICATE" as const }
255
+ : { workflowIdConflictPolicy: "USE_EXISTING" as const }),
256
+ })
257
+ .catch((e) => {
258
+ throw fromTemporal(e);
259
+ });
233
260
  return wrap(h, ready);
234
261
  },
235
262
  async signalWithStart(target, args, o, topic, value) {
package/src/workflow.ts CHANGED
@@ -94,9 +94,11 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
94
94
  child: (target, childArgs, opts) =>
95
95
  executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow) as Promise<never>,
96
96
  async startChild(target, childArgs, opts) {
97
- // REJECT_DUPLICATE: завершённый id тоже занят — как у DBOS, где id не переиспользуется.
97
+ // REJECT_DUPLICATE: завершённый id тоже занят, как у DBOS; reuseClosedId — ALLOW_DUPLICATE, новый запуск.
98
+ const { reuseClosedId, ...rest } = opts;
98
99
  const h = await startChild(target.flow, {
99
- ...childOptions(target.app, childArgs, { ...opts, onExisting: "reject" }),
100
+ ...childOptions(target.app, childArgs, { ...rest, onExisting: "reject" }),
101
+ ...(reuseClosedId ? { workflowIdReusePolicy: "ALLOW_DUPLICATE" as const } : {}),
100
102
  parentClosePolicy: ParentClosePolicy.ABANDON,
101
103
  }).catch(rethrow);
102
104
  return { id: opts.id, result: () => h.result().catch(rethrow) as Promise<never> };