@iworkflow/dbos 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/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  stepScope,
5
5
  checkLimits,
6
6
  checkPriority,
7
+ plainAttrs,
7
8
  DEFAULT_PRIORITY,
8
9
  exitSignal,
9
10
  Failure,
@@ -70,7 +71,7 @@ function stepKey() {
70
71
  const wf = DBOS.workflowID;
71
72
  const step = DBOS.stepID;
72
73
  if (!wf || step === undefined)
73
- throw new Error("heartbeat вне шага DBOS");
74
+ throw new Error("вызов вне шага DBOS");
74
75
  return [wf, step];
75
76
  }
76
77
  var typeOf = (e) => e instanceof Failure ? e.type : e instanceof Error ? e.name : "Error";
@@ -128,12 +129,16 @@ class Semaphore {
128
129
  class AttemptTimeout extends Error {
129
130
  }
130
131
  var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
131
- async function withTimeout(p, ms) {
132
+ async function withTimeout(p, ms, abort) {
132
133
  let timer;
133
134
  try {
134
135
  return await Promise.race([
135
136
  p,
136
- new Promise((_, reject) => timer = setTimeout(() => reject(new AttemptTimeout(`попытка дольше ${ms} мс`)), ms))
137
+ new Promise((_, reject) => timer = setTimeout(() => {
138
+ const e = new AttemptTimeout(`попытка дольше ${ms} мс`);
139
+ abort.abort(e);
140
+ reject(e);
141
+ }, ms))
137
142
  ]);
138
143
  } finally {
139
144
  clearTimeout(timer);
@@ -145,7 +150,10 @@ async function attempts(fn, args, opts, pool, alive, priority, runtime) {
145
150
  let interval = opts.retryIntervalMs ?? 200;
146
151
  for (let n = 1;; n++) {
147
152
  try {
148
- const attempt = () => stepScope(alsScope).run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60000));
153
+ const attempt = () => {
154
+ const abort = new AbortController;
155
+ return stepScope(alsScope).run(runtime(n, abort.signal), () => withTimeout(fn(...args), opts.timeoutMs ?? 60000, abort));
156
+ };
149
157
  return await (pool ? pool.run(attempt, priority) : attempt());
150
158
  } catch (e) {
151
159
  const stop = e instanceof Failure && e.nonRetryable || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
@@ -193,7 +201,7 @@ function dbosRuntime() {
193
201
  pool = new Pool({ connectionString: sysDb(), max: 2 });
194
202
  const db = pool;
195
203
  const patches = patchBook(db, def.app, def.patches ?? []);
196
- const stepRuntime = (n) => ({
204
+ const stepRuntime = (n, signal) => ({
197
205
  async heartbeat(details) {
198
206
  await db.query(`INSERT INTO iwf.heartbeat (workflow_id, step_id, details) VALUES ($1, $2, $3)
199
207
  ON CONFLICT (workflow_id, step_id) DO UPDATE SET details = EXCLUDED.details, at = now()`, [...stepKey(), JSON.stringify(details ?? null)]);
@@ -202,7 +210,12 @@ function dbosRuntime() {
202
210
  const r = await db.query("SELECT details FROM iwf.heartbeat WHERE workflow_id = $1 AND step_id = $2", stepKey());
203
211
  return r.rows[0]?.details ?? undefined;
204
212
  },
205
- attempt: () => n
213
+ attempt: () => n,
214
+ info: () => {
215
+ const [wf, step] = stepKey();
216
+ return { workflowId: wf, runId: wf, stepId: String(step) };
217
+ },
218
+ signal: () => signal
206
219
  });
207
220
  const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
208
221
  const main = def.maxConcurrentSteps ? new Semaphore(def.maxConcurrentSteps) : undefined;
@@ -271,6 +284,10 @@ function dbosRuntime() {
271
284
  throw new FlowError(`процесса ${target} нет или он завершён`, { reason: "not-found" });
272
285
  await DBOS.send(target, value, topic);
273
286
  },
287
+ log: async (level, message, attrs) => {
288
+ const plain = plainAttrs(attrs);
289
+ console[level](message, ...plain ? [plain] : []);
290
+ },
274
291
  setState: (key, value) => DBOS.setEvent(key, value),
275
292
  patched: (id) => patches.patched(workflowId, id),
276
293
  deprecatePatch: async () => {}
@@ -351,7 +368,7 @@ async function dbosClient() {
351
368
  applicationName: target.app,
352
369
  ...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {},
353
370
  priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
354
- ...opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" } : {}
371
+ ...opts.onExisting === "reject" || opts.reuseClosedId ? { workflowIDReusePolicy: "reject" } : {}
355
372
  }, ...args).catch(alreadyStarted(opts.id));
356
373
  return wrap(h.workflowID);
357
374
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/dbos",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "DBOS adapter for @iworkflow/core",
5
5
  "keywords": [
6
6
  "workflow",
@@ -37,7 +37,7 @@
37
37
  "prepack": "bun run build"
38
38
  },
39
39
  "dependencies": {
40
- "@iworkflow/core": "^0.7.0",
40
+ "@iworkflow/core": "^0.9.0",
41
41
  "pg": "^8.0.0"
42
42
  },
43
43
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  type StepRuntime,
7
7
  checkLimits,
8
8
  checkPriority,
9
+ plainAttrs,
9
10
  DEFAULT_PRIORITY,
10
11
  exitSignal,
11
12
  Failure,
@@ -98,7 +99,7 @@ function patchBook(db: Pool, app: string, declared: string[]) {
98
99
  function stepKey(): [string, number] {
99
100
  const wf = DBOS.workflowID;
100
101
  const step = DBOS.stepID;
101
- if (!wf || step === undefined) throw new Error("heartbeat вне шага DBOS");
102
+ if (!wf || step === undefined) throw new Error("вызов вне шага DBOS");
102
103
  return [wf, step];
103
104
  }
104
105
 
@@ -170,12 +171,17 @@ const sleepMs = (ms: number) => new Promise((r) => setTimeout(r, ms));
170
171
 
171
172
 
172
173
  /** Попытка, урезанная по сроку: брошенная продолжает идти в фоне, её итог отбрасывается. */
173
- async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
174
+ async function withTimeout<T>(p: Promise<T>, ms: number, abort: AbortController): Promise<T> {
174
175
  let timer: ReturnType<typeof setTimeout> | undefined;
175
176
  try {
176
177
  return await Promise.race([
177
178
  p,
178
- new Promise<never>((_, reject) => (timer = setTimeout(() => reject(new AttemptTimeout(`попытка дольше ${ms} мс`)), ms))),
179
+ new Promise<never>((_, reject) => (timer = setTimeout(() => {
180
+ const e = new AttemptTimeout(`попытка дольше ${ms} мс`);
181
+ // Брошенная попытка видит отмену через stepSignal() и может остановиться сама.
182
+ abort.abort(e);
183
+ reject(e);
184
+ }, ms))),
179
185
  ]);
180
186
  } finally {
181
187
  clearTimeout(timer);
@@ -190,14 +196,17 @@ async function attempts(
190
196
  pool: Semaphore | undefined,
191
197
  alive: () => Promise<boolean>,
192
198
  priority: number | undefined,
193
- runtime: (n: number) => StepRuntime,
199
+ runtime: (n: number, signal: AbortSignal) => StepRuntime,
194
200
  ) {
195
201
  const max = opts.attempts ?? 3;
196
202
  const started = Date.now();
197
203
  let interval = opts.retryIntervalMs ?? 200;
198
204
  for (let n = 1; ; n++) {
199
205
  try {
200
- const attempt = () => stepScope(alsScope).run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000));
206
+ const attempt = () => {
207
+ const abort = new AbortController();
208
+ return stepScope(alsScope).run(runtime(n, abort.signal), () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000, abort));
209
+ };
201
210
  return await (pool ? pool.run(attempt, priority) : attempt());
202
211
  } catch (e) {
203
212
  const stop = (e instanceof Failure && e.nonRetryable) || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
@@ -255,7 +264,7 @@ export function dbosRuntime(): EngineRuntime {
255
264
  const db = pool;
256
265
  const patches = patchBook(db, def.app, def.patches ?? []);
257
266
  // Рантайм попытки шага: прогресс — в iwf.heartbeat по (процесс, шаг), номер — из цикла повторов.
258
- const stepRuntime = (n: number): StepRuntime => ({
267
+ const stepRuntime = (n: number, signal: AbortSignal): StepRuntime => ({
259
268
  async heartbeat(details) {
260
269
  await db.query(
261
270
  `INSERT INTO iwf.heartbeat (workflow_id, step_id, details) VALUES ($1, $2, $3)
@@ -268,6 +277,12 @@ export function dbosRuntime(): EngineRuntime {
268
277
  return r.rows[0]?.details ?? undefined;
269
278
  },
270
279
  attempt: () => n,
280
+ // id DBOS не переиспользуется: запуск у процесса один.
281
+ info: () => {
282
+ const [wf, step] = stepKey();
283
+ return { workflowId: wf, runId: wf, stepId: String(step) };
284
+ },
285
+ signal: () => signal,
271
286
  });
272
287
  const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
273
288
  // Шаги вне пулов — под общим пределом модуля, как maxConcurrentActivityTaskExecutions у Temporal.
@@ -363,6 +378,12 @@ export function dbosRuntime(): EngineRuntime {
363
378
  throw new FlowError(`процесса ${target} нет или он завершён`, { reason: "not-found" });
364
379
  await DBOS.send(target, value, topic);
365
380
  },
381
+ // Не шагом: строка журнала не сдвигает нумерацию шагов. Цена — повтор строки при восстановлении после рестарта.
382
+ log: async (level, message, attrs) => {
383
+ const plain = plainAttrs(attrs);
384
+ // console, а не DBOS.logger: у того logLevel "warn" — info молча пропал бы.
385
+ console[level](message, ...(plain ? [plain] : []));
386
+ },
366
387
  setState: (key, value) => DBOS.setEvent(key, value),
367
388
  patched: (id) => patches.patched(workflowId, id),
368
389
  // Старых процессов нет — ветка всегда новая; вызов можно удалять.
@@ -454,13 +475,13 @@ export async function dbosClient(): Promise<EngineClient> {
454
475
  ...(opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {}),
455
476
  // Без приоритета DBOS ставит 0 — впереди всех; порт по умолчанию 3, как Temporal.
456
477
  priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
457
- ...(opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" as const } : {}),
478
+ ...(opts.onExisting === "reject" || opts.reuseClosedId ? { workflowIDReusePolicy: "reject" as const } : {}),
458
479
  },
459
480
  ...args,
460
481
  ).catch(alreadyStarted(opts.id));
461
482
  return wrap(h.workflowID);
462
483
  },
463
- async signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting">, topic: string, value: unknown) {
484
+ async signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting" | "reuseClosedId">, topic: string, value: unknown) {
464
485
  // Не атомарно, но без потерь: сигнал ждёт в очереди темы, пока процесс до него дойдёт.
465
486
  const h = await self.start<T>(target, args, opts);
466
487
  // id завершённого DBOS не переиспользует: сигнал ушёл бы в никуда.