@adonis-agora/durable 0.50.0 → 0.51.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/commands/work.d.ts.map +1 -1
  3. package/dist/commands/work.js +3 -1
  4. package/dist/commands/work.js.map +1 -1
  5. package/dist/providers/durable_provider.d.ts.map +1 -1
  6. package/dist/providers/durable_provider.js +3 -1
  7. package/dist/providers/durable_provider.js.map +1 -1
  8. package/dist/src/commands/worker.d.ts +4 -2
  9. package/dist/src/commands/worker.d.ts.map +1 -1
  10. package/dist/src/commands/worker.js +8 -5
  11. package/dist/src/commands/worker.js.map +1 -1
  12. package/dist/src/dashboard/index.d.ts +1 -1
  13. package/dist/src/dashboard/index.js +1 -1
  14. package/dist/src/engine.d.ts +10 -1
  15. package/dist/src/engine.d.ts.map +1 -1
  16. package/dist/src/engine.js +55 -17
  17. package/dist/src/engine.js.map +1 -1
  18. package/dist/src/errors.d.ts +15 -5
  19. package/dist/src/errors.d.ts.map +1 -1
  20. package/dist/src/errors.js +11 -7
  21. package/dist/src/errors.js.map +1 -1
  22. package/dist/src/index.d.ts +1 -1
  23. package/dist/src/index.js +1 -1
  24. package/dist/src/on-error.d.ts +4 -1
  25. package/dist/src/on-error.d.ts.map +1 -1
  26. package/dist/src/on-error.js +15 -2
  27. package/dist/src/on-error.js.map +1 -1
  28. package/dist/src/scheduler.d.ts +44 -3
  29. package/dist/src/scheduler.d.ts.map +1 -1
  30. package/dist/src/scheduler.js +36 -1
  31. package/dist/src/scheduler.js.map +1 -1
  32. package/dist/src/standard-schema.d.ts +8 -5
  33. package/dist/src/standard-schema.d.ts.map +1 -1
  34. package/dist/src/standard-schema.js +9 -7
  35. package/dist/src/standard-schema.js.map +1 -1
  36. package/dist/src/step-discovery.js +2 -2
  37. package/dist/src/step-discovery.js.map +1 -1
  38. package/dist/src/workflow-ref.d.ts +4 -2
  39. package/dist/src/workflow-ref.d.ts.map +1 -1
  40. package/dist/src/workflow-ref.js.map +1 -1
  41. package/package.json +1 -1
  42. package/skills/durable-workflows/SKILL.md +15 -3
@@ -24,7 +24,7 @@ import { loadFanMembers, loadRunDetail, } from './run-detail-summary.js';
24
24
  import { runFacetsOf } from './run-facets.js';
25
25
  import { scanRunValueFacets } from './run-value-facets.js';
26
26
  import { listSignalWaitersOfRuns } from './run-waiting.js';
27
- import { scheduleWindow } from './scheduler.js';
27
+ import { resolveSchedule, scheduleWindow } from './scheduler.js';
28
28
  import { ScheduleClient } from './schedules.js';
29
29
  import { SINGLETON_RESERVED_TAGS, withoutAdmission, withSingletonTracked, } from './singleton-admission.js';
30
30
  import { SingletonGate } from './singleton-gate.js';
@@ -547,7 +547,7 @@ export class WorkflowEngine {
547
547
  * Called by `runTick`; idempotent.
548
548
  */
549
549
  adoptSchedules(schedules) {
550
- this.#allSchedules = schedules;
550
+ this.#allSchedules = schedules.map(resolveSchedule);
551
551
  }
552
552
  /**
553
553
  * The runtime pause override for a schedule key: `true`/`false` when the console set one (wins
@@ -626,7 +626,7 @@ export class WorkflowEngine {
626
626
  continue;
627
627
  }
628
628
  seen.add(schedule.key);
629
- this.#discoveredSchedules.push(schedule);
629
+ this.#discoveredSchedules.push(resolveSchedule(schedule));
630
630
  }
631
631
  }
632
632
  /** Colocated schedules discovered from `static schedule`, for the worker loop to merge with config. */
@@ -1061,11 +1061,15 @@ export class WorkflowEngine {
1061
1061
  : `workflow ${name}@${pinned} is not registered`);
1062
1062
  }
1063
1063
  // Validate the input up front — a bad payload is rejected before any run is created. The schema
