@iworkflow/dbos 0.6.1 → 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,
@@ -188,6 +189,7 @@ function dbosRuntime() {
188
189
  let pool;
189
190
  const exit = exitSignal();
190
191
  async function launch(def) {
192
+ checkLimits(def);
191
193
  pool = new Pool({ connectionString: sysDb(), max: 2 });
192
194
  const db = pool;
193
195
  const patches = patchBook(db, def.app, def.patches ?? []);
@@ -203,6 +205,7 @@ function dbosRuntime() {
203
205
  attempt: () => n
204
206
  });
205
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;
206
209
  const enqueue = (target, args, opts, reject, priority) => DBOS.enqueueWorkflowWithOptions({
207
210
  queueName: target.app,
208
211
  workflowName: target.flow,
@@ -233,7 +236,7 @@ function dbosRuntime() {
233
236
  priority: row?.priority || DEFAULT_PRIORITY,
234
237
  now: () => DBOS.now(),
235
238
  step: (step, stepArgs, opts = {}) => {
236
- const pool = opts.pool ? pools[opts.pool] : undefined;
239
+ const pool = opts.pool ? pools[opts.pool] : main;
237
240
  if (opts.pool && !pool)
238
241
  throw new Error(`пул "${opts.pool}" не объявлен в AppDefinition.pools модуля ${def.app}`);
239
242
  const alive = async () => {
@@ -286,15 +289,20 @@ function dbosRuntime() {
286
289
  const poll = Number(process.env.DBOS_QUEUE_POLL_MS) || undefined;
287
290
  await DBOS.registerQueue(def.app, { minPollingIntervalMs: poll, onConflict: "always_update" });
288
291
  }
292
+ const closePool = async () => {
293
+ await pool?.end().catch(() => {});
294
+ pool = undefined;
295
+ };
289
296
  return {
290
297
  done: () => exit.promise(),
291
298
  async start(def) {
299
+ if (pool)
300
+ throw new Error("рантайм уже запущен");
292
301
  exit.arm();
293
302
  try {
294
303
  await launch(def);
295
304
  } catch (e) {
296
- await pool?.end().catch(() => {});
297
- pool = undefined;
305
+ await closePool();
298
306
  exit.reject(e);
299
307
  throw e;
300
308
  }
@@ -302,8 +310,8 @@ function dbosRuntime() {
302
310
  async stop() {
303
311
  try {
304
312
  await DBOS.shutdown();
305
- await pool?.end();
306
313
  } finally {
314
+ await closePool();
307
315
  exit.resolve();
308
316
  }
309
317
  }
@@ -342,8 +350,9 @@ async function dbosClient() {
342
350
  workflowID: opts.id,
343
351
  applicationName: target.app,
344
352
  ...opts.timeoutMs ? { workflowTimeoutMS: opts.timeoutMs } : {},
345
- priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY
346
- }, ...args);
353
+ priority: checkPriority(opts.priority) ?? DEFAULT_PRIORITY,
354
+ ...opts.onExisting === "reject" ? { workflowIDReusePolicy: "reject" } : {}
355
+ }, ...args).catch(alreadyStarted(opts.id));
347
356
  return wrap(h.workflowID);
348
357
  },
349
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.1",
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.1",
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,
@@ -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 не переиспользует: сигнал ушёл бы в никуда.