nx 22.7.0-beta.12 → 22.7.0-beta.13

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 (66) hide show
  1. package/dist/schemas/nx-schema.json +25 -0
  2. package/dist/schemas/project-schema.json +25 -0
  3. package/dist/src/adapter/ngcli-adapter.js +11 -12
  4. package/dist/src/command-line/daemon/daemon.js +8 -4
  5. package/dist/src/command-line/graph/graph.js +8 -1
  6. package/dist/src/command-line/show/show-target/utils.js +4 -3
  7. package/dist/src/command-line/yargs-utils/shared-options.js +5 -1
  8. package/dist/src/config/nx-json.d.ts +3 -1
  9. package/dist/src/config/workspace-json-project-json.d.ts +2 -1
  10. package/dist/src/core/graph/main.js +1 -1
  11. package/dist/src/daemon/client/client.d.ts +10 -3
  12. package/dist/src/daemon/client/client.js +21 -31
  13. package/dist/src/daemon/client/daemon-environment.d.ts +4 -0
  14. package/dist/src/daemon/client/daemon-environment.js +119 -0
  15. package/dist/src/daemon/client/daemon-socket-messenger.d.ts +2 -5
  16. package/dist/src/daemon/client/daemon-socket-messenger.js +1 -1
  17. package/dist/src/daemon/message-types/daemon-message.d.ts +6 -0
  18. package/dist/src/daemon/message-types/daemon-message.js +6 -0
  19. package/dist/src/daemon/server/handle-hash-tasks.d.ts +1 -1
  20. package/dist/src/daemon/server/handle-hash-tasks.js +1 -1
  21. package/dist/src/daemon/server/handle-outputs-tracking.d.ts +4 -4
  22. package/dist/src/daemon/server/handle-outputs-tracking.js +11 -11
  23. package/dist/src/daemon/server/outputs-tracking.d.ts +18 -3
  24. package/dist/src/daemon/server/outputs-tracking.js +49 -22
  25. package/dist/src/daemon/server/project-graph-incremental-recomputation.d.ts +2 -1
  26. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +20 -4
  27. package/dist/src/daemon/server/server.js +71 -40
  28. package/dist/src/executors/run-commands/running-tasks.js +2 -3
  29. package/dist/src/executors/run-script/run-script.impl.js +16 -8
  30. package/dist/src/hasher/hash-task.d.ts +9 -1
  31. package/dist/src/hasher/hash-task.js +41 -14
  32. package/dist/src/hasher/native-task-hasher-impl.d.ts +1 -1
  33. package/dist/src/hasher/native-task-hasher-impl.js +4 -6
  34. package/dist/src/hasher/task-hasher.d.ts +20 -9
  35. package/dist/src/hasher/task-hasher.js +34 -6
  36. package/dist/src/native/index.d.ts +34 -7
  37. package/dist/src/native/native-bindings.js +1 -1
  38. package/dist/src/native/nx.wasi-browser.js +0 -1
  39. package/dist/src/native/nx.wasi.cjs +0 -1
  40. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  41. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  42. package/dist/src/plugins/js/utils/register.js +83 -4
  43. package/dist/src/project-graph/error-types.js +31 -11
  44. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
  45. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +9 -0
  46. package/dist/src/project-graph/plugins/isolation/message-types.d.ts +1 -1
  47. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +9 -0
  48. package/dist/src/project-graph/plugins/isolation/messaging.js +2 -0
  49. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +36 -68
  50. package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +6 -0
  51. package/dist/src/tasks-runner/cache.d.ts +6 -0
  52. package/dist/src/tasks-runner/cache.js +58 -0
  53. package/dist/src/tasks-runner/init-tasks-runner.js +13 -7
  54. package/dist/src/tasks-runner/run-command.js +13 -5
  55. package/dist/src/tasks-runner/task-env.js +17 -1
  56. package/dist/src/tasks-runner/task-orchestrator.d.ts +79 -7
  57. package/dist/src/tasks-runner/task-orchestrator.js +327 -120
  58. package/dist/src/tasks-runner/tasks-schedule.d.ts +5 -2
  59. package/dist/src/tasks-runner/tasks-schedule.js +31 -17
  60. package/dist/src/tasks-runner/utils.d.ts +3 -3
  61. package/dist/src/tasks-runner/utils.js +5 -6
  62. package/package.json +12 -12
  63. package/dist/src/daemon/client/exec-is-server-available.d.ts +0 -1
  64. package/dist/src/daemon/client/exec-is-server-available.js +0 -11
  65. package/dist/src/daemon/client/generate-help-output.d.ts +0 -1
  66. package/dist/src/daemon/client/generate-help-output.js +0 -26
@@ -8,6 +8,7 @@ const fs_1 = require("fs");
8
8
  const pc = tslib_1.__importStar(require("picocolors"));
9
9
  const path_1 = require("path");
10
10
  const perf_hooks_1 = require("perf_hooks");
