@vzn/vx 0.0.615 → 0.0.616

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.615",
3
+ "version": "0.0.616",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.615",
25
- "@vzn/vx-linux-arm64": "0.0.615",
26
- "@vzn/vx-darwin-x64": "0.0.615",
27
- "@vzn/vx-darwin-arm64": "0.0.615"
24
+ "@vzn/vx-linux-x64": "0.0.616",
25
+ "@vzn/vx-linux-arm64": "0.0.616",
26
+ "@vzn/vx-darwin-x64": "0.0.616",
27
+ "@vzn/vx-darwin-arm64": "0.0.616"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -76,12 +76,13 @@ export { restoreHit, type RestoreHitArgs } from './hit-restore.js'
76
76
  import type { DeferredOutputs } from './deferred-outputs.js'
77
77
  import type { Logger } from './logger.js'
78
78
  import {
79
- computeGroupKey,
79
+ computeGroupHash,
80
80
  computeTaskHash,
81
81
  describeTaskInputs,
82
82
  type HashCache,
83
83
  movedInput,
84
84
  type TaskInputComponent,
85
+ type UpfrontGroupKey,
85
86
  } from './task-hash.js'
86
87
  import { getContext } from './remote-prefetch.js'
87
88
  import { expandGroupUpstream, filterUpstreamHashes } from './upstream.js'