1064
- // runs first and its OUTPUT (parsed/coerced) is what the run stores; `validateInput` then sees it.
1065
- if (registered.inputSchema) {
1066
- input = await validateStandard(registered.inputSchema, input, `workflow "${name}" input`);
1067
- }
1068
- await registered.validateInput?.(input);
1064
+ // runs first and `validateInput` sees its OUTPUT (parsed/coerced), but the run stores the input
1065
+ // AS SENT: the body gets the parsed value by re-parsing it before every turn (`turnInput`), so a
1066
+ // value JSON can't carry (a `Date` from `z.coerce.date()`) reaches it the same way on every
1067
+ // turn, whichever store holds the row. Everything else here — singleton and concurrency keys
1068
+ // included — reads the stored input, the one value the singleton gate re-reads later.
1069
+ const parsed = registered.inputSchema
1070
+ ? await validateStandard(registered.inputSchema, input, `workflow "${name}" input`, 'invalid_input')
1071
+ : input;
1072
+ await registered.validateInput?.(parsed);
1069
1073
  // Idempotent by run id: a redelivered trigger (at-least-once queues) or a scheduler re-tick for
1070
1074
  // the same id is a no-op, returning the existing run's state instead of starting a duplicate.
1071
1075
  const prior = await this.store.getRun(runId);
@@ -3689,10 +3693,28 @@ export class WorkflowEngine {
3689
3693
  return this.settleRun(run, await this.followHandoff(run, handoff));
3690
3694
  }
3691
3695
  const history = await this.remoteHistory(run.id, checkpoints);
3696
+ // The remote body gets the parsed input too, re-parsed here each turn like an inline one. It
3697
+ // travels as JSON, so a value JSON can't carry arrives as its JSON form — but coercions and
3698
+ // defaults do apply. Only the TASK carries it: `run` (and so the store) keeps the input as sent.
3699
+ let turnRun;
3700
+ try {
3701
+ turnRun = registered.inputSchema
3702
+ ? { ...run, input: await this.turnInput(run, registered) }
3703
+ : run;
3704
+ }
3705
+ catch (err) {
3706
+ return this.settleRun(run, {
3707
+ kind: 'failed',
3708
+ error: {
3709
+ message: err instanceof Error ? err.message : String(err),
3710
+ ...codeAndDetails(err),
3711
+ },
3712
+ });
3713
+ }
3692
3714
  // Broker-backed executors: dispatch the turn and suspend on it. The decision is applied by
3693
3715
  // whichever replica consumes it (completeRemoteDecision), never awaited here.
3694
3716
  if (remote.executor.dispatch)
3695
- return this.dispatchRemoteTurn(run, registered, history);
3717
+ return this.dispatchRemoteTurn(turnRun, registered, history);
3696
3718
  // INLINE advance (an executor that produces the decision in-process): await it under the lease.
3697
3719
  const advance = remote.executor.advance;