11
+ const project_graph_1 = require("../project-graph/project-graph");
11
12
  const run_commands_impl_1 = require("../executors/run-commands/run-commands.impl");
12
13
  const hash_task_1 = require("../hasher/hash-task");
13
14
  const task_graph_utils_1 = require("./task-graph-utils");
@@ -45,18 +46,22 @@ class TaskOrchestrator {
45
46
  this.taskDetails = (0, hash_task_1.getTaskDetails)();
46
47
  this.cache = (0, cache_1.getCache)(this.options);
47
48
  this.tuiEnabled = (0, is_tui_enabled_1.isTuiEnabled)();
49
+ // Derived from projectGraph once — passed to getExecutorForTask /
50
+ // getCustomHasher so they don't have to re-walk the graph per call.
51
+ this.projects = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(this.projectGraph).projects;
48
52
  this.forkedProcessTaskRunner = new forked_process_task_runner_1.ForkedProcessTaskRunner(this.options, this.tuiEnabled);
49
53
  this.runningTasksService = !native_1.IS_WASM
50
54
  ? new native_1.RunningTasksService((0, db_connection_1.getLocalDbConnection)())
51
55
  : null;
52
- this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.taskGraph, this.options);
56
+ this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options);
53
57
  // region internal state
54
58
  this.batchEnv = (0, task_env_1.getEnvVariablesForBatchProcess)(this.options.skipNxCache, this.options.captureStderr);
55
59
  this.reverseTaskDeps = (0, utils_1.calculateReverseDeps)(this.taskGraph);
56
60
  this.initializingTaskIds = new Set(this.initiatingTasks.map((t) => t.id));
57
61
  this.processedTasks = new Map();
58
- this.completedTasks = {};
62
+ this.completedTasks = new Map();
59
63
  this.waitingForTasks = [];
64
+ this.pendingDiscreteWorkers = new Set();
60
65
  this.groups = [];
61
66
  this.continuousTasksStarted = 0;
62
67
  this.bailed = false;
@@ -89,34 +94,31 @@ class TaskOrchestrator {
89
94
  await this.init();
90
95
  perf_hooks_1.performance.mark('task-execution:start');
91
96
  const { discrete, continuous, total } = getThreadPoolSize(this.options, this.taskGraph);
92
- const threads = [];
93
97
  process.stdout.setMaxListeners(total + events_1.defaultMaxListeners);
94
98
  process.stderr.setMaxListeners(total + events_1.defaultMaxListeners);
95
99
  process.setMaxListeners(total + events_1.defaultMaxListeners);
96
- // initial seeding of the queue
97
- for (let i = 0; i < discrete; ++i) {
98
- threads.push(this.executeDiscreteTaskLoop());
99
- }
100
+ const doNotSkipCache = this.options.skipNxCache === false ||
101
+ this.options.skipNxCache === undefined;
102
+ // Start continuous task loops (these run independently)
103
+ const continuousLoops = [];
100
104
  for (let i = 0; i < continuous; ++i) {
101
- threads.push(this.executeContinuousTaskLoop(continuous));
105
+ continuousLoops.push(this.executeContinuousTaskLoop(continuous));
102
106
  }
107
+ // Set up forced shutdown handler
108
+ const shutdownPromise = this.tuiEnabled
109
+ ? new Promise((resolve) => {
110
+ this.options.lifeCycle.registerForcedShutdownCallback(() => {
111
+ this.stopRequested = true;
112
+ resolve(undefined);
113
+ });
114
+ })
115
+ : new Promise((resolve) => {
116
+ this.resolveStopPromise = resolve;
117
+ });
118
+ const coordinatorLoop = this.executeCoordinatorLoop(doNotSkipCache, discrete);
103
119
  await Promise.race([
104
- Promise.all(threads),
105
- ...(this.tuiEnabled
106
- ? [
107
- new Promise((resolve) => {
108
- this.options.lifeCycle.registerForcedShutdownCallback(() => {
109
- // The user force quit the TUI with ctrl+c, so proceed onto cleanup
110
- this.stopRequested = true;
111
- resolve(undefined);
112
- });
113
- }),
114
- ]
115
- : [
116
- new Promise((resolve) => {
117
- this.resolveStopPromise = resolve;
118
- }),
119
- ]),
120
+ Promise.all([coordinatorLoop, ...continuousLoops]),
121
+ shutdownPromise,
120
122
  ]);
121
123
  perf_hooks_1.performance.mark('task-execution:end');
122
124
  perf_hooks_1.performance.measure('task-execution', 'task-execution:start', 'task-execution:end');
@@ -124,20 +126,57 @@ class TaskOrchestrator {
124
126
  this.cache.removeOldCacheRecords();
125
127
  }
126
128
  await this.cleanup();
127
- return this.completedTasks;
129
+ // Public API (defaultTasksRunner) returns a plain object keyed by
130
+ // task id. Internal state is a Map for faster lookup.
131
+ return Object.fromEntries(this.completedTasks);
128
132
  }