@@ -190,6 +191,11 @@ export interface ExecuteArgs {
190
191
  * used verbatim instead of deriving the same key again.
191
192
  */
192
193
  upfrontKey?: string
194
+ /**
195
+ * An unkeyed group's up-front key (`deriveStableKeys`' `groupKeys`): used
196
+ * verbatim when the live upstream folds the same ids and keys.
197
+ */
198
+ upfrontGroupKey?: UpfrontGroupKey
193
199
  /**
194
200
  * Start the sandbox runtime, on the first task that executes inside one
195
201
  * (run.ts, `prepareSandbox`). Absent when no task in the run declares a
@@ -263,17 +269,25 @@ export async function executeTask(args: ExecuteArgs): Promise<TaskOutcome> {
263
269
  */
264
270
  async function executeGroupTask(args: ExecuteArgs): Promise<TaskOutcome> {
265
271
  const wallclockNs = process.hrtime.bigint() - args.runStartHrTimeNs
266
- const hash = await computeGroupKey({
267
- node: args.node,
268
- upstream: args.upstream,
269
- workspaceRoot: args.workspaceRoot,
270
- workspaceFingerprint: args.workspaceFingerprint,
271
- cache: args.cache,
272
- forwardArgs: args.forwardArgs,
273
- nestedProjectDirs: args.nestedProjectDirs,
274
- ...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
275
- ...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
276
- })
272
+ // `computeGroupKey`: an unkeyed group's key is its upstream's alone, so
273
+ // the up-front one stands when that upstream is unchanged.
274
+ const upfront = args.upfrontGroupKey
275
+ const hash =
276
+ args.node.config.cache === undefined
277
+ ? upfront !== undefined && sameUpstream(args.upstream, upfront.upstream)
278
+ ? upfront.key
279
+ : computeGroupHash(args.upstream)
280
+ : await computeTaskHash({
281
+ node: args.node,
282
+ upstream: args.upstream,
283
+ workspaceRoot: args.workspaceRoot,
284
+ workspaceFingerprint: args.workspaceFingerprint,
285
+ cache: args.cache,
286
+ forwardArgs: args.forwardArgs,
287
+ nestedProjectDirs: args.nestedProjectDirs,
288
+ ...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
289
+ ...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
290
+ })
277
291
  return {
278
292
  node: args.node,
279
293
  status: 'success',
@@ -288,6 +302,17 @@ async function executeGroupTask(args: ExecuteArgs): Promise<TaskOutcome> {
288
302
  }
289
303
  }
290
304
 
305
+ /** The same ids with the same keys, in order: what `computeGroupHash` reads. */
306
+ function sameUpstream(live: TaskOutcome[], upfront: TaskOutcome[]): boolean {
307
+ if (live.length !== upfront.length) return false
308
+ for (let i = 0; i < live.length; i++) {
309
+ const a = live[i]
310
+ const b = upfront[i]!
311
+ if (a === undefined || a.node.id !== b.node.id || a.hash !== b.hash) return false
312
+ }
313
+ return true
314
+ }
315
+
291
316
  /**
292
317
  * Persistent task: dev server / file watcher / daemon. Spawn, wait
293
318
  * for ready (regex match or immediate), stash the subprocess in the
@@ -45,7 +45,7 @@ import { normalizeGlob, relPosix, span, staticPrefix } from '../util/index.js'
45
45
  import type { CacheEntry, CacheLayer, GitFilesCache } from '../cache/index.js'
46
46
  import type { TaskNode } from '../graph/index.js'
47
47
  import { deriveStableKeys, workspaceInputsReach } from './stable-keys.js'
48
- import type { HashCache } from './task-hash.js'
48
+ import type { HashCache, UpfrontGroupKey } from './task-hash.js'
49
49
  import { getContext } from './remote-prefetch.js'
50
50
 
51
51
  export interface ShortCircuitArgs {
@@ -91,6 +91,8 @@ export interface ShortCircuit {
91
91
  * again (`deriveStableKeys`' `uncachedKeys`).
92
92
  */
93
93
  uncachedKeys: Map<string, string>
94
+ /** Unkeyed group id → its up-front key (`deriveStableKeys`' `groupKeys`). */
95
+ groupKeys: Map<string, UpfrontGroupKey>
94
96
  }
95
97
 
96
98
  /**
@@ -105,11 +107,17 @@ export interface ShortCircuit {
105
107
  export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<ShortCircuit> {
106
108
  let stableKeys
107
109
  const uncachedKeys = new Map<string, string>()
110
+ const groupKeys = new Map<string, UpfrontGroupKey>()
108
111
  const endKeys = span('stable keys')
109
112
  try {
110
- stableKeys = await deriveStableKeys({ ...args, uncachedKeys })
113
+ stableKeys = await deriveStableKeys({ ...args, uncachedKeys, groupKeys })
111
114
  } catch {
112
- return { preProbed: new Map(), restoreTier: new Set(), uncachedKeys: new Map() }
115
+ return {
116
+ preProbed: new Map(),
117
+ restoreTier: new Set(),
118
+ uncachedKeys: new Map(),
119
+ groupKeys: new Map(),
120
+ }
113
121
  } finally {
114
122
  endKeys()
115
123
  }
@@ -120,14 +128,14 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
120
128
  const preProbed = new Map<string, ProbedEntry>()
121
129
  const restoreTier = new Set<string>()
122
130
  if (candidates.length === 0 && uncachedKeys.size === 0) {
123
- return { preProbed, restoreTier, uncachedKeys }
131
+ return { preProbed, restoreTier, uncachedKeys, groupKeys }
124
132
  }
125
133
 
126
134
  const keptOut = restoreTierExclusions(args.nodes, args.workspaceRoot)
127
135
  // A root-anchored output may land in a project no edge leads from, so an
128
136
  // uncached key there is derived again once the writer may have run.
129
137
  for (const id of keptOut) uncachedKeys.delete(id)
130
- if (candidates.length === 0) return { preProbed, restoreTier, uncachedKeys }
138
+ if (candidates.length === 0) return { preProbed, restoreTier, uncachedKeys, groupKeys }
131
139
 
132
140
  // Bounded pool over the stable candidates: probe local ONCE each. A
133
141
  // confirmed hit becomes restore-tier (unless workspace outputs disable
@@ -153,7 +161,7 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
153
161
  preProbed.set(node.id, { hash, hit })
154
162
  if (hit !== null && !keptOut.has(node.id)) restoreTier.add(node.id)
155
163
  }
156
- return { preProbed, restoreTier, uncachedKeys }
164
+ return { preProbed, restoreTier, uncachedKeys, groupKeys }
157
165
  } catch {
158
166
  // Fall through to the per-hash pool, which isolates a failing probe
159
167
  // to its own task.
@@ -176,7 +184,7 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
176
184
  }
177
185
  await Promise.all(Array.from({ length: workers }, () => pump()))
178
186
 
179
- return { preProbed, restoreTier, uncachedKeys }
187
+ return { preProbed, restoreTier, uncachedKeys, groupKeys }
180
188
  }
181
189
 
182
190
  /**
@@ -54,6 +54,7 @@ import { isDefaultBuild } from './projects.js'
54
54
  import { prepareSandbox } from './sandbox-request.js'
55
55
  import type { OutputDirSnapshot, SaveFacts } from './miss-save.js'
56
56
  import { admitTasks, taintTracker } from './admission.js'
57
+ import type { ExecuteArgs } from './execute-task.js'
57
58
  import { excludedTaint } from './excluded-keys.js'
58
59
  import { keyUpstream } from './upstream.js'
59
60
  import { busLogger, createEventBus, terminalSubscriber, type EventBus } from './events.js'
@@ -116,6 +117,7 @@ const emptyShortCircuit = (): ShortCircuit => ({
116
117
  preProbed: new Map(),
117
118
  restoreTier: new Set(),
118
119
  uncachedKeys: new Map(),
120
+ groupKeys: new Map(),
119
121
  })
120
122
 
121
123
  /**
@@ -938,9 +940,19 @@ async function runOnBus(
938
940
  // The scheduler's fail-fast stop, as the retry loop of a task already
939
941
  // in flight sees it.
940
942
  const failFast = new AbortController()
941
- const buildExecuteArgs = (node: TaskNode, upstream: TaskOutcome[], reuseProbe = true) => {
943
+ // A literal of the fields every task carries, then each optional one
944
+ // assigned only when present: twelve conditional spreads per task cost
945
+ // ~18 ms of object copying on a 1,090-package warm run (X-149).
946
+ const track = telemetry?.track
947
+ const armSandbox = sandboxArmer !== null ? () => sandboxArmer.arm() : undefined
948
+ const buildExecuteArgs = (
949
+ node: TaskNode,
950
+ upstream: TaskOutcome[],
951
+ reuseProbe = true,
952
+ ): ExecuteArgs => {
942
953
  const probe = reuseProbe ? shortCircuit.preProbed.get(node.id) : undefined
943
954
  const upfrontKey = shortCircuit.uncachedKeys.get(node.id)
955
+ const upfrontGroupKey = shortCircuit.groupKeys.get(node.id)
944
956
  if (
945
957
  options.continueMode === 'always' &&
946
958
  node.deps.some((d) => deadServerBehind(nodes, serverDied, d) !== undefined)
@@ -948,7 +960,7 @@ async function runOnBus(
948
960
  taintSeeds.add(node.id)
949
961
  const tainted = taint.judge(node, upstream)
950
962
  if (tainted) taintedRan.add(node.id)
951
- return {
963
+ const a: ExecuteArgs = {
952
964
  node,
953
965
  upstream,
954
966
  workspaceRoot,
@@ -956,31 +968,18 @@ async function runOnBus(
956
968
  cache,
957
969
  cachePolicy: policy,
958
970
  forwardArgs: options.forwardArgs,
959
- ...(options.retries !== undefined ? { retries: options.retries } : {}),
960
- ...(taskTimeoutDefault !== undefined ? { timeout: taskTimeoutDefault } : {}),
961
971
  log,
962
972
  executor: placements.executors.get(node.id) ?? UNPLACED_EXECUTOR,
963
- ...(download.modeOf.get(node.id) === 'deferred' ? { download: 'deferred' as const } : {}),
964
973
  deferred: deferredOutputs,
965
- ...(placements.remoteOnlyNoop.has(node.id) ? { remoteOnlyNoop: true } : {}),
966
- ...(placements.remoteOnly.has(node.id) ? { remoteOnly: true } : {}),
967
974
  nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
968
975
  cacheDir,
969
976
  runStartHrTimeNs,
970
977
  persistentRegistry,
971
978
  liveChildren,
972
- ...(telemetry?.track !== undefined ? { track: telemetry.track } : {}),
973
- ...(explainMiss !== undefined ? { explainMiss } : {}),
974
979
  gitFilesCache,
975
980
  hashCache,
976
981
  probesAfterWrites: lateProbes,
977
- ...(probe !== undefined ? { preProbed: probe } : {}),
978
- ...(upfrontKey !== undefined ? { upfrontKey } : {}),
979
- ...(tainted ? { taintedUpstream: true } : {}),
980
- ...(dependedOn.has(node.id) ? {} : { noDependants: true as const }),
981
- ...(holders.has(node.id) ? { terminal: true as const } : {}),
982
982
  fingerprintWatch,
983
- ...(sandboxArmer !== null ? { armSandbox: () => sandboxArmer.arm() } : {}),
984
983
  keyedProjects: keyed,
985
984
  outputDirSnapshots,
986
985
  deferSave: saveLane.defer,
@@ -988,6 +987,21 @@ async function runOnBus(
988
987
  stopSignal: stopRun.signal,
989
988
  failFast: failFast.signal,
990
989
  }
990
+ if (options.retries !== undefined) a.retries = options.retries
991
+ if (taskTimeoutDefault !== undefined) a.timeout = taskTimeoutDefault
992
+ if (download.modeOf.get(node.id) === 'deferred') a.download = 'deferred'
993
+ if (placements.remoteOnlyNoop.has(node.id)) a.remoteOnlyNoop = true
994
+ if (placements.remoteOnly.has(node.id)) a.remoteOnly = true
995
+ if (track !== undefined) a.track = track
996
+ if (explainMiss !== undefined) a.explainMiss = explainMiss
997
+ if (probe !== undefined) a.preProbed = probe
998
+ if (upfrontKey !== undefined) a.upfrontKey = upfrontKey
999
+ if (upfrontGroupKey !== undefined) a.upfrontGroupKey = upfrontGroupKey
1000
+ if (tainted) a.taintedUpstream = true
1001
+ if (!dependedOn.has(node.id)) a.noDependants = true
1002
+ if (holders.has(node.id)) a.terminal = true
1003
+ if (armSandbox !== undefined) a.armSandbox = armSandbox
1004
+ return a
991
1005
  }
992
1006
 
993
1007
  const executeWithDedup = admitTasks({
@@ -16,7 +16,12 @@ import type { CacheLayer, GitFilesCache } from '../cache/index.js'
16
16
  import { isLiteralPattern, normalizeGlob, relPosix } from '../util/index.js'
17
17
  import { commandWriteReach, mayWriteFingerprint, undeclaredWriteReach } from './sandbox-request.js'
18
18
  import { foldedDeps } from './keyed-projects.js'
19
- import { computeGroupKey, computeTaskHash, type HashCache } from './task-hash.js'
19
+ import {
20
+ computeGroupHash,
21
+ computeTaskHash,
22
+ type HashCache,
23
+ type UpfrontGroupKey,
24
+ } from './task-hash.js'
20
25
  import { filterUpstreamHashes, keyedOutcome, keyUpstream } from './upstream.js'
21
26
 
22
27
  export interface DeriveStableKeysArgs {
@@ -33,6 +38,11 @@ export interface DeriveStableKeysArgs {
33
38
  * that key reads, so execute-task need not derive it a second time.
34
39
  */
35
40
  uncachedKeys?: Map<string, string>
41
+ /**
42
+ * Filled with each unkeyed group's key and the upstream it folded:
43
+ * execute-task reuses the key when the live upstream matches.
44
+ */
45
+ groupKeys?: Map<string, UpfrontGroupKey>
36
46
  }
