@vzn/vx 0.0.361 → 0.0.362
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.
|
|
3
|
+
"version": "0.0.362",
|
|
4
4
|
"description": "A fast, correct 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.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.362",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.362",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.362",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.362"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/cache.ts
CHANGED
|
@@ -1590,8 +1590,24 @@ export class Cache implements CacheLayer {
|
|
|
1590
1590
|
// whole. A commit that fails after the rename takes the artifact back
|
|
1591
1591
|
// out: the old rows then name no artifact, which a probe reads as a
|
|
1592
1592
|
// miss.
|
|
1593
|
+
//
|
|
1594
|
+
// A previous artifact is moved aside first, never renamed over: ext4
|
|
1595
|
+
// flushes the incoming file's delayed blocks when a rename replaces a
|
|
1596
|
+
// file, 0.55 ms against 0.04 on the main thread with the write lock
|
|
1597
|
+
// held (a `--force` run, 2026-10-02). A reader probing between the two
|
|
1598
|
+
// renames finds no artifact, a miss (`ArtifactVanishedError`); one
|
|
1599
|
+
// holding the old file still reads it whole. The aside name is a temp
|
|
1600
|
+
// name, so a crash before its unlink leaves an orphan the sweep takes.
|
|
1601
|
+
const aside = this.tempPath(hash)
|
|
1602
|
+
let displaced = false
|
|
1593
1603
|
let renamed = false
|
|
1594
1604
|
const tx = this.db.transaction(() => {
|
|
1605
|
+
try {
|
|
1606
|
+
renameSync(finalPath, aside)
|
|
1607
|
+
displaced = true
|
|
1608
|
+
} catch (err) {
|
|
1609
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
|
|
1610
|
+
}
|
|
1595
1611
|
renameSync(tmpPath, finalPath)
|
|
1596
1612
|
renamed = true
|
|
1597
1613
|
insertEntry.run(
|
|
@@ -1628,11 +1644,13 @@ export class Cache implements CacheLayer {
|
|
|
1628
1644
|
try {
|
|
1629
1645
|
tx.immediate()
|
|
1630
1646
|
} catch (err) {
|
|
1647
|
+
endTx()
|
|
1631
1648
|
await unlink(renamed ? finalPath : tmpPath).catch(() => undefined)
|
|
1649
|
+
if (displaced) await unlink(aside).catch(() => undefined)
|
|
1632
1650
|
throw err
|
|
1633
|
-
} finally {
|
|
1634
|
-
endTx()
|
|
1635
1651
|
}
|
|
1652
|
+
endTx()
|
|
1653
|
+
if (displaced) await unlink(aside).catch(() => undefined)
|
|
1636
1654
|
}
|
|
1637
1655
|
|
|
1638
1656
|
/** Apply the deferred accessed_at bumps in one statement. */
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -181,6 +181,14 @@ export interface ScheduleOptions {
|
|
|
181
181
|
* be the next clean run's stale hit (`ExecuteArgs.taintedUpstream`).
|
|
182
182
|
*/
|
|
183
183
|
continueMode?: ContinueMode
|
|
184
|
+
/**
|
|
185
|
+
* A failure the graph's outcomes do not hold yet: a persistent task
|
|
186
|
+
* that died after it became ready. Its outcome said `success` when it
|
|
187
|
+
* was ready, so this is how a dependant not yet started learns of it:
|
|
188
|
+
* under 'deps-ok' it is skipped as below a failed task, and under
|
|
189
|
+
* 'never' it stops dispatch.
|
|
190
|
+
*/
|
|
191
|
+
serverDied?: (id: string) => boolean
|
|
184
192
|
execute: (node: TaskNode, upstream: TaskOutcome[]) => Promise<TaskOutcome>
|
|
185
193
|
onStart?: (node: TaskNode) => void
|
|
186
194
|
onFinish?: (outcome: TaskOutcome) => void
|
|
@@ -563,8 +571,19 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
563
571
|
// they're dep-independent, so they typically restore before a dep
|
|
564
572
|
// could fail anyway.
|
|
565
573
|
const aborted = (): boolean => options.signal?.aborted === true
|
|
574
|
+
const serverDied = options.serverDied ?? ((): boolean => false)
|
|
575
|
+
const servers =
|
|
576
|
+
options.serverDied === undefined
|
|
577
|
+
? []
|
|
578
|
+
: [...nodes.values()]
|
|
579
|
+
.filter((n) => n.config.exec?.persistent !== undefined)
|
|
580
|
+
.map((n) => n.id)
|
|
566
581
|
const willSkip = (id: string): boolean => {
|
|
567
582
|
if (failFastTripped || aborted()) return true
|
|
583
|
+
if (continueMode === 'never' && servers.some(serverDied)) {
|
|
584
|
+
failFastTripped = true
|
|
585
|
+
return true
|
|
586
|
+
}
|
|
568
587
|
if (inRestoreTier(id)) return false
|
|
569
588
|
if (continueMode === 'always') return false
|
|
570
589
|
const node = nodes.get(id) as TaskNode
|
|
@@ -580,7 +599,8 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
580
599
|
u?.status === 'failed' ||
|
|
581
600
|
u?.status === 'skipped' ||
|
|
582
601
|
u?.status === 'aborted' ||
|
|
583
|
-
blockedRestores.has(d)
|
|
602
|
+
blockedRestores.has(d) ||
|
|
603
|
+
serverDied(d)
|
|
584
604
|
)
|
|
585
605
|
})
|
|
586
606
|
}
|
|
@@ -651,6 +671,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
651
671
|
const viaRestore = node.deps.find((d) => blockedRestores.has(d))
|
|
652
672
|
const blockedBy =
|
|
653
673
|
blocker?.node.id ??
|
|
674
|
+
node.deps.find(serverDied) ??
|
|
654
675
|
viaSkip?.blockedBy ??
|
|
655
676
|
(viaRestore === undefined ? undefined : blockedRestores.get(viaRestore))
|
|
656
677
|
finishOne(id, {
|
|
@@ -821,7 +821,15 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
821
821
|
})
|
|
822
822
|
.catch(async (raw: unknown) => {
|
|
823
823
|
const err = nameExecutorFailure(args.executor, raw)
|
|
824
|
-
|
|
824
|
+
// A remote executor's message carries the server's own text, which
|
|
825
|
+
// may echo the env it was sent: masked here, where it is printed,
|
|
826
|
+
// and on the error the scheduler prints with its cause (L-39).
|
|
827
|
+
if (secrets !== null) {
|
|
828
|
+
for (const e of [err, err instanceof Error ? err.cause : undefined])
|
|
829
|
+
if (e instanceof Error) e.message = secrets.mask(e.message)
|
|
830
|
+
}
|
|
831
|
+
const message =
|
|
832
|
+
err instanceof Error ? err.message : (secrets?.mask(String(err)) ?? String(err))
|
|
825
833
|
log.taskStderr(node, `${message}\n`)
|
|
826
834
|
await sweepPlaceholders(placeholders)
|
|
827
835
|
throw err
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -922,6 +922,12 @@ async function runOnBus(
|
|
|
922
922
|
...(hasPooledExecutor(executors) ? { poolOf: poolOfPlacement(placements) } : {}),
|
|
923
923
|
...(admit !== undefined ? { admit } : {}),
|
|
924
924
|
...(options.continueMode !== undefined ? { continueMode: options.continueMode } : {}),
|
|
925
|
+
// A server that ended on its own is failed (the end of the run says
|
|
926
|
+
// so); what has not yet started hears it before it dispatches (C-88).
|
|
927
|
+
serverDied: (id) => {
|
|
928
|
+
const child = persistentRegistry.get(id)
|
|
929
|
+
return child !== undefined && hasEnded(child) && child.exitCode !== 0
|
|
930
|
+
},
|
|
925
931
|
signal: stopRun.signal,
|
|
926
932
|
onStart: (node) => {
|
|
927
933
|
log.taskStart?.(node)
|
|
@@ -296,7 +296,7 @@ function servesCommand(command: string): boolean {
|
|
|
296
296
|
/**
|
|
297
297
|
* Whether script `name` never exits: by name, as a watcher, as a server, or
|
|
298
298
|
* running such a script of its own package by name (docusaurus's
|
|
299
|
-
* `start:baseUrl`: `cross-env BASE_URL=… pnpm start`).
|
|
299
|
+
* `start:baseUrl`: `cross-env BASE_URL=… pnpm start`) or through a runner.
|
|
300
300
|
*/
|
|
301
301
|
function isPersistent(
|
|
302
302
|
name: string,
|
|
@@ -307,8 +307,13 @@ function isPersistent(
|
|
|
307
307
|
if (typeof own !== 'string' || seen.has(name)) return false
|
|
308
308
|
seen.add(name)
|
|
309
309
|
if (PERSISTENT_TASK_NAMES.has(name) || isWatcher(name, own) || servesCommand(own)) return true
|
|
310
|
+
// Through a runner too: `concurrently "npm:web" "npm:api"` and `run-p web
|
|
311
|
+
// api` over two servers mapped as one-shot tasks (D-113).
|
|
312
|
+
const text = Object.entries(scripts).filter(
|
|
313
|
+
(e): e is [string, string] => typeof e[1] === 'string',
|
|
314
|
+
)
|
|
310
315
|
return foreground(own).some((segment) =>
|
|
311
|
-
|
|
316
|
+
scriptRefs(segment, text).some((ref) => isPersistent(ref, scripts, seen)),
|
|
312
317
|
)
|
|
313
318
|
}
|
|
314
319
|
|