129
133
  nextBatch() {
130
134
  return this.tasksSchedule.nextBatch();
131
135
  }
132
- async executeDiscreteTaskLoop() {
133
- const doNotSkipCache = this.options.skipNxCache === false ||
134
- this.options.skipNxCache === undefined;
136
+ /**
137
+ * Coordinator loop. All batch operations (hashing, cache resolution)
138
+ * happen on this single thread — no races. Cache misses are dispatched
139
+ * as fire-and-forget workers. Workers signal completion via
140
+ * scheduleNextTasksAndReleaseThreads which wakes all waiting loops.
141
+ *
142
+ * Safety: the dispatch phase (step 5) is fully synchronous — no
143
+ * worker can run during it. So all tasks picked up by nextTask()
144
+ * are guaranteed to be in processedTasks from step 1.
145
+ */
146
+ async executeCoordinatorLoop(doNotSkipCache, parallelism) {
135
147
  while (true) {
136
- // completed all the tasks
137
- if (!this.tasksSchedule.hasTasks() || this.bailed || this.stopRequested) {
138
- return null;
148
+ if (this.bailed || this.stopRequested)
149
+ break;
150
+ // 1. Hash BEFORE processAll so processTask sees hashes set, and so
151
+ // resolveCachedTasksBulk can look them up in the cache. Each task
152
+ // is hashed with its own task-specific env (project/target .env
153
+ // files, custom hasher env reads) — the shared batchEnv would
154
+ // compute a different cache key than the single-task path and
155
+ // risk stale cache reuse after env changes.
156
+ {
157
+ const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
158
+ const unhashed = scheduledTasks
159
+ .map((id) => this.taskGraph.tasks[id])
160
+ .filter((t) => !t.hash &&
161
+ this.taskGraph.dependencies[t.id].every((depId) => this.completedTasks.has(depId)));
162
+ if (unhashed.length > 1) {
163
+ const perTaskEnvs = {};
164
+ for (const task of unhashed) {
165
+ perTaskEnvs[task.id] = (0, task_env_1.getTaskSpecificEnv)(task, this.projectGraph);
166
+ }
167
+ await (0, hash_task_1.hashTasks)(this.hasher, this.projectGraph, this.taskGraphForHashing, perTaskEnvs, this.taskDetails, unhashed);
168
+ }
169
+ }
170
+ // 2. Bulk-resolve cache hits before processTask — avoids N
171
+ // lifecycle calls for tasks that will be resolved from cache.
172
+ if (doNotSkipCache) {
173
+ const resolved = await this.resolveCachedTasksBulk();
174
+ if (resolved)
175
+ continue;
139
176
  }
177
+ // 3. Process remaining scheduled tasks (cache misses + non-cacheable).
140
178
  this.processAllScheduledTasks();
179
+ // 4. Handle batch executors
141
180
  const batch = this.nextBatch();
142
181
  if (batch) {
143
182
  const groupId = this.closeGroup();
@@ -145,14 +184,20 @@ class TaskOrchestrator {
145
184
  this.openGroup(groupId);
146
185
  continue;
147
186
  }
148
- const task = this.tasksSchedule.nextTask((t) => !t.continuous);
149
- if (task) {
187
+ // 5. Dispatch cache misses as individual workers
188
+ while (this.pendingDiscreteWorkers.size < parallelism) {
189
+ const task = this.tasksSchedule.nextTask((t) => !t.continuous);
190
+ if (!task)
191
+ break;
150
192
  const groupId = this.closeGroup();
151
- await this.applyFromCacheOrRunTask(doNotSkipCache, task, groupId);
152
- this.openGroup(groupId);
153
- continue;
193
+ this.dispatchDiscreteWorker(doNotSkipCache, task, groupId);
154
194
  }
155
- // block until some other task completes, then try again
195
+ // 6. Nothing left to dispatch and nothing in flight — done.
196
+ if (!this.tasksSchedule.hasTasks() &&
197
+ this.pendingDiscreteWorkers.size === 0) {
198
+ break;
199
+ }
200
+ // 7. Wait for a worker to finish (woken by scheduleNextTasksAndReleaseThreads)
156
201
  await new Promise((res) => this.waitingForTasks.push(res));
157
202
  }
158
203
  }
@@ -186,14 +231,6 @@ class TaskOrchestrator {
186
231
  await new Promise((res) => this.waitingForTasks.push(res));
187
232
  }
188
233
  }
189
- processTasks(taskIds) {
190
- for (const taskId of taskIds) {
191
- // Task is already handled or being handled
192
- if (!this.processedTasks.has(taskId)) {
193
- this.processedTasks.set(taskId, this.processTask(taskId));
194
- }
195
- }
196
- }
197
234
  // region Processing Scheduled Tasks
198
235
  async processTask(taskId) {
199
236
  const task = this.taskGraph.tasks[taskId];
@@ -206,41 +243,117 @@ class TaskOrchestrator {
206
243
  }
207
244
  processAllScheduledTasks() {
208
245
  const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
209
- this.processTasks(scheduledTasks);
246
+ for (const taskId of scheduledTasks) {
247
+ // Task is already handled or being handled
248
+ if (!this.processedTasks.has(taskId)) {
249
+ this.processedTasks.set(taskId, this.processTask(taskId));
250
+ }
251
+ }
210
252
  }
211
253
  // endregion Processing Scheduled Tasks
212
254
  // region Applying Cache
213
255
  async applyCachedResults(tasks) {
214
256
  const cacheableTasks = tasks.filter((t) => (0, utils_1.isCacheableTask)(t, this.options));
215
- const res = await Promise.all(cacheableTasks.map((t) => this.applyCachedResult(t)));
216
- return res.filter((r) => r !== null);
257
+ if (cacheableTasks.length === 0)
258
+ return [];
259
+ const cacheHits = await this.fetchCacheHits(cacheableTasks);
260
+ if (cacheHits.length === 0)
261
+ return [];
262
+ return this.finalizeCacheHits(cacheHits);
263
+ }
264
+ /**
265
+ * Batch cache lookup + filter to successful entries. Handles both
266
+ * local (one rarray SQL call) and remote (parallel HTTP retrievals)
267
+ * inside DbCache.getBatch.
268
+ */
269
+ async fetchCacheHits(tasks) {
270
+ const batchResults = await this.cache.getBatch(tasks);
271
+ const cacheHits = [];
272
+ for (const task of tasks) {
273
+ const cachedResult = batchResults.get(task.hash);
274
+ if (cachedResult && cachedResult.code === 0) {
275
+ cacheHits.push({ task, cachedResult });
276
+ }
277
+ }
278
+ return cacheHits;
217
279
  }
218
- async applyCachedResult(task) {
219
- const cachedResult = await this.cache.get(task);
220
- if (!cachedResult || cachedResult.code !== 0)
221
- return null;
222
- const outputs = task.outputs;
223
- const shouldCopyOutputsFromCache =
224
- // No output files to restore
225
- !!outputs.length &&
226
- // Remote caches are restored to output dirs when applied and using db cache
227
- (!cachedResult.remote || !(0, cache_1.dbCacheEnabled)()) &&
228
- // Output files have not been touched since last run
229
- (await this.shouldCopyOutputsFromCache(outputs, task.hash));
230
- if (shouldCopyOutputsFromCache) {
231
- await this.cache.copyFilesFromCache(task.hash, cachedResult, outputs);
280
+ /**
281
+ * For each confirmed cache hit: decide whether to copy outputs from
282
+ * the cache (skipping if the on-disk outputs already match the
283
+ * recorded hash), copy in parallel, derive the task status, print
284
+ * terminal output, and return the assembled results.
285
+ */
286
+ async finalizeCacheHits(cacheHits) {
287
+ // Batch-check which tasks need outputs copied from cache. Remote
288
+ // cache entries come pre-restored to their output dirs when the
289
+ // db cache is on, so we only check ones that aren't.
290
+ const usingDbCache = (0, cache_1.dbCacheEnabled)();
291
+ const tasksNeedingOutputCheck = cacheHits.filter(({ task, cachedResult }) => task.outputs.length > 0 && (!cachedResult.remote || !usingDbCache));
292
+ const shouldCopyMap = await this.shouldCopyOutputsFromCacheBatch(tasksNeedingOutputCheck.map(({ task }) => ({
293
+ outputs: task.outputs,
294
+ hash: task.hash,
295
+ })));
296
+ // Copy outputs in parallel for tasks that need it.
297
+ await Promise.all(cacheHits.map(async ({ task, cachedResult }) => {
298
+ if (shouldCopyMap.get(task.hash)) {
299
+ await this.cache.copyFilesFromCache(task.hash, cachedResult, task.outputs);
300
+ }
301
+ }));
302
+ // Derive status, print terminal output, build results.
303
+ const results = [];
304
+ for (const { task, cachedResult } of cacheHits) {
305
+ const shouldCopy = shouldCopyMap.get(task.hash) ?? false;
306
+ const status = cachedResult.remote
307
+ ? 'remote-cache'
308
+ : shouldCopy
309
+ ? 'local-cache'
310
+ : 'local-cache-kept-existing';
311
+ this.options.lifeCycle.printTaskTerminalOutput(task, status, cachedResult.terminalOutput);
312
+ results.push({
313
+ task,
314
+ code: cachedResult.code,
315
+ status,
316
+ terminalOutput: cachedResult.terminalOutput,
317
+ });
318
+ }
319
+ return results;
320
+ }
321
+ /**
322
+ * Coordinator wrapper around {@link resolveCachedTasks}: peeks at
323
+ * scheduledTasks (without removing anything from the schedule),
324
+ * filters to cacheable hashed discrete candidates, and delegates the
325
+ * cache fetch + lifecycle to the public method. Returns true if any
326
+ * tasks were resolved.
327
+ *
328
+ * The coordinator relies on this running unconditionally (when cache
329
+ * is enabled): tasks dispatched in step 5 via runTaskDirectly skip
330
+ * their own cache lookup on the assumption that this has already
331
+ * confirmed them as misses. Don't add length-based bails.
332
+ */
333
+ async resolveCachedTasksBulk() {
334
+ const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
335
+ const candidates = [];
336
+ for (const id of scheduledTasks) {
337
+ const task = this.taskGraph.tasks[id];
338
+ if (task.hash &&
339
+ !task.continuous &&
340
+ (0, utils_1.isCacheableTask)(task, this.options)) {
341
+ candidates.push(task);
342
+ }
343
+ }
344
+ if (candidates.length === 0)
345
+ return false;
346
+ // postRunSteps → complete() → tasksSchedule.complete() will filter
347
+ // resolved hits out of scheduledTasks before we return, so there's
348
+ // no need to mutate the schedule here.
349
+ const groupId = this.closeGroup();
350
+ try {
351
+ const results = await this.resolveCachedTasks(true, candidates, groupId);
352
+ return results.length > 0;
353
+ }
354
+ finally {
355
+ this.openGroup(groupId);
232
356
  }
233
- const status = cachedResult.remote
234
- ? 'remote-cache'
235
- : shouldCopyOutputsFromCache
236
- ? 'local-cache'
237
- : 'local-cache-kept-existing';
238
- this.options.lifeCycle.printTaskTerminalOutput(task, status, cachedResult.terminalOutput);
239
- return {
240
- code: cachedResult.code,
241
- task,
242
- status,
243
- };
244
357
  }
245
358
  // endregion Applying Cache
246
359
  // region Batch
@@ -298,7 +411,15 @@ class TaskOrchestrator {
298
411
  return { cachedResults, needsRehashAfterExecution };
299
412
  }
300
413
  async hashBatchTasks(tasks) {
301
- await (0, hash_task_1.hashTasks)(this.hasher, this.projectGraph, this.taskGraphForHashing, this.batchEnv, this.taskDetails, tasks);
414
+ // Batch executors run every task in the same forked process, but
415
+ // each task still has its own .env files / custom-hasher env — use
416
+ // task-specific env for hashing so the cache key matches the
417
+ // single-task path.
418
+ const perTaskEnvs = {};
419
+ for (const task of tasks) {
420
+ perTaskEnvs[task.id] = (0, task_env_1.getTaskSpecificEnv)(task, this.projectGraph);
421
+ }
422
+ await (0, hash_task_1.hashTasks)(this.hasher, this.projectGraph, this.taskGraphForHashing, perTaskEnvs, this.taskDetails, tasks);
302
423
  }
303
424
  async applyFromCacheOrRunBatch(doNotSkipCache, batch, groupId) {
304
425
  const applyFromCacheOrRunBatchStart = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:start');
@@ -341,12 +462,12 @@ class TaskOrchestrator {
341
462
  }
342
463
  // Update batch status based on all task results
343
464
  const hasFailures = taskEntries.some(([taskId]) => {
344
- const status = this.completedTasks[taskId];
465
+ const status = this.completedTasks.get(taskId);
345
466
  return status === 'failure' || status === 'skipped';
346
467
  });
347
468
  this.options.lifeCycle.setBatchStatus?.(batch.id, hasFailures ? "Failure" /* BatchStatus.Failure */ : "Success" /* BatchStatus.Success */);
348
469
  this.forkedProcessTaskRunner.cleanUpBatchProcesses();
349
- const tasksCompleted = taskEntries.filter(([taskId]) => this.completedTasks[taskId]);
470
+ const tasksCompleted = taskEntries.filter(([taskId]) => this.completedTasks.has(taskId));
350
471
  // Batch is still not done, run it again
351
472
  if (tasksCompleted.length !== taskEntries.length) {
352
473
  await this.applyFromCacheOrRunBatch(doNotSkipCache, {
@@ -420,52 +541,120 @@ class TaskOrchestrator {
420
541
  }
421
542
  // endregion Batch
422
543
  // region Single Task
423
- async applyFromCacheOrRunTask(doNotSkipCache, task, groupId) {
544
+ /**
545
+ * Bulk-resolve cache hits for a set of tasks: fetch cached entries,
546
+ * copy outputs as needed, fire lifecycle, and return the TaskResults
547
+ * for the hits. Tasks that aren't in the cache (or aren't cacheable)
548
+ * are silently omitted from the return value — callers are responsible
549
+ * for running those via {@link runTaskDirectly}.
550
+ *
551
+ * Fires scheduleTask lifecycle for hits that haven't been through
552
+ * processAllScheduledTasks yet. That's a coordinator gap-filler and
553
+ * a no-op for callers that pre-process the schedule.
554
+ *
555
+ * The caller provides `groupId` — cache hits share one slot since they
556
+ * don't actually compete for parallelism.
557
+ */
558
+ async resolveCachedTasks(doNotSkipCache, tasks, groupId) {
559
+ if (!doNotSkipCache || tasks.length === 0)
560
+ return [];
561
+ const cacheableTasks = tasks.filter((t) => (0, utils_1.isCacheableTask)(t, this.options));
562
+ if (cacheableTasks.length === 0)
563
+ return [];
564
+ const cacheHits = await this.fetchCacheHits(cacheableTasks);
565
+ if (cacheHits.length === 0)
566
+ return [];
567
+ // scheduleTask lifecycle for hits the coordinator resolved before
568
+ // processAllScheduledTasks could fire it. No-op for callers that
569
+ // already ran processAllScheduledTasks (every hit is in processedTasks).
570
+ await Promise.all(cacheHits
571
+ .filter(({ task }) => !this.processedTasks.has(task.id))
572
+ .map(({ task }) => this.options.lifeCycle.scheduleTask(task)));
573
+ const hitTasks = cacheHits.map((h) => h.task);
574
+ await this.preRunSteps(hitTasks, { groupId });
575
+ const results = await this.finalizeCacheHits(cacheHits);
576
+ await this.postRunSteps(results, doNotSkipCache, { groupId });
577
+ return results;
578
+ }
579
+ /**
580
+ * Fire a discrete-task worker and track it in pendingDiscreteWorkers until
581
+ * it settles. Uses runTaskDirectly (not applyFromCacheOrRun*) because
582
+ * resolveCachedTasksBulk already confirmed this task is a cache miss —
583
+ * another lookup would re-query the DB and (for Nx Cloud users) repeat
584
+ * the remote HTTP retrieval.
585
+ */
586
+ dispatchDiscreteWorker(doNotSkipCache, task, groupId) {
587
+ const worker = this.runTaskDirectly(doNotSkipCache, task, groupId)
588
+ .catch((e) => this.handleDiscreteWorkerFailure(doNotSkipCache, task, groupId, e))
589
+ .finally(() => {
590
+ this.openGroup(groupId);
591
+ this.pendingDiscreteWorkers.delete(worker);
592
+ // Wake coordinator — the delete above may satisfy the exit condition
593
+ // (pendingDiscreteWorkers.size === 0) that was missed when
594
+ // scheduleNextTasksAndReleaseThreads fired earlier.
595
+ this.waitingForTasks.forEach((f) => f(null));
596
+ this.waitingForTasks.length = 0;
597
+ });
598
+ this.pendingDiscreteWorkers.add(worker);
599
+ }
600
+ /**
601
+ * Route a worker rejection (e.g. remote cache errors) through the normal
602
+ * failure path instead of letting it become an unhandled promise. Guard
603
+ * against double-finalize: completeTasks() populates `completedTasks`,
604
+ * so a rejection arriving after postRunSteps has already finalized the
605
+ * task must not run postRunSteps again.
606
+ */
607
+ async handleDiscreteWorkerFailure(doNotSkipCache, task, groupId, e) {
608
+ if (this.completedTasks.has(task.id))
609
+ return;
610
+ const terminalOutput = e?.message ?? '';
611
+ this.options.lifeCycle.printTaskTerminalOutput(task, 'failure', terminalOutput);
612
+ await this.postRunSteps([{ task, status: 'failure', terminalOutput }], doNotSkipCache, { groupId });
613
+ }
614
+ /**
615
+ * Spawn and wait on a task's child process, unconditionally — no cache
616
+ * lookup. Callers must have already confirmed the task is a cache miss
617
+ * (or disabled caching entirely).
618
+ */
619
+ async runTaskDirectly(doNotSkipCache, task, groupId) {
424
620
  // Wait for task to be processed
425
621
  const taskSpecificEnv = await this.processedTasks.get(task.id);
426
622
  await this.preRunSteps([task], { groupId });
427
623
  const pipeOutput = await this.pipeOutputCapture(task);
428
- // obtain metadata
429
624
  const temporaryOutputPath = this.cache.temporaryOutputPath(task);
430
625
  const streamOutput = this.outputStyle === 'static'
431
626
  ? false
432
627
  : (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
433
- let env = pipeOutput
628
+ const env = pipeOutput
434
629
  ? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, process.env.FORCE_COLOR === undefined
435
630
  ? 'true'
436
631
  : process.env.FORCE_COLOR, this.options.skipNxCache, this.options.captureStderr, null, null)
437
632
  : (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
438
- let results = doNotSkipCache ? await this.applyCachedResults([task]) : [];
439
- // the task wasn't cached
440
633
  let resolveDiscreteExit;
441
- if (results.length === 0) {
442
- const discreteExitHandled = new Promise((r) => (resolveDiscreteExit = r));
443
- this.discreteTaskExitHandled.set(task.id, discreteExitHandled);
444
- const childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
445
- this.runningDiscreteTasks.set(task.id, {
446
- runningTask: childProcess,
447
- stopping: false,
448
- });
449
- const { code, terminalOutput } = await childProcess.getResults();
450
- const isStopping = this.runningDiscreteTasks.get(task.id)?.stopping ?? false;
451
- this.runningDiscreteTasks.delete(task.id);
452
- results.push({
453
- task,
454
- code,
455
- status: isStopping ? 'stopped' : code === 0 ? 'success' : 'failure',
456
- terminalOutput,
457
- });
458
- }
634
+ const discreteExitHandled = new Promise((r) => (resolveDiscreteExit = r));
635
+ this.discreteTaskExitHandled.set(task.id, discreteExitHandled);
636
+ const childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
637
+ this.runningDiscreteTasks.set(task.id, {
638
+ runningTask: childProcess,
639
+ stopping: false,
640
+ });
641
+ const { code, terminalOutput } = await childProcess.getResults();
642
+ const isStopping = this.runningDiscreteTasks.get(task.id)?.stopping ?? false;
643
+ this.runningDiscreteTasks.delete(task.id);
644
+ const result = {
645
+ task,
646
+ code,
647
+ status: isStopping ? 'stopped' : code === 0 ? 'success' : 'failure',
648
+ terminalOutput,
649
+ };
459
650
  try {
460
- await this.postRunSteps(results, doNotSkipCache, { groupId });
651
+ await this.postRunSteps([result], doNotSkipCache, { groupId });
461
652
  }
462
653
  finally {
463
- if (resolveDiscreteExit) {
464
- this.discreteTaskExitHandled.delete(task.id);
465
- resolveDiscreteExit();
466
- }
654
+ this.discreteTaskExitHandled.delete(task.id);
655
+ resolveDiscreteExit();
467
656
  }
468
- return results[0];
657
+ return result;
469
658
  }
470
659
  async runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput) {
471
660
  const shouldPrefix = streamOutput &&
@@ -475,7 +664,7 @@ class TaskOrchestrator {
475
664
  if (process.env.NX_RUN_COMMANDS_DIRECTLY !== 'false' &&
476
665
  targetConfiguration.executor === 'nx:run-commands') {
477
666
  try {
478
- const { schema } = (0, utils_1.getExecutorForTask)(task, this.projectGraph);
667
+ const { schema } = (0, utils_1.getExecutorForTask)(task, this.projects);
479
668
  const combinedOptions = (0, params_1.combineOptionsForExecutor)(task.overrides, task.target.configuration ?? targetConfiguration.defaultConfiguration, targetConfiguration, schema, task.target.project, (0, path_1.relative)(task.projectRoot ?? workspace_root_1.workspaceRoot, process.cwd()), process.env.NX_VERBOSE_LOGGING === 'true');
480
669
  if (combinedOptions.env) {
481
670
  env = {
@@ -680,13 +869,21 @@ class TaskOrchestrator {
680
869
  }
681
870
  async postRunSteps(results, doNotSkipCache, { groupId }) {
682
871
  const now = Date.now();
683
- for (const { task } of results) {
872
+ const tasksToRecord = [];
873
+ for (const { task, status } of results) {
684
874
  // Only set endTime as fallback (batch provides timing via result.task)
685
875
  task.endTime ??= now;
686
- if (!this.stopRequested) {
687
- await this.recordOutputsHash(task);
876
+ // Skip recording for tasks whose outputs already match the cache —
877
+ // the daemon already has the correct hash recorded.
878
+ if (!this.stopRequested &&
879
+ task.outputs.length > 0 &&
880
+ status !== 'local-cache-kept-existing') {
881
+ tasksToRecord.push({ outputs: task.outputs, hash: task.hash });
688
882
  }
689
883
  }
884
+ if (tasksToRecord.length > 0) {
885
+ await this.recordOutputsHashBatch(tasksToRecord);
886
+ }
690
887
  if (doNotSkipCache && !this.stopRequested) {
691
888
  // cache the results
692
889
  perf_hooks_1.performance.mark('cache-results-start');
@@ -744,7 +941,7 @@ class TaskOrchestrator {
744
941
  const taskIds = [];
745
942
  for (const { task, status, terminalOutput } of results) {
746
943
  taskIds.push(task.id);
747
- if (this.completedTasks[task.id] === undefined && status !== 'skipped') {
944
+ if (!this.completedTasks.has(task.id) && status !== 'skipped') {
748
945
  tasksToReport.push({
749
946
  task,
750
947
  status,
@@ -766,9 +963,9 @@ class TaskOrchestrator {
766
963
  // 3. Set completedTasks + update TUI + collect dependent tasks to skip
767
964
  const dependentTasksToSkip = [];
768
965
  for (const { task, status, displayStatus } of results) {
769
- if (this.completedTasks[task.id] !== undefined)
966
+ if (this.completedTasks.has(task.id))
770
967
  continue;
771
- this.completedTasks[task.id] = status;
968
+ this.completedTasks.set(task.id, status);
772
969
  if (this.tuiEnabled) {
773
970
  this.options.lifeCycle.setTaskStatus(task.id, displayStatus ?? (0, native_1.parseTaskStatus)(status));
774
971
  }
@@ -814,7 +1011,7 @@ class TaskOrchestrator {
814
1011
  if (this.tuiEnabled) {
815
1012
  return true;
816
1013
  }
817
- const { schema } = (0, utils_1.getExecutorForTask)(task, this.projectGraph);
1014
+ const { schema } = (0, utils_1.getExecutorForTask)(task, this.projects);
818
1015
  return (schema.outputCapture === 'pipe' ||
819
1016
  process.env.NX_STREAM_OUTPUT === 'true');
820
1017
  }
@@ -837,23 +1034,33 @@ class TaskOrchestrator {
837
1034
  openGroup(id) {
838
1035
  this.groups[id] = false;
839
1036
  }
840
- async shouldCopyOutputsFromCache(outputs, hash) {
1037
+ async shouldCopyOutputsFromCacheBatch(tasks) {
1038
+ const resultMap = new Map();
1039
+ if (tasks.length === 0)
1040
+ return resultMap;
841
1041
  if (this.daemon?.enabled()) {
842
- return !(await this.daemon.outputsHashesMatch(outputs, hash));
1042
+ const matches = await this.daemon.outputsHashesMatchBatch(tasks);
1043
+ for (let i = 0; i < tasks.length; i++) {
1044
+ resultMap.set(tasks[i].hash, !matches[i]);
1045
+ }
843
1046
  }
844
1047
  else {
845
- return true;
1048
+ // No daemon → can't verify on-disk outputs, always copy.
1049
+ for (const task of tasks) {
1050
+ resultMap.set(task.hash, true);
1051
+ }
846
1052
  }
1053
+ return resultMap;
847
1054
  }
848
- async recordOutputsHash(task) {
1055
+ async recordOutputsHashBatch(entries) {
849
1056
  if (this.daemon?.enabled()) {
850
- return this.daemon.recordOutputsHash(task.outputs, task.hash);
1057
+ return this.daemon.recordOutputsHashBatch(entries);
851
1058
  }
852
1059
  }
853
1060
  // endregion utils
854
1061
  async handleContinuousTaskExit(code, task, groupId, ownsRunningTasksService) {
855
1062
  // If cleanup already completed this task, nothing left to do
856
- if (this.completedTasks[task.id] !== undefined) {
1063
+ if (this.completedTasks.has(task.id)) {
857
1064
  return;
858
1065
  }
859
1066
  const stoppingReason = this.runningContinuousTasks.get(task.id)?.stoppingReason;
@@ -867,7 +1074,7 @@ class TaskOrchestrator {
867
1074
  }
868
1075
  }
869
1076
  async completeContinuousTask(task, groupId, ownsRunningTasksService, reason) {
870
- if (this.completedTasks[task.id] !== undefined)
1077
+ if (this.completedTasks.has(task.id))
871
1078
  return;
872
1079
  this.runningContinuousTasks.delete(task.id);
873
1080
  if (ownsRunningTasksService) {
@@ -1,6 +1,7 @@
1
1
  import { DefaultTasksRunnerOptions } from './default-tasks-runner';
2
2
  import { Task, TaskGraph } from '../config/task-graph';
3
3
  import { ProjectGraph } from '../config/project-graph';
4
+ import { ProjectConfiguration } from '../config/workspace-json-project-json';
4
5
  export interface Batch {
5
6
  id: string;
6
7
  executorName: string;
@@ -8,6 +9,7 @@ export interface Batch {
8
9
  }
9
10
  export declare class TasksSchedule {
10
11
  private readonly projectGraph;
12
+ private readonly projects;
11
13
  private readonly taskGraph;
12
14
  private readonly options;
13
15
  private notScheduledTaskGraph;
@@ -22,7 +24,7 @@ export declare class TasksSchedule {
22
24
  private estimatedTaskTimings;
23
25
  private projectDependencies;
24
26
  private batchCounters;
25
- constructor(projectGraph: ProjectGraph, taskGraph: TaskGraph, options: DefaultTasksRunnerOptions);
27
+ constructor(projectGraph: ProjectGraph, projects: Record<string, ProjectConfiguration>, taskGraph: TaskGraph, options: DefaultTasksRunnerOptions);
26
28
  init(): Promise<void>;
27
29
  scheduleNextTasks(): Promise<void>;
28
30
  hasTasks(): boolean;
@@ -35,7 +37,8 @@ export declare class TasksSchedule {
35
37
  nextBatch(): Batch;
36
38
  getIncompleteTasks(): Task[];
37
39
  private scheduleTasks;
38
- private scheduleTask;
40
+ private scheduleTaskBatch;
41
+ private sortScheduledTasks;
39
42
  private scheduleBatches;
40
43
  private scheduleBatch;
41
44
  private processTaskForBatches;