@iworkflow/core 0.7.0 → 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/README.md CHANGED
@@ -38,6 +38,6 @@ try {
38
38
 
39
39
  Приоритет: `client.start(..., { priority })` (1 — первым, 5 — последним) → `ctx.priority`; шаги и
40
40
  дети его наследуют, `StepOptions.priority` — свой. Срок повтора из отказа: `new Failure(msg, { type,
41
- retryAfterMs })`; `benign: true` — штатный отказ, Temporal не пишет его ошибкой. Номер попытки в шаге — `stepAttempt()`. Время в процессе — `await ctx.now()`.
41
+ retryAfterMs })`; `benign: true` — штатный отказ, Temporal не пишет его ошибкой. Номер попытки в шаге — `stepAttempt()`, процесс и запуск — `stepInfo()`, отмена попытки — `stepSignal()`. Журнал без повторов — `ctx.log`. Время в процессе — `await ctx.now()`.
42
42
  `client.signalWithStart(target, args, opts, topic, value)` — сигнал идущему или запуск с сигналом.
43
43
  Контракт обоих движков — `lab/test/contract.test.ts` репозитория.
package/dist/errors.d.ts CHANGED
@@ -50,3 +50,5 @@ export declare function checkPriority(p: number | undefined): number | undefined
50
50
  export declare const DEFAULT_PRIORITY = 3;
51
51
  /** Пределы модуля — целые ≥ 1: иначе DBOS молча вешает шаги, а Temporal падает на старте. */
52
52
  export declare function checkLimits(def: AppDefinition): void;
53
+ /** attrs журнала — в JSON-данные: BigInt строкой, Error — name, message и свои поля; непереносимый ключ выпадает один. */
54
+ export declare function plainAttrs(attrs: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
@@ -3,6 +3,14 @@ export interface StepRuntime {
3
3
  lastHeartbeat(): Promise<unknown>;
4
4
  /** Номер попытки шага, с 1. */
5
5
  attempt(): number;
6
+ /** Процесс шага, запуск и шаг в нём. */
7
+ info(): {
8
+ workflowId: string;
9
+ runId: string;
10
+ stepId: string;
11
+ };
12
+ /** Отмена попытки. Temporal — срок и отмена процесса, узнаёт через heartbeat; DBOS — только срок попытки. */
13
+ signal(): AbortSignal;
6
14
  }
7
15
  /** Область шага: какой рантайм у шага, что идёт сейчас. Самый внутренний шаг — свой, без угадывания движка. */
8
16
  export interface StepScope {
@@ -20,3 +28,12 @@ export declare const heartbeat: (details?: unknown) => Promise<void>;
20
28
  export declare const lastHeartbeat: <T>() => Promise<T | undefined>;
21
29
  /** Номер попытки этого шага, с 1. DBOS считает заново после рестарта процесса, Temporal — нет. */
22
30
  export declare const stepAttempt: () => number;
31
+ /** Ключ идемпотентности внешней записи — runId + stepId: id переиспользуется (reuseClosedId), runId нет. */
32
+ export declare const stepInfo: () => {
33
+ workflowId: string;
34
+ runId: string;
35
+ stepId: string;
36
+ attempt: number;
37
+ };
38
+ /** Сигнал отмены попытки: передать в fetch и т. п.; aborted — итог попытки отброшен, не оборачивать в Failure. */
39
+ export declare const stepSignal: () => AbortSignal;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from "./types";
2
- export { heartbeat, lastHeartbeat, stepAttempt, stepScope, type StepRuntime, type StepScope } from "./heartbeat";
2
+ export { heartbeat, lastHeartbeat, stepAttempt, stepInfo, stepScope, stepSignal, type StepRuntime, type StepScope } from "./heartbeat";
3
3
  export * from "./errors";
4
4
  export { exitSignal, type ExitSignal } from "./exit";
package/dist/index.js CHANGED
@@ -3,15 +3,20 @@ var KEY = Symbol.for("@iworkflow/core.step-scope");
3
3
  function stepScope(make) {
4
4
  return globalThis[KEY] ??= make();
5
5
  }
6
- function current() {
6
+ function current(name = "heartbeat") {
7
7
  const rt = globalThis[KEY]?.current();
8
8
  if (!rt)
9
- throw new Error("heartbeat вне шага: шаг движка процесса сейчас не идёт");
9
+ throw new Error(`${name} вне шага: шаг движка процесса сейчас не идёт`);
10
10
  return rt;
11
11
  }
12
12
  var heartbeat = (details) => current().heartbeat(details);
13
13
  var lastHeartbeat = async () => await current().lastHeartbeat() ?? undefined;
14
- var stepAttempt = () => current().attempt();
14
+ var stepAttempt = () => current("stepAttempt").attempt();
15
+ var stepInfo = () => {
16
+ const rt = current("stepInfo");
17
+ return { ...rt.info(), attempt: rt.attempt() };
18
+ };
19
+ var stepSignal = () => current("stepSignal").signal();
15
20
  // src/errors.ts
16
21
  class Failure extends Error {
17
22
  type;
@@ -64,6 +69,20 @@ function checkLimits(def) {
64
69
  for (const [p, n] of Object.entries(def.pools ?? {}))
65
70
  check(`pools.${p}`, n);
66
71
  }
72
+ function plainAttrs(attrs) {
73
+ if (!attrs)
74
+ return;
75
+ const plain = (_, v) => typeof v === "bigint" ? String(v) : v instanceof Error ? { ...v, name: v.name, message: v.message } : v;
76
+ const out = {};
77
+ for (const [k, v] of Object.entries(attrs)) {
78
+ try {
79
+ const json = JSON.stringify(v, plain);
80
+ if (json !== undefined)
81
+ out[k] = JSON.parse(json);
82
+ } catch {}
83
+ }
84
+ return out;
85
+ }
67
86
  // src/exit.ts
68
87
  function exitSignal() {
69
88
  let d = Promise.withResolvers();
@@ -99,6 +118,9 @@ export {
99
118
  isFlowError,
100
119
  lastHeartbeat,
101
120
  nonRetryable,
121
+ plainAttrs,
102
122
  stepAttempt,
103
- stepScope
123
+ stepInfo,
124
+ stepScope,
125
+ stepSignal
104
126
  };
package/dist/types.d.ts CHANGED
@@ -67,10 +67,12 @@ export interface FlowContext {
67
67
  */
68
68
  startChild<T>(target: FlowRef, args: unknown[], opts: StartChildOptions): Promise<ChildHandle<T>>;
69
69
  sleep(ms: number): Promise<void>;
70
- /** Ждать сигнал по теме; null — вышло время. Сигнал, пришедший раньше, не теряется. */
70
+ /** Ждать сигнал по теме; null — вышло время, при timeoutMs 0 — сигнала нет сейчас (Temporal: таймер 1 мс). Пришедший раньше не теряется. */
71
71
  recv<T>(topic: string, timeoutMs: number): Promise<T | null>;
72
72
  /** Сигнал другому процессу по id — в том числе в другом модуле. Процесса нет или он завершён — FlowError "not-found". */
73
73
  signal(workflowId: string, topic: string, value: unknown): Promise<void>;
74
+ /** Журнал из процесса: Temporal не повторяет строку при переигрывании, DBOS может повторить после рестарта. */
75
+ log(level: "info" | "warn" | "error", message: string, attrs?: Record<string, unknown>): Promise<void>;
74
76
  /** Опубликовать значение для query снаружи; читается и после завершения процесса. */
75
77
  setState(key: string, value: unknown): Promise<void>;
76
78
  /**
@@ -135,6 +137,11 @@ export interface StartOptions {
135
137
  priority?: number;
136
138
  /** Id уже занят (идёт или завершён; Temporal помнит завершённый в пределах хранения namespace) — FlowError "already-started". */
137
139
  onExisting?: "reject";
140
+ /**
141
+ * Отказ, только пока процесс идёт; завершённый id — новый запуск (ночные задачи с постоянным id).
142
+ * Включает onExisting "reject". Только Temporal: DBOS id не переиспользует — already-started.
143
+ */
144
+ reuseClosedId?: boolean;
138
145
  }
139
146
  /** Сторона вызывающего вне процесса (HTTP, CLI, тест). */
140
147
  export interface EngineClient {
@@ -144,7 +151,7 @@ export interface EngineClient {
144
151
  * Сигнал процессу; нет идущего с этим id — запустить и отдать сигнал ему. Temporal заводит новый
145
152
  * запуск и под id завершённого; DBOS id не переиспользует — сигнал уйдёт завершённому и пропадёт.
146
153
  */
147
- signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting">, topic: string, value: unknown): Promise<FlowHandle<T>>;
154
+ signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting" | "reuseClosedId">, topic: string, value: unknown): Promise<FlowHandle<T>>;
148
155
  /** Уже запущенный процесс по id. */
149
156
  handle<T>(id: string): FlowHandle<T>;
150
157
  close(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Engine-neutral durable workflow port: flows, steps, child workflows, signals, queries, patches, heartbeat",
5
5
  "keywords": [
6
6
  "workflow",
package/src/errors.ts CHANGED
@@ -85,3 +85,20 @@ export function checkLimits(def: AppDefinition): void {
85
85
  check("maxConcurrentSteps", def.maxConcurrentSteps);
86
86
  for (const [p, n] of Object.entries(def.pools ?? {})) check(`pools.${p}`, n);
87
87
  }
88
+
89
+ /** attrs журнала — в JSON-данные: BigInt строкой, Error — name, message и свои поля; непереносимый ключ выпадает один. */
90
+ export function plainAttrs(attrs: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
91
+ if (!attrs) return undefined;
92
+ const plain = (_: string, v: unknown) =>
93
+ typeof v === "bigint" ? String(v) : v instanceof Error ? { ...v, name: v.name, message: v.message } : v;
94
+ const out: Record<string, unknown> = {};
95
+ for (const [k, v] of Object.entries(attrs)) {
96
+ try {
97
+ const json = JSON.stringify(v, plain);
98
+ if (json !== undefined) out[k] = JSON.parse(json);
99
+ } catch {
100
+ // цикл или бросающий геттер — только этот ключ
101
+ }
102
+ }
103
+ return out;
104
+ }
package/src/heartbeat.ts CHANGED
@@ -6,6 +6,10 @@ export interface StepRuntime {
6
6
  lastHeartbeat(): Promise<unknown>;
7
7
  /** Номер попытки шага, с 1. */
8
8
  attempt(): number;
9
+ /** Процесс шага, запуск и шаг в нём. */
10
+ info(): { workflowId: string; runId: string; stepId: string };
11
+ /** Отмена попытки. Temporal — срок и отмена процесса, узнаёт через heartbeat; DBOS — только срок попытки. */
12
+ signal(): AbortSignal;
9
13
  }
10
14
 
11
15
  /** Область шага: какой рантайм у шага, что идёт сейчас. Самый внутренний шаг — свой, без угадывания движка. */
@@ -25,9 +29,9 @@ export function stepScope(make: () => StepScope): StepScope {
25
29
  return ((globalThis as Slot)[KEY] ??= make());
26
30
  }
27
31
 
28
- function current(): StepRuntime {
32
+ function current(name = "heartbeat"): StepRuntime {
29
33
  const rt = (globalThis as Slot)[KEY]?.current();
30
- if (!rt) throw new Error("heartbeat вне шага: шаг движка процесса сейчас не идёт");
34
+ if (!rt) throw new Error(`${name} вне шага: шаг движка процесса сейчас не идёт`);
31
35
  return rt;
32
36
  }
33
37
 
@@ -39,4 +43,14 @@ export const lastHeartbeat = async <T>(): Promise<T | undefined> =>
39
43
  ((await current().lastHeartbeat()) ?? undefined) as T | undefined;
40
44
 
41
45
  /** Номер попытки этого шага, с 1. DBOS считает заново после рестарта процесса, Temporal — нет. */
42
- export const stepAttempt = (): number => current().attempt();
46
+ export const stepAttempt = (): number => current("stepAttempt").attempt();
47
+
48
+ /** Ключ идемпотентности внешней записи — runId + stepId: id переиспользуется (reuseClosedId), runId нет. */
49
+ // У DBOS runId = workflowId: id не переиспользуется.
50
+ export const stepInfo = (): { workflowId: string; runId: string; stepId: string; attempt: number } => {
51
+ const rt = current("stepInfo");
52
+ return { ...rt.info(), attempt: rt.attempt() };
53
+ };
54
+
55
+ /** Сигнал отмены попытки: передать в fetch и т. п.; aborted — итог попытки отброшен, не оборачивать в Failure. */
56
+ export const stepSignal = (): AbortSignal => current("stepSignal").signal();
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from "./types";
2
- export { heartbeat, lastHeartbeat, stepAttempt, stepScope, type StepRuntime, type StepScope } from "./heartbeat";
2
+ export { heartbeat, lastHeartbeat, stepAttempt, stepInfo, stepScope, stepSignal, type StepRuntime, type StepScope } from "./heartbeat";
3
3
  export * from "./errors";
4
4
  export { exitSignal, type ExitSignal } from "./exit";
package/src/types.ts CHANGED
@@ -75,10 +75,12 @@ export interface FlowContext {
75
75
  */
76
76
  startChild<T>(target: FlowRef, args: unknown[], opts: StartChildOptions): Promise<ChildHandle<T>>;
77
77
  sleep(ms: number): Promise<void>;
78
- /** Ждать сигнал по теме; null — вышло время. Сигнал, пришедший раньше, не теряется. */
78
+ /** Ждать сигнал по теме; null — вышло время, при timeoutMs 0 — сигнала нет сейчас (Temporal: таймер 1 мс). Пришедший раньше не теряется. */
79
79
  recv<T>(topic: string, timeoutMs: number): Promise<T | null>;
80
80
  /** Сигнал другому процессу по id — в том числе в другом модуле. Процесса нет или он завершён — FlowError "not-found". */
81
81
  signal(workflowId: string, topic: string, value: unknown): Promise<void>;
82
+ /** Журнал из процесса: Temporal не повторяет строку при переигрывании, DBOS может повторить после рестарта. */
83
+ log(level: "info" | "warn" | "error", message: string, attrs?: Record<string, unknown>): Promise<void>;
82
84
  /** Опубликовать значение для query снаружи; читается и после завершения процесса. */
83
85
  setState(key: string, value: unknown): Promise<void>;
84
86
  /**
@@ -151,6 +153,11 @@ export interface StartOptions {
151
153
  priority?: number;
152
154
  /** Id уже занят (идёт или завершён; Temporal помнит завершённый в пределах хранения namespace) — FlowError "already-started". */
153
155
  onExisting?: "reject";
156
+ /**
157
+ * Отказ, только пока процесс идёт; завершённый id — новый запуск (ночные задачи с постоянным id).
158
+ * Включает onExisting "reject". Только Temporal: DBOS id не переиспользует — already-started.
159
+ */
160
+ reuseClosedId?: boolean;
154
161
  }
155
162
 
156
163
  /** Сторона вызывающего вне процесса (HTTP, CLI, тест). */
@@ -161,7 +168,7 @@ export interface EngineClient {
161
168
  * Сигнал процессу; нет идущего с этим id — запустить и отдать сигнал ему. Temporal заводит новый
162
169
  * запуск и под id завершённого; DBOS id не переиспользует — сигнал уйдёт завершённому и пропадёт.
163
170
  */
164
- signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting">, topic: string, value: unknown): Promise<FlowHandle<T>>;
171
+ signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting" | "reuseClosedId">, topic: string, value: unknown): Promise<FlowHandle<T>>;
165
172
  /** Уже запущенный процесс по id. */
166
173
  handle<T>(id: string): FlowHandle<T>;
167
174
  close(): Promise<void>;