37
47
 
38
48
  export interface StableKey {
@@ -217,20 +227,27 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
217
227
  // dependents that filter inputs.tasks through the group still
218
228
  // cascade (and forward their producers, above). They inherit
219
229
  // instability from any unstable member.
220
- keyById.set(
221
- id,
222
- await computeGroupKey({
223
- node,
224
- upstream,
225
- workspaceRoot: args.workspaceRoot,
226
- workspaceFingerprint: args.workspaceFingerprint,
227
- cache: args.cache,
228
- forwardArgs: args.forwardArgs,
229
- nestedProjectDirs: args.nestedDirsByProject.get(node.projectName) ?? [],
230
- gitFilesCache: args.gitFilesCache,
231
- hashCache: args.hashCache,
232
- }),
233
- )
230
+ // `computeGroupKey`, with the unkeyed branch kept synchronous.
231
+ if (node.config.cache === undefined) {
232
+ const key = computeGroupHash(upstream)
233
+ keyById.set(id, key)
234
+ args.groupKeys?.set(id, { key, upstream })
235
+ } else {
236
+ keyById.set(
237
+ id,
238
+ await computeTaskHash({
239
+ node,
240
+ upstream,
241
+ workspaceRoot: args.workspaceRoot,
242
+ workspaceFingerprint: args.workspaceFingerprint,
243
+ cache: args.cache,
244
+ forwardArgs: args.forwardArgs,
245
+ nestedProjectDirs: args.nestedDirsByProject.get(node.projectName) ?? [],
246
+ gitFilesCache: args.gitFilesCache,
247
+ hashCache: args.hashCache,
248
+ }),
249
+ )
250
+ }
234
251
  if (node.deps.some((d) => unstableById.has(d))) unstableById.add(id)
235
252
  continue
236
253
  }
