@adonis-agora/durable 0.55.0 → 0.56.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.
@@ -13,7 +13,7 @@ import { Entities } from './entities.js';
13
13
  import { ContinueAsNew, GatherError, NonDeterminismError, RemoteStepTimeout, RemoteWorkflowTimeout, WorkflowSuspended, } from './errors.js';
14
14
  import { EventAccumulators } from './event-accumulators.js';
15
15
  import { eventMatches, eventMatchOf, eventPrefix } from './events.js';
16
- import { childIdsOf, FOLDED_PREFIX, fanOfWaiter, parseStepFanGroup, stepFanGroup, } from './fan-join.js';
16
+ import { childIdsOf, FOLDED_PREFIX, fanOfWaiter, parentRunIdOf, parseStepFanGroup, stepFanGroup, } from './fan-join.js';
17
17
  import { aggregateAnnouncements } from './handshake/announced.js';
18
18
  import { buildWorkflowDirectory } from './handshake/directory.js';
19
19
  import { isNonEmptyRunScope, TERMINAL_RUN_STATUSES } from './interfaces.js';
@@ -1396,8 +1396,7 @@ export class WorkflowEngine {
1396
1396
  await registered.validateInput?.(parsed);
1397
1397
  // Idempotent by run id: a redelivered trigger (at-least-once queues) or a scheduler re-tick for
1398
1398
  // the same id is a no-op, returning the existing run's state instead of starting a duplicate.
1399
- const prior = await this.store.getRun(runId);
1400
- if (prior) {
1399
+ const existing = async (prior) => {
1401
1400
  // A PENDING prior still needs an executor: re-nudge the dispatcher so a redelivered trigger —
1402
1401
  // or a continuation persisted pre-settle (see the ContinueAsNew handoff) picked up by a custom
1403
1402
  // dispatcher — doesn't sit `pending` until the next poll. Idempotent: execution is leased, so
@@ -1409,6 +1408,15 @@ export class WorkflowEngine {
1409
1408
  });
1410
1409
  }
1411
1410
  return { runId, status: prior.status, output: prior.output, error: prior.error };
1411
+ };
1412
+ // A quota or a singleton can reject a start, and must not reject one for a run that already
1413
+ // exists, so those workflows look the run up first. Any other start goes straight to the insert,
1414
+ // whose duplicate-id rejection finds the existing run below.
1415
+ const admits = registered.singleton || registered.concurrency || opts?.concurrency;
1416
+ if (admits) {
1417
+ const prior = await this.store.getRun(runId);
1418
+ if (prior)
1419
+ return existing(prior);
1412
1420
  }
1413
1421
  // Start-time concurrency quota (per-start override, else the workflow's): reject BEFORE creating
1414
1422
  // anything when the key's in-flight runs already reach the limit. See {@link ConcurrencyQuota}.
@@ -1456,17 +1464,15 @@ export class WorkflowEngine {
1456
1464
  await this.store.createRun(run);
1457
1465
  }
1458
1466
  catch (err) {
1459
- // Lost a true createRun RACE (two pods scheduler-ticking the same window, concurrent
1460
- // `signalWithStart`s for one entity id): the read-before-create above saw no prior, but the
1461
- // store's duplicate-id rejection says someone else won the insert in the gap. Converge on the
1462
- // winner — re-read and report ITS state, exactly like the prior-run branch — instead of
1463
- // surfacing a unique-constraint error (which would abort a scheduler tick, or worse, lose the
1464
- // signal half of a signalWithStart). A re-read that finds nothing means the throw was a real
1465
- // store failure: rethrow it.
1467
+ // The run id exists: a redelivered trigger, or a start that lost the insert to a racing one
1468
+ // (two pods scheduler-ticking the same window, concurrent `signalWithStart`s for one entity id).
1469
+ // Converge on the existing run — re-read and report ITS state, exactly like the prior-run
1470
+ // branch — instead of surfacing a unique-constraint error (which would abort a scheduler tick,
1471
+ // or worse, lose the signal half of a signalWithStart). A re-read that finds nothing means the
1472
+ // throw was a real store failure: rethrow it.
1466
1473
  const winner = await this.store.getRun(runId);
1467
- if (winner) {
1468
- return { runId, status: winner.status, output: winner.output, error: winner.error };
1469
- }
1474
+ if (winner)
1475
+ return existing(winner);
1470
1476
  throw err;
1471
1477
  }
1472
1478
  // The run is durably enqueued; a dispatcher (in-process by default) executes it — `start` does
@@ -3353,6 +3359,9 @@ export class WorkflowEngine {
3353
3359
  await this.deliverSignal(waiter, completion);
3354
3360
  return;
3355
3361
  }
3362
+ // No waiter, and no parent that could consume a buffered copy (a root run): nothing to do.
3363
+ if (parentRunIdOf(token.slice(CHILD_TOKEN_PREFIX.length)) === undefined)
3364
+ return;
3356
3365
  // No waiter: the parent may already have settled this child from its run row (see
3357
3366
  // `markChildDelivered`); a buffered copy would then never be consumed.
3358
3367
  const delivered = async () => childId !== undefined &&