@iworkflow/dbos 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/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { DBOS, DBOSClient, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
3
3
  import {
4
4
  stepScope,
5
+ checkLimits,
5
6
  checkPriority,
6
7
  DEFAULT_PRIORITY,
7
8
  exitSignal,
@@ -127,7 +128,6 @@ class Semaphore {
127
128
  class AttemptTimeout extends Error {
128
129
  }
129
130
  var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
130
- var scope = stepScope(alsScope);
131
131
  async function withTimeout(p, ms) {
132
132
  let timer;
133
133
  try {
@@ -145,7 +145,7 @@ async function attempts(fn, args, opts, pool, alive, priority, runtime) {
145
145
  let interval = opts.retryIntervalMs ?? 200;
146
146
  for (let n = 1;; n++) {
147
147
  try {
148
- const attempt = () => scope.run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60000));
148
+ const attempt = () => stepScope(alsScope).run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60000));
149
149
  return await (pool ? pool.run(attempt, priority) : attempt());
150
150
  } catch (e) {
151
151
  const stop = e instanceof Failure && e.nonRetryable || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
@@ -189,6 +189,7 @@ function dbosRuntime() {
189
189
  let pool;
190
190
  const exit = exitSignal();
191
191
  async function launch(def) {
192
+ checkLimits(def);
192
193
  pool = new Pool({ connectionString: sysDb(), max: 2 });
193
194
  const db = pool;
194
195
  const patches = patchBook(db, def.app, def.patches ?? []);
@@ -204,6 +205,7 @@ function dbosRuntime() {
204
205
  attempt: () => n
205
206
  });
206
207
  const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
208
+ const main = def.maxConcurrentSteps ? new Semaphore(def.maxConcurrentSteps) : undefined;
207
209
  const enqueue = (target, args, opts, reject, priority) => DBOS.enqueueWorkflowWithOptions({
208
210
  queueName: target.app,
209
211
  workflowName: target.flow,
@@ -234,7 +236,7 @@ function dbosRuntime() {
234
236
  priority: row?.priority || DEFAULT_PRIORITY,
235
237
  now: () => DBOS.now(),
236
238
  step: (step, stepArgs, opts = {}) => {
237
- const pool = opts.pool ? pools[opts.pool] : undefined;
239
+ const pool = opts.pool ? pools[opts.pool] : main;
238
240
  if (opts.pool && !pool)
239
241
  throw new Error(`пул "${opts.pool}" не объявлен в AppDefinition.pools модуля ${def.app}`);
240
242
  const alive = async () => {
@@ -287,15 +289,20 @@ function dbosRuntime() {
287
289
  const poll = Number(process.env.DBOS_QUEUE_POLL_MS) || undefined;
288
290
  await DBOS.registerQueue(def.app, { minPollingIntervalMs: poll, onConflict: "always_update" });
289
291
  }
292
+ const closePool = async () => {
293
+ await pool?.end().catch(() => {});
294
+ pool = undefined;
295
+ };
290
296
  return {
291
297
  done: () => exit.promise(),
292
298
  async start(def) {
299
+ if (pool)
300
+ throw new Error("рантайм уже запущен");
293
301
  exit.arm();
294
302
  try {
295
303
  await launch(def);
296
304
  } catch (e) {
297
- await pool?.end().catch(() => {});
298
- pool = undefined;
305
+ await closePool();
299
306
  exit.reject(e);
300
307
  throw e;
301
308
  }
@@ -303,8 +310,8 @@ function dbosRuntime() {
303
310
  async stop() {
304
311
  try {
305
312
  await DBOS.shutdown();
306
- await pool?.end();
307
313
  } finally {
314
+ await closePool();
308
315
  exit.resolve();
309
316
  }
310
317
  }
@@ -343,8 +350,9 @@ async function dbosClient() {
343
350
  workflowID: opts.id,
344
351
  applicationName: target.app,
345
352
  ...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {},
346
- priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY
347
- }, ...args);
353
+ priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
354
+ ...opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" } : {}
355
+ }, ...args).catch(alreadyStarted(opts.id));
348
356
  return wrap(h.workflowID);
349
357
  },
350
358
  async signalWithStart(target, args, opts, topic, value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iworkflow/dbos",
3
- "version": "0.6.0",
3
+ "version": "0.7.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.6.0",
40
+ "@iworkflow/core": "^0.7.0",
41
41
  "pg": "^8.0.0"
42
42
  },
43
43
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { DBOS, DBOSClient, Error as DBOSErrors, type WorkflowStatus } from "@dbo
4
4
  import {
5
5
  stepScope,
6
6
  type StepRuntime,
7
+ checkLimits,
7
8
  checkPriority,
8
9
  DEFAULT_PRIORITY,
9
10
  exitSignal,
@@ -166,7 +167,7 @@ class AttemptTimeout extends Error {}
166
167
 
167
168
  const sleepMs = (ms: number) => new Promise((r) => setTimeout(r, ms));
168
169
 
169
- const scope = stepScope(alsScope);
170
+
170
171
 
171
172
  /** Попытка, урезанная по сроку: брошенная продолжает идти в фоне, её итог отбрасывается. */
172
173
  async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
@@ -196,7 +197,7 @@ async function attempts(
196
197
  let interval = opts.retryIntervalMs ?? 200;
197
198
  for (let n = 1; ; n++) {
198
199
  try {
199
- const attempt = () => scope.run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000));
200
+ const attempt = () => stepScope(alsScope).run(runtime(n), () => withTimeout(fn(...args), opts.timeoutMs ?? 60_000));
200
201
  return await (pool ? pool.run(attempt, priority) : attempt());
201
202
  } catch (e) {
202
203
  const stop = (e instanceof Failure && e.nonRetryable) || (opts.nonRetryableTypes ?? []).includes(typeOf(e));
@@ -249,6 +250,7 @@ export function dbosRuntime(): EngineRuntime {
249
250
  // Воркеров у DBOS нет: done() — до stop или сбоя start; падения исполнителя DBOS не видно.
250
251
  const exit = exitSignal();
251
252
  async function launch(def: AppDefinition) {
253
+ checkLimits(def);
252
254
  pool = new Pool({ connectionString: sysDb(), max: 2 });
253
255
  const db = pool;
254
256
  const patches = patchBook(db, def.app, def.patches ?? []);
@@ -268,6 +270,8 @@ export function dbosRuntime(): EngineRuntime {
268
270
  attempt: () => n,
269
271
  });
270
272
  const pools = Object.fromEntries(Object.entries(def.pools ?? {}).map(([p, n]) => [p, new Semaphore(n)]));
273
+ // Шаги вне пулов — под общим пределом модуля, как maxConcurrentActivityTaskExecutions у Temporal.
274
+ const main = def.maxConcurrentSteps ? new Semaphore(def.maxConcurrentSteps) : undefined;
271
275
  const enqueue = (
272
276
  target: { app: string; flow: string },
273
277
  args: unknown[],
@@ -312,7 +316,7 @@ export function dbosRuntime(): EngineRuntime {
312
316
  priority: row?.priority || DEFAULT_PRIORITY,
313
317
  now: () => DBOS.now(),
314
318
  step: (step, stepArgs, opts = {}) => {
315
- const pool = opts.pool ? pools[opts.pool] : undefined;
319
+ const pool = opts.pool ? pools[opts.pool] : main;
316
320
  if (opts.pool && !pool) throw new Error(`пул "${opts.pool}" не объявлен в AppDefinition.pools модуля ${def.app}`);
317
321
  // heartbeatTimeoutMs у DBOS не действует: умерший процесс ловится рестартом, шаг повторяется.
318
322
  // Срок вышел, а DBOS ещё не отменил процесс — ждём отмены: иначе процесс поймает отказ и завершится успехом.
@@ -382,16 +386,21 @@ export function dbosRuntime(): EngineRuntime {
382
386
  const poll = Number(process.env.DBOS_QUEUE_POLL_MS) || undefined;
383
387
  await DBOS.registerQueue(def.app, { minPollingIntervalMs: poll, onConflict: "always_update" });
384
388
  }
389
+ const closePool = async () => {
390
+ await pool?.end().catch(() => {});
391
+ pool = undefined;
392
+ };
385
393
  return {
386
394
  done: () => exit.promise(),
387
395
  async start(def: AppDefinition) {
396
+ // Второй start не трогает пул и done() идущего: его сбой закрыл бы живое соединение.
397
+ if (pool) throw new Error("рантайм уже запущен");
388
398
  exit.arm();
389
399
  // Не запустился — done() отклоняется: для наблюдателя это сбой, а не штатный выход.
390
400
  try {
391
401
  await launch(def);
392
402
  } catch (e) {
393
- await pool?.end().catch(() => {});
394
- pool = undefined;
403
+ await closePool();
395
404
  exit.reject(e);
396
405
  throw e;
397
406
  }
@@ -399,8 +408,9 @@ export function dbosRuntime(): EngineRuntime {
399
408
  async stop() {
400
409
  try {
401
410
  await DBOS.shutdown();
402
- await pool?.end();
403
411
  } finally {
412
+ // Пул закрывается и при сбое shutdown: иначе соединения копятся от цикла к циклу.
413
+ await closePool();
404
414
  exit.resolve();
405
415
  }
406
416
  },
@@ -444,12 +454,13 @@ export async function dbosClient(): Promise<EngineClient> {
444
454
  ...(opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {}),
445
455
  // Без приоритета DBOS ставит 0 — впереди всех; порт по умолчанию 3, как Temporal.
446
456
  priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
457
+ ...(opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" as const } : {}),
447
458
  },
448
459
  ...args,
449
- );
460
+ ).catch(alreadyStarted(opts.id));
450
461
  return wrap(h.workflowID);
451
462
  },
452
- async signalWithStart<T>(target: FlowRef, args: unknown[], opts: StartOptions, topic: string, value: unknown) {
463
+ async signalWithStart<T>(target: FlowRef, args: unknown[], opts: Omit<StartOptions, "onExisting">, topic: string, value: unknown) {
453
464
  // Не атомарно, но без потерь: сигнал ждёт в очереди темы, пока процесс до него дойдёт.
454
465
  const h = await self.start<T>(target, args, opts);
455
466
  // id завершённого DBOS не переиспользует: сигнал ушёл бы в никуда.