3698
3720
  if (!advance) {
@@ -3701,7 +3723,7 @@ export class WorkflowEngine {
3701
3723
  let decision;
3702
3724
  try {
3703
3725
  const silenceMs = this.remoteAdvanceSilenceMs;
3704
- const pending = advance.call(remote.executor, run, history);
3726
+ const pending = advance.call(remote.executor, turnRun, history);
3705
3727
  decision =
3706
3728
  silenceMs != null
3707
3729
  ? await this.awaitTurnWithSilence(run.id, pending, silenceMs)
@@ -4642,6 +4664,19 @@ export class WorkflowEngine {
4642
4664
  return { runId: run.id, status: 'cancelled', error };
4643
4665
  }
4644
4666
  /** The run body, lease held + renewed by {@link execute}. */
4667
+ /**
4668
+ * The input a turn's body receives: the stored input parsed by the workflow's `inputSchema`, or the
4669
+ * stored input itself without one. Re-parsed on every turn rather than stored parsed, because the
4670
+ * store round-trips the row through JSON (on the SQL stores) or a structured clone (in memory): a
4671
+ * stored parsed value would reach the body as a `Date` in tests and a string in production. A
4672
+ * stored input that no longer parses — its schema changed without a version bump — throws the same
4673
+ * `SchemaValidationError` (`invalid_input`, never retried) that fails the run.
4674
+ */
4675
+ async turnInput(run, registered) {
4676
+ if (!registered?.inputSchema)
4677
+ return run.input;
4678
+ return validateStandard(registered.inputSchema, run.input, `workflow "${run.workflow}" input`, 'invalid_input');
4679
+ }
4645
4680
  async runExecution(run, fn, registered) {
4646
4681
  // First execution of an enqueued run: mark it running and announce the start, BEFORE the singleton
4647
4682
  // gate, so an admitted run's body never starts while it still reads `pending`. A resumed run is
@@ -4689,7 +4724,8 @@ export class WorkflowEngine {
4689
4724
  // Establish the ambient ctx for the duration of this body turn so context-aware statics
4690
4725
  // (`BaseWorkflow.start`/`dispatch`) reachable from `fn` route through this run's `ctx.child`/
4691
4726
  // `ctx.startChild`. Re-set on every replay turn (each is its own async scope) — correct.
4692
- const output = await workflowAls.run(ctx, () => fn(ctx, run.input));
4727
+ const input = await this.turnInput(run, registered);
4728
+ const output = await workflowAls.run(ctx, () => fn(ctx, input));
4693
4729
  // A compensating cancel requested while this turn ran (or one whose resume replays straight to
4694
4730
  // `return`, with no suspension point left) still wins: undo + settle cancelled, never completed.
4695
4731
  if (await this.takeCancelIntent(run.id)) {
@@ -4918,7 +4954,7 @@ export class WorkflowEngine {
4918
4954
  throw new Error('dispatched compensation requires a Transport');
4919
4955
  const token = stepGroup(def, run.namespace);
4920
4956
  const validInput = def.input
4921
- ? await validateStandard(def.input, args, `step "${def.name}" input`)
4957
+ ? await validateStandard(def.input, args, `step "${def.name}" input`, 'invalid_input')
4922
4958
  : args;
4923
4959
  const id = `${run.id}:compensate:${def.name}:${attempt}`;
4924
4960
  const name = `compensate:${def.name}`;
@@ -5087,7 +5123,7 @@ export class WorkflowEngine {
5087
5123
  ?.searchAttributesSchema
5088
5124
  : undefined;
5089
5125
  if (schema) {
5090
- validateStandardSync(schema, merged, `workflow "${run?.workflow}": ctx.upsertSearchAttributes`);
5126
+ validateStandardSync(schema, merged, `workflow "${run?.workflow}" ctx.upsertSearchAttributes`);
5091
5127
  }
5092
5128
  await this.store.updateRun(runId, {
5093
5129
  searchAttributes: merged,
@@ -5418,7 +5454,7 @@ export class WorkflowEngine {
5418
5454
  // Optional runtime schema: validate only when the def carries one (a bare `@Step()` / string-name
5419
5455
  // call carries none — the engine passes the input through untouched then).
5420
5456
  const validInput = step.input
5421
- ? await validateStandard(step.input, input, `step "${step.name}" input`)
5457
+ ? await validateStandard(step.input, input, `step "${step.name}" input`, 'invalid_input')
5422
5458
  : input;
5423
5459
  // Routing token: BY NAME (sanitized for brokers), optionally partition-suffixed. Computed once and
5424
5460
  // used at BOTH the checkpoint's workerGroup and the dispatched task's routing `group`, so the same
@@ -5567,7 +5603,9 @@ export class WorkflowEngine {
5567
5603
  async sendStepFan(runId, positions, step, inputs, batch, group, opts, runNamespace) {
5568
5604
  const token = stepGroup(step, runNamespace);
5569
5605
  await this.ensureRoutable(token, step.requires);
5570
- const input = (i) => step.input ? validateStandard(step.input, inputs[i], `step "${step.name}" input`) : inputs[i];
5606
+ const input = (i) => step.input
5607
+ ? validateStandard(step.input, inputs[i], `step "${step.name}" input`, 'invalid_input')
5608
+ : inputs[i];
5571
5609
  const validInputs = new Map();
5572
5610
  for (const i of batch)
5573
5611
  validInputs.set(i, await input(i));
@@ -5999,7 +6037,7 @@ export class WorkflowEngine {
5999
6037
  if (this.pool.size === 0)
6000
6038
  throw new Error('remote steps require a Transport');
6001
6039
  const validInput = step.input
6002
- ? await validateStandard(step.input, input, `step "${step.name}" input`)
6040
+ ? await validateStandard(step.input, input, `step "${step.name}" input`, 'invalid_input')
6003
6041
  : input;
6004
6042
  const token = stepGroup(step, runNamespace);
6005
6043
  // Capability/protocol guard (design §7.5) — same as the durable path, before the `step.started`
@@ -6118,7 +6156,7 @@ export class WorkflowEngine {
6118
6156
  /** Shared finalization for a remote resolution, however it arrived (live await or salvaged). */
6119
6157
  async settleRemoteAttempt(runId, seq, step, input, resolution, attempt, token, enqueuedAt) {
6120
6158
  const output = (step.output
6121
- ? await validateStandard(step.output, resolution.output, `step "${step.name}" output`)
6159
+ ? await validateStandard(step.output, resolution.output, `step "${step.name}" output`, 'invalid_output')
6122
6160
  : resolution.output);
6123
6161
  // The worker reports when it actually picked the task up; fall back to dispatch time if a
6124
6162
  // transport doesn't carry it (queue-wait then reads as zero rather than going negative).