@nestjs/workflows 0.0.1 → 0.0.2

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.
@@ -49,6 +49,11 @@ export declare class ResultWaiter<T = unknown> {
49
49
  * connections idle at that moment, and one that answers later stays open until its keep-alive timeout.
50
50
  */
51
51
  close(): Promise<void>;
52
+ /**
53
+ * Reads `id`, unless the wait settles first. It races a promise of its own, not one the whole wait shares: each
54
+ * race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
55
+ */
56
+ private read;
52
57
  private add;
53
58
  private remove;
54
59
  }
@@ -47,13 +47,11 @@ export class ResultWaiter {
47
47
  const timeoutMs = options.timeout === undefined ? Infinity : parseDuration(options.timeout);
48
48
  const deadline = performance.now() + timeoutMs;
49
49
  options.signal?.throwIfAborted();
50
- let settle;
51
- const settled = new Promise((resolve) => (settle = () => resolve(SETTLED)));
52
- const wait = { closed: this.closed, settle };
50
+ const wait = { closed: this.closed };
53
51
  const nudge = () => wait.wake?.();
54
52
  const close = () => {
55
53
  wait.closed = true;
56
- settle();
54
+ wait.settle?.();
57
55
  nudge();
58
56
  };
59
57
  this.add(id, wait);
@@ -61,7 +59,7 @@ export class ResultWaiter {
61
59
  options.signal?.addEventListener('abort', nudge);
62
60
  try {
63
61
  for (let delay = 25;; delay = Math.min(delay * 2, 1_000)) {
64
- const read = wait.outcome || wait.closed ? SETTLED : await Promise.race([this.options.read(id), settled]);
62
+ const read = wait.outcome || wait.closed ? SETTLED : await this.read(id, wait);
65
63
  if (wait.outcome) {
66
64
  return unwrap(wait.outcome);
67
65
  }
@@ -104,7 +102,7 @@ export class ResultWaiter {
104
102
  const settled = typeof outcome === 'function' ? outcome() : outcome;
105
103
  for (const wait of waits) {
106
104
  wait.outcome ??= settled;
107
- wait.settle();
105
+ wait.settle?.();
108
106
  wait.wake?.();
109
107
  }
110
108
  }
@@ -117,6 +115,19 @@ export class ResultWaiter {
117
115
  this.closing.abort();
118
116
  await nextMacrotask();
119
117
  }
118
+ /**
119
+ * Reads `id`, unless the wait settles first. It races a promise of its own, not one the whole wait shares: each
120
+ * race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
121
+ */
122
+ async read(id, wait) {
123
+ const settled = new Promise((resolve) => (wait.settle = () => resolve(SETTLED)));
124
+ try {
125
+ return await Promise.race([this.options.read(id), settled]);
126
+ }
127
+ finally {
128
+ wait.settle = undefined;
129
+ }
130
+ }
120
131
  add(id, wait) {
121
132
  let waits = this.waits.get(id);
122
133
  if (!waits) {
@@ -49,8 +49,8 @@ export declare class LeasedWorker<T> {
49
49
  /** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
50
50
  private readonly claiming;
51
51
  private stopped;
52
- private stop;
53
- private readonly stoppedPromise;
52
+ /** Ends the `drain()` rounds waiting now, at shutdown. */
53
+ private readonly stopping;
54
54
  private wake?;
55
55
  private kicked;
56
56
  private loop?;
@@ -76,6 +76,11 @@ export declare class LeasedWorker<T> {
76
76
  * `shutdownTimeout` spent waiting for them before the rest are detached. Call it from `onModuleDestroy`.
77
77
  */
78
78
  shutdown(): Promise<void>;
79
+ /**
80
+ * Waits for `work`, or until shutdown. It races a promise of its own, not one the worker keeps: each race leaves a
81
+ * reaction on its inputs until they settle, so a long-lived one would keep one per round.
82
+ */
83
+ private untilStopped;
79
84
  private poll;
80
85
  private idle;
81
86
  /** Claims due items and starts executing them (before any caller sees them). */
@@ -43,7 +43,8 @@ export class LeasedWorker {
43
43
  /** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
44
44
  this.claiming = new Set();
45
45
  this.stopped = false;
46
- this.stoppedPromise = new Promise((resolve) => (this.stop = resolve));
46
+ /** Ends the `drain()` rounds waiting now, at shutdown. */
47
+ this.stopping = new Set();
47
48
  this.kicked = false;
48
49
  /** When the loop next calls `produce()` (`performance.now()`). */
49
50
  this.produceAt = 0;
@@ -96,7 +97,7 @@ export class LeasedWorker {
96
97
  break;
97
98
  }
98
99
  total += executions.length;
99
- await Promise.race([Promise.all(executions), this.stoppedPromise]);
100
+ await this.untilStopped(Promise.all(executions));
100
101
  }
101
102
  return total;
102
103
  }
@@ -109,7 +110,9 @@ export class LeasedWorker {
109
110
  return;
110
111
  }
111
112
  this.stopped = true;
112
- this.stop();
113
+ for (const stop of this.stopping) {
114
+ stop();
115
+ }
113
116
  this.wake?.();
114
117
  // What a claim in flight claims starts now, told to stop at once, so it is handed back before the store closes.
115
118
  await Promise.allSettled(this.claiming);
@@ -127,6 +130,24 @@ export class LeasedWorker {
127
130
  }
128
131
  await this.loop;
129
132
  }
133
+ /**
134
+ * Waits for `work`, or until shutdown. It races a promise of its own, not one the worker keeps: each race leaves a
135
+ * reaction on its inputs until they settle, so a long-lived one would keep one per round.
136
+ */
137
+ async untilStopped(work) {
138
+ let stop;
139
+ const stopped = new Promise((resolve) => (stop = resolve));
140
+ if (this.stopped) {
141
+ stop();
142
+ }
143
+ this.stopping.add(stop);
144
+ try {
145
+ await Promise.race([work, stopped]);
146
+ }
147
+ finally {
148
+ this.stopping.delete(stop);
149
+ }
150
+ }
130
151
  async poll() {
131
152
  while (!this.stopped) {
132
153
  if (this.options.produce && performance.now() >= this.produceAt) {
@@ -151,8 +151,6 @@ export declare class WorkflowExecution {
151
151
  private readonly childCounters;
152
152
  /** The first interrupt that stops this execution (not a suspension). */
153
153
  private stoppedBy;
154
- /** The step whose function is running, in that function's async context. */
155
- private readonly insideStep;
156
154
  /** Each journal entry's size in bytes, by name, and their sum. */
157
155
  private readonly entrySizes;
158
156
  private journalBytes;
@@ -132,8 +132,6 @@ export class WorkflowExecution {
132
132
  this.childCounters = new Map();
133
133
  /** The first interrupt that stops this execution (not a suspension). */
134
134
  this.stoppedBy = null;
135
- /** The step whose function is running, in that function's async context. */
136
- this.insideStep = new AsyncLocalStorage();
137
135
  /** Each journal entry's size in bytes, by name, and their sum. */
138
136
  this.entrySizes = new Map();
139
137
  this.journalBytes = 0;
@@ -776,7 +774,7 @@ export class WorkflowExecution {
776
774
  await this.write([current]);
777
775
  },
778
776
  };
779
- const run = Promise.resolve().then(() => runInStepScope(ctx.idempotencyKey, () => this.insideStep.run(name, () => fn(ctx))));
777
+ const run = Promise.resolve().then(() => runInStepScope(ctx.idempotencyKey, () => insideStep.run({ execution: this, name, outer: insideStep.getStore() }, () => fn(ctx))));
780
778
  return Promise.race([run, watchdog]).finally(() => {
781
779
  settled = true;
782
780
  clearTimeout(overall);
@@ -805,10 +803,14 @@ export class WorkflowExecution {
805
803
  * replay, so it is a definition error, reported where it happens.
806
804
  */
807
805
  assertNotInStep(call, advice = 'Call ctx methods from run() and pass the values the step needs into it.') {
808
- const step = this.insideStep.getStore();
809
- if (step === undefined) {
806
+ let frame = insideStep.getStore();
807
+ while (frame && frame.execution !== this) {
808
+ frame = frame.outer;
809
+ }
810
+ if (!frame) {
810
811
  return;
811
812
  }
813
+ const step = frame.name;
812
814
  const where = step.startsWith(COMPENSATE) ? `the compensation of step "${step.slice(COMPENSATE.length)}"` : `step "${step}"`;
813
815
  throw this.setFatal(new WorkflowDefinitionError(`${this.describe()}: ${where} called ctx.${call}. A step's function runs once and replays return its ` +
814
816
  `result, so it can't use ctx. ${advice}`));
@@ -1069,6 +1071,11 @@ function conditionsOf(name, conditions) {
1069
1071
  return branches;
1070
1072
  }
1071
1073
  const COMPENSATE = '$compensate:';
1074
+ /**
1075
+ * The steps whose functions are running, in their async context. One store for every execution, not one each: on
1076
+ * Node 20 and 22, a store that ran stays in a process-wide list every async resource walks, until `disable()`.
1077
+ */
1078
+ const insideStep = new AsyncLocalStorage();
1072
1079
  const PARENT_CLOSE = ['cancel', 'terminate', 'abandon'];
1073
1080
  /** The most a custom status (`ctx.setStatus()`) may take as JSON. */
1074
1081
  const MAX_CUSTOM_STATUS_BYTES = 16_384;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/workflows",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@workflows)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -69,37 +69,37 @@
69
69
  "@commitlint/cli": "21.2.3",
70
70
  "@commitlint/config-angular": "21.2.3",
71
71
  "@electric-sql/pglite": "0.5.8",
72
- "@nestjs/common": "12.1.0",
73
- "@nestjs/core": "12.1.0",
72
+ "@nestjs/common": "12.1.2",
73
+ "@nestjs/core": "12.1.2",
74
74
  "@nestjs/cqrs": "12.1.0",
75
75
  "@nestjs/drizzle": "0.0.1",
76
76
  "@nestjs/locks": "0.0.1",
77
77
  "@nestjs/mail": "0.0.1",
78
- "@nestjs/outbox": "0.0.1",
79
- "@nestjs/platform-express": "12.1.0",
80
- "@nestjs/platform-fastify": "12.1.0",
81
- "@nestjs/testing": "12.1.0",
78
+ "@nestjs/outbox": "0.1.0",
79
+ "@nestjs/platform-express": "12.1.2",
80
+ "@nestjs/platform-fastify": "12.1.2",
81
+ "@nestjs/testing": "12.1.2",
82
82
  "@prisma/adapter-mariadb": "7.10.0",
83
83
  "@prisma/adapter-pg": "7.10.0",
84
84
  "@prisma/client": "7.10.0",
85
- "@types/node": "24.13.6",
85
+ "@types/node": "24.19.1",
86
86
  "@types/pg": "8.23.1",
87
87
  "drizzle-orm": "0.45.3",
88
88
  "husky": "9.1.7",
89
89
  "kysely": "0.29.6",
90
- "lint-staged": "17.5.1",
90
+ "lint-staged": "17.6.0",
91
91
  "mysql2": "3.24.5",
92
- "oxlint": "1.85.0",
93
- "pg": "8.23.0",
94
- "prettier": "3.9.8",
92
+ "oxlint": "1.87.0",
93
+ "pg": "8.23.1",
94
+ "prettier": "3.9.9",
95
95
  "prisma": "7.10.0",
96
96
  "reflect-metadata": "0.2.2",
97
- "release-it": "21.1.0",
97
+ "release-it": "21.1.1",
98
98
  "rxjs": "7.8.2",
99
99
  "typeorm": "1.1.1",
100
100
  "typescript": "7.0.2",
101
- "vite": "8.3.0",
102
- "vitest": "5.0.1"
101
+ "vite": "8.3.3",
102
+ "vitest": "5.0.3"
103
103
  },
104
104
  "peerDependencies": {
105
105
  "@nestjs/common": "^11.0.0 || ^12.0.0",