@iworkflow/temporal 0.4.0 → 0.5.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
@@ -7,5 +7,6 @@ Temporal для `@iworkflow/core`. Peer: `@temporalio/{client,worker,workflow,ac
7
7
  - `@iworkflow/temporal/workflow` — только для входа песочницы модуля:
8
8
  `export const outreach = temporalFlow(flows.outreach, ["send-twice"])`.
9
9
 
10
- `opts` (`TemporalOptions`): `address`, `namespace`, `tls`, `apiKey` — источник токена, меняется каждые `apiKeyRenewMs`.
10
+ `opts` (`TemporalOptions`): `address`, `namespace`, `tls`, `apiKey` — источник токена, меняется каждые `apiKeyRenewMs`;
11
+ `plugins` — плагины SDK воркерам и клиенту; `temporalClient({ lazy: true })` — соединение при первом вызове.
11
12
  Без них — переменные `TEMPORAL_URL` (по умолчанию `127.0.0.1:17233`), `TEMPORAL_NAMESPACE`.
@@ -1,4 +1,5 @@
1
- import type { TLSConfig } from "@temporalio/client";
1
+ import type { ConnectionPlugin, TLSConfig } from "@temporalio/client";
2
+ import type { WorkerPlugin } from "@temporalio/worker";
2
3
  /** Подключение к Temporal; не задано — TEMPORAL_URL и TEMPORAL_NAMESPACE из окружения. */
3
4
  export interface TemporalOptions {
4
5
  address?: string;
@@ -8,6 +9,8 @@ export interface TemporalOptions {
8
9
  /** Источник токена; соединение берёт свежий каждые apiKeyRenewMs (по умолчанию 60 с). */
9
10
  apiKey?: () => Promise<string>;
10
11
  apiKeyRenewMs?: number;
12
+ /** Плагины SDK (OpenTelemetry и т. п.) — воркерам и клиенту. */
13
+ plugins?: (ConnectionPlugin & WorkerPlugin)[];
11
14
  }
12
15
  export declare const address: (o: TemporalOptions) => string;
13
16
  export declare const namespace: (o: TemporalOptions) => string;
package/dist/index.d.ts CHANGED
@@ -8,4 +8,7 @@ export type { TemporalOptions };
8
8
  export declare function temporalRuntime(workflowsPath: string, opts?: TemporalOptions & {
9
9
  workflowBundlePath?: string;
10
10
  }): EngineRuntime;
11
- export declare function temporalClient(opts?: TemporalOptions): Promise<EngineClient>;
11
+ /** lazy — соединение при первом вызове: модуль поднимается и без Temporal. */
12
+ export declare function temporalClient(opts?: TemporalOptions & {
13
+ lazy?: boolean;
14
+ }): Promise<EngineClient>;
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ import { Context } from "@temporalio/activity";
4
4
  import { NativeConnection, Worker } from "@temporalio/worker";
5
5
  import {
6
6
  bindStepRuntime,
7
+ checkPriority,
8
+ DEFAULT_PRIORITY,
7
9
  exitSignal,
8
10
  FlowError as FlowError2
9
11
  } from "@iworkflow/core";
@@ -74,7 +76,8 @@ function toTemporal(e, nonRetryable) {
74
76
  message: e.message,
75
77
  type: e.type,
76
78
  details: [e.details],
77
- nonRetryable: nonRetryable || e.nonRetryable
79
+ nonRetryable: nonRetryable || e.nonRetryable,
80
+ ...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}
78
81
  });
79
82
  if (isFlowError(e))
80
83
  return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
@@ -99,18 +102,31 @@ var notFound = (id) => (e) => {
99
102
  throw new FlowError2(`процесса ${id} нет`, { reason: "not-found" });
100
103
  throw e;
101
104
  };
102
- var wrap = (h) => ({
105
+ var wrap = (h, ready = async () => {}) => ({
103
106
  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,
107
+ result: async () => {
108
+ await ready();
109
+ return h.result().catch((e) => {
110
+ throw e instanceof WorkflowFailedError && cause(e.cause) || e;
111
+ });
112
+ },
113
+ signal: async (topic, value) => {
114
+ await ready();
115
+ await h.signal(SIGNAL, { topic, value });
116
+ },
117
+ query: async (key) => {
118
+ await ready();
119
+ return await h.query(QUERY, key) ?? undefined;
120
+ },
109
121
  async describe() {
122
+ await ready();
110
123
  const d = await h.describe().catch(notFound(h.workflowId));
111
124
  return { id: h.workflowId, status: STATUS[d.status.name] ?? "running", startedAt: d.startTime };
112
125
  },
113
- terminate: async (reason) => void await h.terminate(reason).catch(notFound(h.workflowId))
126
+ async terminate(reason) {
127
+ await ready();
128
+ await h.terminate(reason).catch(notFound(h.workflowId));
129
+ }
114
130
  });
115
131
  var withFailures = (activities) => Object.fromEntries(Object.entries(activities).map(([name, fn]) => [
116
132
  name,
@@ -139,7 +155,8 @@ function temporalRuntime(workflowsPath, opts = {}) {
139
155
  stopping = false;
140
156
  bindStepRuntime({
141
157
  heartbeat: async (details) => Context.current().heartbeat(details),
142
- lastHeartbeat: async () => Context.current().info.heartbeatDetails
158
+ lastHeartbeat: async () => Context.current().info.heartbeatDetails,
159
+ attempt: () => Context.current().info.attempt
143
160
  });
144
161
  let c;
145
162
  try {
@@ -152,7 +169,7 @@ function temporalRuntime(workflowsPath, opts = {}) {
152
169
  stopRenew = renewApiKey(opts, (t) => c.setApiKey(t));
153
170
  try {
154
171
  const activities = withFailures(def.activities);
155
- const base = { connection: c, namespace: namespace(opts) };
172
+ const base = { connection: c, namespace: namespace(opts), ...opts.plugins ? { plugins: opts.plugins } : {} };
156
173
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
157
174
  workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
158
175
  for (const [pool, limit] of Object.entries(def.pools ?? {}))
@@ -205,21 +222,52 @@ function temporalRuntime(workflowsPath, opts = {}) {
205
222
  };
206
223
  }
207
224
  async function temporalClient(opts = {}) {
208
- const connection = await Connection.connect(await connectOptions(opts));
209
- const stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
225
+ let connection;
226
+ let stopRenew;
227
+ let ready = async () => {
228
+ return;
229
+ };
230
+ const plugins = opts.plugins ? { plugins: opts.plugins } : {};
231
+ if (opts.lazy) {
232
+ let token = "";
233
+ const apiKey = opts.apiKey;
234
+ let pending;
235
+ if (apiKey)
236
+ ready = async () => token || (token = await (pending ??= apiKey().finally(() => pending = undefined)));
237
+ connection = Connection.lazy({
238
+ ...await connectOptions({ ...opts, apiKey: undefined }),
239
+ ...apiKey ? { apiKey: () => token } : {},
240
+ ...plugins
241
+ });
242
+ stopRenew = renewApiKey(opts, (t) => token = t);
243
+ } else {
244
+ connection = await Connection.connect({ ...await connectOptions(opts), ...plugins });
245
+ stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
246
+ }
210
247
  const client = new Client({ connection, namespace: namespace(opts) });
248
+ const startOptions = (target, args, o) => ({
249
+ taskQueue: target.app,
250
+ workflowId: o.id,
251
+ args,
252
+ ...o.timeoutMs ? { workflowExecutionTimeout: o.timeoutMs } : {},
253
+ priority: { priorityKey: checkPriority(o.priority) ?? DEFAULT_PRIORITY }
254
+ });
211
255
  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 } : {}
256
+ async start(target, args, o) {
257
+ await ready();
258
+ const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
259
+ return wrap(h, ready);
260
+ },
261
+ async signalWithStart(target, args, o, topic, value) {
262
+ await ready();
263
+ const h = await client.workflow.signalWithStart(target.flow, {
264
+ ...startOptions(target, args, o),
265
+ signal: SIGNAL,
266
+ signalArgs: [{ topic, value }]
219
267
  });
220
- return wrap(h);
268
+ return wrap(h, ready);
221
269
  },
222
- handle: (id) => wrap(client.workflow.getHandle(id)),
270
+ handle: (id) => wrap(client.workflow.getHandle(id), ready),
223
271
  close: () => (stopRenew(), connection.close())
224
272
  };
225
273
  }
package/dist/workflow.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  TemporalFailure,
16
16
  workflowInfo
17
17
  } from "@temporalio/workflow";
18
- import { FlowError as FlowError2 } from "@iworkflow/core";
18
+ import { checkPriority, DEFAULT_PRIORITY, FlowError as FlowError2 } from "@iworkflow/core";
19
19
 
20
20
  // src/failure.ts
21
21
  import {
@@ -56,7 +56,8 @@ function toTemporal(e, nonRetryable) {
56
56
  message: e.message,
57
57
  type: e.type,
58
58
  details: [e.details],
59
- nonRetryable: nonRetryable || e.nonRetryable
59
+ nonRetryable: nonRetryable || e.nonRetryable,
60
+ ...e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}
60
61
  });
61
62
  if (isFlowError(e))
62
63
  return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
@@ -71,7 +72,8 @@ var queryDef = defineQuery(QUERY);
71
72
  var poolQueue = (app, pool) => `${app}@${pool}`;
72
73
  var proxies = new Map;
73
74
  function activitiesWith(opts = {}) {
74
- const key = JSON.stringify(opts);
75
+ const priority = checkPriority(opts.priority) ?? workflowInfo().priority?.priorityKey;
76
+ const key = JSON.stringify({ ...opts, priority });
75
77
  let p = proxies.get(key);
76
78
  if (!p) {
77
79
  p = proxyActivities({
@@ -79,9 +81,10 @@ function activitiesWith(opts = {}) {
79
81
  startToCloseTimeout: opts.timeoutMs ?? 60000,
80
82
  ...opts.totalTimeoutMs ? { scheduleToCloseTimeout: opts.totalTimeoutMs } : {},
81
83
  ...opts.heartbeatTimeoutMs ? { heartbeatTimeout: opts.heartbeatTimeoutMs } : {},
84
+ ...priority ? { priority: { priorityKey: priority } } : {},
82
85
  retry: {
83
86
  maximumAttempts: opts.attempts ?? 3,
84
- initialInterval: opts.retryIntervalMs ?? 200,
87
+ initialInterval: Math.max(opts.retryIntervalMs ?? 200, 1),
85
88
  backoffCoefficient: opts.backoff ?? 1,
86
89
  ...opts.maxRetryIntervalMs ? { maximumInterval: opts.maxRetryIntervalMs } : {},
87
90
  ...opts.nonRetryableTypes ? { nonRetryableErrorTypes: opts.nonRetryableTypes } : {}
@@ -116,6 +119,8 @@ function temporalFlow(flow, patches = []) {
116
119
  workflowId: info.workflowId,
117
120
  parentId: info.parent?.workflowId,
118
121
  startedAt: info.startTime,
122
+ priority: info.priority?.priorityKey || DEFAULT_PRIORITY,
123
+ now: async () => Date.now(),
119
124
  step: (name, stepArgs, opts) => activitiesWith(opts)[name](...stepArgs).catch(rethrow),
120
125
  child: (target, childArgs, opts) => executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow),
121
126
  async startChild(target, childArgs, opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/temporal",
3
- "version": "0.4.0",
3
+ "version": "0.5.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.4.0"
44
+ "@iworkflow/core": "^0.5.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@temporalio/activity": "^1.24.0",
package/src/connection.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Подключение к Temporal: адрес, TLS, токен с заменой на месте — общее для воркера и клиента.
2
- import type { TLSConfig } from "@temporalio/client";
2
+ import type { ConnectionPlugin, TLSConfig } from "@temporalio/client";
3
+ import type { WorkerPlugin } from "@temporalio/worker";
3
4
 
4
5
  /** Подключение к Temporal; не задано — TEMPORAL_URL и TEMPORAL_NAMESPACE из окружения. */
5
6
  export interface TemporalOptions {
@@ -10,6 +11,8 @@ export interface TemporalOptions {
10
11
  /** Источник токена; соединение берёт свежий каждые apiKeyRenewMs (по умолчанию 60 с). */
11
12
  apiKey?: () => Promise<string>;
12
13
  apiKeyRenewMs?: number;
14
+ /** Плагины SDK (OpenTelemetry и т. п.) — воркерам и клиенту. */
15
+ plugins?: (ConnectionPlugin & WorkerPlugin)[];
13
16
  }
14
17
 
15
18
  export const address = (o: TemporalOptions) => o.address ?? process.env.TEMPORAL_URL ?? "127.0.0.1:17233";
package/src/failure.ts CHANGED
@@ -41,6 +41,8 @@ export function toTemporal(e: unknown, nonRetryable: boolean): unknown {
41
41
  type: e.type,
42
42
  details: [e.details],
43
43
  nonRetryable: nonRetryable || e.nonRetryable,
44
+ // 0 сервер считает «не задано» и берёт паузу политики; 1 мс — «сразу», как у DBOS.
45
+ ...(e.retryAfterMs !== undefined ? { nextRetryDelay: Math.max(e.retryAfterMs, 1) } : {}),
44
46
  });
45
47
  if (isFlowError(e))
46
48
  return ApplicationFailure.create({ message: e.message, type: e.type ?? e.reason, details: [e.details], nonRetryable });
package/src/index.ts CHANGED
@@ -4,6 +4,8 @@ import { Context } from "@temporalio/activity";
4
4
  import { NativeConnection, Worker } from "@temporalio/worker";
5
5
  import {
6
6
  bindStepRuntime,
7
+ checkPriority,
8
+ DEFAULT_PRIORITY,
7
9
  exitSignal,
8
10
  FlowError,
9
11
  type Activity,
@@ -11,7 +13,9 @@ import {
11
13
  type EngineClient,
12
14
  type EngineRuntime,
13
15
  type FlowHandle,
16
+ type FlowRef,
14
17
  type FlowStatus,
18
+ type StartOptions,
15
19
  } from "@iworkflow/core";
16
20
  import { address, connectOptions, namespace, renewApiKey, type TemporalOptions } from "./connection";
17
21
  import { cause, toTemporal } from "./failure";
@@ -38,19 +42,32 @@ const notFound = (id: string) => (e: unknown): never => {
38
42
  throw e;
39
43
  };
40
44
 
41
- const wrap = <T>(h: WorkflowHandle): FlowHandle<T> => ({
45
+ /** ready — токен ленивого клиента: без него SDK уйдёт с пустым ключом. */
46
+ const wrap = <T>(h: WorkflowHandle, ready: () => Promise<unknown> = async () => {}): FlowHandle<T> => ({
42
47
  id: h.workflowId,
43
- result: () =>
44
- h.result().catch((e) => {
48
+ result: async () => {
49
+ await ready();
50
+ return h.result().catch((e) => {
45
51
  throw (e instanceof WorkflowFailedError && cause(e.cause)) || e;
46
- }),
47
- signal: (topic, value) => h.signal(SIGNAL, { topic, value }),
48
- query: async <V>(key: string) => ((await h.query(QUERY, key)) ?? undefined) as V | undefined,
52
+ });
53
+ },
54
+ signal: async (topic, value) => {
55
+ await ready();
56
+ await h.signal(SIGNAL, { topic, value });
57
+ },
58
+ query: async <V>(key: string) => {
59
+ await ready();
60
+ return ((await h.query(QUERY, key)) ?? undefined) as V | undefined;
61
+ },
49
62
  async describe() {
63
+ await ready();
50
64
  const d = await h.describe().catch(notFound(h.workflowId));
51
65
  return { id: h.workflowId, status: STATUS[d.status.name] ?? "running", startedAt: d.startTime };
52
66
  },
53
- terminate: async (reason) => void (await h.terminate(reason).catch(notFound(h.workflowId))),
67
+ async terminate(reason) {
68
+ await ready();
69
+ await h.terminate(reason).catch(notFound(h.workflowId));
70
+ },
54
71
  });
55
72
 
56
73
  /** Шаги отдают Failure как ApplicationFailure: тип, детали и nonRetryable доезжают до процесса. */
@@ -92,6 +109,7 @@ export function temporalRuntime(
92
109
  bindStepRuntime({
93
110
  heartbeat: async (details) => Context.current().heartbeat(details),
94
111
  lastHeartbeat: async () => Context.current().info.heartbeatDetails,
112
+ attempt: () => Context.current().info.attempt,
95
113
  });
96
114
  let c: NativeConnection;
97
115
  try {
@@ -105,7 +123,7 @@ export function temporalRuntime(
105
123
  // Воркер не создался (нет бандла, namespace) — соединение и таймер токена закрываются.
106
124
  try {
107
125
  const activities = withFailures(def.activities);
108
- const base = { connection: c, namespace: namespace(opts) };
126
+ const base = { connection: c, namespace: namespace(opts), ...(opts.plugins ? { plugins: opts.plugins } : {}) };
109
127
  const code = opts.workflowBundlePath ? { workflowBundle: { codePath: opts.workflowBundlePath } } : { workflowsPath };
110
128
  workers = [await Worker.create({ ...base, taskQueue: def.app, ...code, activities })];
111
129
  // Пул — своя очередь с пределом одновременных шагов в этом процессе.
@@ -162,22 +180,59 @@ export function temporalRuntime(
162
180
  };
163
181
  }
164
182
 
165
- export async function temporalClient(opts: TemporalOptions = {}): Promise<EngineClient> {
166
- const connection = await Connection.connect(await connectOptions(opts));
167
- const stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
183
+ /** lazy — соединение при первом вызове: модуль поднимается и без Temporal. */
184
+ export async function temporalClient(opts: TemporalOptions & { lazy?: boolean } = {}): Promise<EngineClient> {
185
+ let connection: Connection;
186
+ let stopRenew: () => void;
187
+ let ready = async (): Promise<unknown> => undefined;
188
+ const plugins = opts.plugins ? { plugins: opts.plugins } : {};
189
+ if (opts.lazy) {
190
+ // Ни Temporal, ни источник токена не нужны для старта: ключ — функцией, берётся при первом вызове.
191
+ let token = "";
192
+ const apiKey = opts.apiKey;
193
+ // Неудача не залипает: следующий вызов спросит токен снова.
194
+ // Один запрос на всех ждущих; неудача не залипает: следующий вызов спросит снова.
195
+ let pending: Promise<string> | undefined;
196
+ if (apiKey)
197
+ ready = async () =>
198
+ token ||
199
+ (token = await (pending ??= apiKey().finally(() => (pending = undefined))));
200
+ connection = Connection.lazy({
201
+ ...(await connectOptions({ ...opts, apiKey: undefined })),
202
+ ...(apiKey ? { apiKey: () => token } : {}),
203
+ ...plugins,
204
+ });
205
+ stopRenew = renewApiKey(opts, (t) => (token = t));
206
+ } else {
207
+ connection = await Connection.connect({ ...(await connectOptions(opts)), ...plugins });
208
+ stopRenew = renewApiKey(opts, (t) => connection.setApiKey(t));
209
+ }
210
+ // Плагины — у соединения: оно само передаёт их клиенту.
168
211
  const client = new Client({ connection, namespace: namespace(opts) });
212
+ const startOptions = (target: FlowRef, args: unknown[], o: StartOptions) => ({
213
+ taskQueue: target.app,
214
+ workflowId: o.id,
215
+ args,
216
+ ...(o.timeoutMs ? { workflowExecutionTimeout: o.timeoutMs } : {}),
217
+ // Явно и по умолчанию: ctx.priority и очередь видят одно и то же, как у DBOS.
218
+ priority: { priorityKey: checkPriority(o.priority) ?? DEFAULT_PRIORITY },
219
+ });
169
220
  return {
170
- async start(target, args, opts) {
171
- const h = await client.workflow.start(target.flow, {
172
- taskQueue: target.app,
173
- workflowId: opts.id,
174
- args,
175
- workflowIdConflictPolicy: "USE_EXISTING",
176
- ...(opts.timeoutMs ? { workflowExecutionTimeout: opts.timeoutMs } : {}),
221
+ async start(target, args, o) {
222
+ await ready();
223
+ const h = await client.workflow.start(target.flow, { ...startOptions(target, args, o), workflowIdConflictPolicy: "USE_EXISTING" });
224
+ return wrap(h, ready);
225
+ },
226
+ async signalWithStart(target, args, o, topic, value) {
227
+ await ready();
228
+ const h = await client.workflow.signalWithStart(target.flow, {
229
+ ...startOptions(target, args, o),
230
+ signal: SIGNAL,
231
+ signalArgs: [{ topic, value }],
177
232
  });
178
- return wrap(h);
233
+ return wrap(h, ready);
179
234
  },
180
- handle: (id) => wrap(client.workflow.getHandle(id)),
235
+ handle: (id) => wrap(client.workflow.getHandle(id), ready),
181
236
  close: () => (stopRenew(), connection.close()),
182
237
  };
183
238
  }
package/src/workflow.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  workflowInfo,
19
19
  } from "@temporalio/workflow";
20
20
 
21
- import { FlowError, type ChildOptions, type Flow, type FlowContext, type StepOptions } from "@iworkflow/core";
21
+ import { checkPriority, DEFAULT_PRIORITY, FlowError, type ChildOptions, type Flow, type FlowContext, type StepOptions } from "@iworkflow/core";
22
22
  import { fromTemporal, toTemporal } from "./failure";
23
23
 
24
24
  export const SIGNAL = "iwf.signal";
@@ -32,7 +32,9 @@ export const poolQueue = (app: string, pool: string) => `${app}@${pool}`;
32
32
  const proxies = new Map<string, Record<string, (...a: unknown[]) => Promise<unknown>>>();
33
33
 
34
34
  function activitiesWith(opts: StepOptions = {}) {
35
- const key = JSON.stringify(opts);
35
+ // Шаг без своего приоритета — с приоритетом процесса, как у Temporal по умолчанию.
36
+ const priority = checkPriority(opts.priority) ?? workflowInfo().priority?.priorityKey;
37
+ const key = JSON.stringify({ ...opts, priority });
36
38
  let p = proxies.get(key);
37
39
  if (!p) {
38
40
  p = proxyActivities({
@@ -40,9 +42,11 @@ function activitiesWith(opts: StepOptions = {}) {
40
42
  startToCloseTimeout: opts.timeoutMs ?? 60_000,
41
43
  ...(opts.totalTimeoutMs ? { scheduleToCloseTimeout: opts.totalTimeoutMs } : {}),
42
44
  ...(opts.heartbeatTimeoutMs ? { heartbeatTimeout: opts.heartbeatTimeoutMs } : {}),
45
+ ...(priority ? { priority: { priorityKey: priority } } : {}),
43
46
  retry: {
44
47
  maximumAttempts: opts.attempts ?? 3,
45
- initialInterval: opts.retryIntervalMs ?? 200,
48
+ // 0 Temporal не принимает (ValueError — вечный повтор задачи процесса): 1 мс — «сразу», как у DBOS.
49
+ initialInterval: Math.max(opts.retryIntervalMs ?? 200, 1),
46
50
  backoffCoefficient: opts.backoff ?? 1,
47
51
  ...(opts.maxRetryIntervalMs ? { maximumInterval: opts.maxRetryIntervalMs } : {}),
48
52
  ...(opts.nonRetryableTypes ? { nonRetryableErrorTypes: opts.nonRetryableTypes } : {}),
@@ -83,6 +87,9 @@ export function temporalFlow(flow: Flow, patches: string[] = []) {
83
87
  workflowId: info.workflowId,
84
88
  parentId: info.parent?.workflowId,
85
89
  startedAt: info.startTime,
90
+ priority: info.priority?.priorityKey || DEFAULT_PRIORITY,
91
+ // Date.now() в песочнице Temporal детерминирован: время задачи процесса.
92
+ now: async () => Date.now(),
86
93
  step: (name, stepArgs, opts) => activitiesWith(opts)[name]!(...stepArgs).catch(rethrow) as Promise<never>,
87
94
  child: (target, childArgs, opts) =>
88
95
  executeChild(target.flow, childOptions(target.app, childArgs, opts)).catch(rethrow) as Promise<never>,