@@ -432,6 +432,16 @@ export async function computeGroupKey(args: ComputeHashArgs): Promise<string> {
432
432
  return computeTaskHash(args)
433
433
  }
434
434
 
435
+ /**
436
+ * An unkeyed group's key is `computeGroupHash` of its upstream alone, so a
437
+ * live upstream with the same ids and keys in the same order derives these
438
+ * bytes (execute-task's `sameUpstream`).
439
+ */
440
+ export interface UpfrontGroupKey {
441
+ key: string
442
+ upstream: TaskOutcome[]
443
+ }
444
+
435
445
  /**
436
446
  * Derive a stable hash for a group task (no `exec`) from its upstream
437
447
  * outcomes. Lets downstream tasks that filter `inputs.tasks` to
@@ -30,13 +30,9 @@ export function keyUpstream(node: TaskNode, upstream: TaskOutcome[]): TaskOutcom
30
30
 
31
31
  /** A dependency as a key reads it: only `node` and `hash` matter. */
32
32
  export function keyedOutcome(node: TaskNode, hash: string | undefined): TaskOutcome {
33
- return {
34
- node,
35
- status: 'success',
36
- exitCode: 0,
37
- durationMs: 0,
38
- ...(hash !== undefined ? { hash } : {}),
39
- }
33
+ const o: TaskOutcome = { node, status: 'success', exitCode: 0, durationMs: 0 }
34
+ if (hash !== undefined) o.hash = hash
35
+ return o
40
36
  }
41
37
 
42
38
  /** `node.deps` less its ordering-only edges: the dependencies a key may fold. */