@iworkflow/temporal 0.6.1 → 0.8.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/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,
@@ -140,7 +141,15 @@ var wrap = (h, ready = async () => {}) => ({
140
141
  var TEMPORAL_STEP = {
141
142
  heartbeat: async (details) => Context.current().heartbeat(details),
142
143
  lastHeartbeat: async () => Context.current().info.heartbeatDetails,
143
- 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
144
153
  };
145
154
  var temporalStep = (fn) => stepScope(alsScope).run(TEMPORAL_STEP, fn);
146
155
  function temporalActivities(activities) {
@@ -167,8 +176,14 @@ function temporalRuntime(workflowsPath, opts = {}) {
167
176
  async start(def) {
168
177
  if (starting)
169
178
  throw new Error("воркер уже запущен");
170
- starting = true;
171
179
  exit.arm();
180
+ try {
181
+ checkLimits(def);
182
+ } catch (e) {
183
+ exit.reject(e);
184
+ throw e;
185
+ }
186
+ starting = true;
172
187
  stopping = false;
173
188
  let c;
174
189
  try {
@@ -183,7 +198,15 @@ function temporalRuntime(workflowsPath, opts = {}) {
183
198
  const activities = temporalActivities(def.activities);
184
199
  const base = { connection: c, namespace: namespace(opts), ...opts.plugins ? { plugins: opts.plugins } : {} };
185
200
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
186
- workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
201
+ workers = [
202
+ await Worker.create({
203
+ ...base,
204
+ taskQueue: def.app,
205
+ ...code,
206
+ activities,
207
+ ...def.maxConcurrentSteps ? { maxConcurrentActivityTaskExecutions: def.maxConcurrentSteps } : {}
208
+ })
209
+ ];
187
210
  for (const [pool, limit] of Object.entries(def.pools ?? {}))
188
211
  workers.push(await Worker.create({
189
212
  ...base,
@@ -267,7 +290,15 @@ async function temporalClient(opts = {}) {
267
290
  return {
268
291
  async start(target, args, o) {
269
292
  await ready();
270
- const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
293
+ const h = await client.workflow.start(target.flow, {
294
+ ...startOptions(target, args, o),
295
+ ...o.onExisting === "reject" || o.reuseClosedId ? {
296
+ workflowIdConflictPolicy: "FAIL",
297
+ workflowIdReusePolicy: o.reuseClosedId ? "ALLOW_DUPLICATE" : "REJECT_DUPLICATE"
298
+ } : { workflowIdConflictPolicy: "USE_EXISTING" }
299
+ }).catch((e) => {
300
+ throw fromTemporal(e);
301
+ });
271
302
  return wrap(h, ready);
272
303
  },
273
304
  async signalWithStart(target, args, o, topic, value) {
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 {
@@ -126,8 +127,10 @@ function temporalFlow(flow, patches = []) {
126
127
  step: (name, stepArgs, opts) => activitiesWith(opts)[name](...stepArgs).catch(rethrow),
127
128
  child: (target, childArgs, opts) => executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow),
128
129
  async startChild(target, childArgs, opts) {
130
+ const { reuseClosedId, ...rest } = opts;
129
131
  const h = await startChild(target.flow, {
130
- ...childOptions(target.app, childArgs, { ...opts, onExisting: "reject" }),
132
+ ...childOptions(target.app, childArgs, { ...rest, onExisting: "reject" }),
133
+ ...reuseClosedId ? { workflowIdReusePolicy: "ALLOW_DUPLICATE" } : {},
131
134
  parentClosePolicy: ParentClosePolicy.ABANDON
132
135
  }).catch(rethrow);
133
136
  return { id: opts.id, result: () => h.result().catch(rethrow) };
@@ -142,6 +145,7 @@ function temporalFlow(flow, patches = []) {
142
145
  throw e;
143
146
  throw new FlowError2(`процесса ${workflowId} нет или он завершён`, { reason: "not-found" });
144
147
  }),
148
+ log: async (level, message, attrs) => log[level](message, plainAttrs(attrs)),
145
149
  async setState(key, value) {
146
150
  state.set(key, value);
147
151
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/temporal",
3
- "version": "0.6.1",
3
+ "version": "0.8.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.1"
44
+ "@iworkflow/core": "^0.8.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@temporalio/activity": "^1.24.0",
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 };
@@ -77,6 +78,14 @@ const TEMPORAL_STEP: StepRuntime = {
77
78
  heartbeat: async (details) => Context.current().heartbeat(details),
78
79
  lastHeartbeat: async () => Context.current().info.heartbeatDetails,
79
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,
80
89
  };
81
90
 
82
91
  /** Один вызов шага в рантайме Temporal без обёртки отказов — для тестов в MockActivityEnvironment. */
@@ -120,8 +129,14 @@ export function temporalRuntime(
120
129
  return {
121
130
  async start(def: AppDefinition) {
122
131
  if (starting) throw new Error("воркер уже запущен");
123
- starting = true;
124
132
  exit.arm();
133
+ try {
134
+ checkLimits(def);
135
+ } catch (e) {
136
+ exit.reject(e);
137
+ throw e;
138
+ }
139
+ starting = true;
125
140
  stopping = false;
126
141
  let c: NativeConnection;
127
142
  try {
@@ -137,7 +152,15 @@ export function temporalRuntime(
137
152
  const activities = temporalActivities(def.activities);
138
153
  const base = { connection: c, namespace: namespace(opts), ...(opts.plugins ? { plugins: opts.plugins } : {}) };
139
154
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
140
- workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
155
+ workers = [
156
+ await Worker.create({
157
+ ...base,
158
+ taskQueue: def.app,
159
+ ...code,
160
+ activities,
161
+ ...(def.maxConcurrentSteps ? { maxConcurrentActivityTaskExecutions: def.maxConcurrentSteps } : {}),
162
+ }),
163
+ ];
141
164
  // Пул — своя очередь с пределом одновременных шагов в этом процессе.
142
165
  for (const [pool, limit] of Object.entries(def.pools ?? {}))
143
166
  workers.push(
@@ -232,7 +255,19 @@ export async function temporalClient(opts: TemporalOptions & { lazy?: boolean }
232
255
  return {
233
256
  async start(target, args, o) {
234
257
  await ready();
235
- const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
258
+ const h = await client.workflow
259
+ .start(target.flow, {
260
+ ...startOptions(target, args, o),
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
+ }
266
+ : { workflowIdConflictPolicy: "USE_EXISTING" as const }),
267
+ })
268
+ .catch((e) => {
269
+ throw fromTemporal(e);
270
+ });
236
271
  return wrap(h, ready);
237
272
  },
238
273
  async signalWithStart(target, args, o, topic, value) {
package/src/workflow.ts CHANGED
@@ -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";
@@ -94,15 +95,18 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
94
95
  child: (target, childArgs, opts) =>
95
96
  executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow) as Promise<never>,
96
97
  async startChild(target, childArgs, opts) {
97
- // REJECT_DUPLICATE: завершённый id тоже занят — как у DBOS, где id не переиспользуется.
98
+ // REJECT_DUPLICATE: завершённый id тоже занят, как у DBOS; reuseClosedId — ALLOW_DUPLICATE, новый запуск.
99
+ const { reuseClosedId, ...rest } = opts;
98
100
  const h = await startChild(target.flow, {
99
- ...childOptions(target.app, childArgs, { ...opts, onExisting: "reject" }),
101
+ ...childOptions(target.app, childArgs, { ...rest, onExisting: "reject" }),
102
+ ...(reuseClosedId ? { workflowIdReusePolicy: "ALLOW_DUPLICATE" as const } : {}),
100
103
  parentClosePolicy: ParentClosePolicy.ABANDON,
101
104
  }).catch(rethrow);
102
105
  return { id: opts.id, result: () => h.result().catch(rethrow) as Promise<never> };
103
106
  },
104
107
  sleep: (ms) => sleep(ms),
105
108
  async recv<T>(topic: string, timeoutMs: number) {
109
+ // 0 и меньше — таймер 1 мс (так было и до 0.8): отдаёт активацию, цикл опроса не виснет.
106
110
  const ready = await condition(() => (inbox.get(topic)?.length ?? 0) > 0, timeoutMs);
107
111
  return ready ? (inbox.get(topic)!.shift() as T) : null;
108
112
  },
@@ -113,6 +117,9 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
113
117
  if (!(e instanceof TemporalFailure)) throw e;
114
118
  throw new FlowError(`процесса ${workflowId} нет или он завершён`, { reason: "not-found" });
115
119
  }),
120
+ // log песочницы молчит при переигрывании.
121
+ // Неклонируемые attrs (функция, BigInt) уронили бы активацию при отправке из песочницы.
122
+ log: async (level, message, attrs) => log[level](message, plainAttrs(attrs)),
116
123
  async setState(key, value) {
117
124
  state.set(key, value);
118
125
  },