@vzn/vx-dte 0.0.0 → 0.0.649
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/LICENSE +21 -0
- package/README.md +923 -1
- package/index.ts +4 -0
- package/package.json +40 -4
- package/src/cpu-account.ts +134 -0
- package/src/dispatch.ts +268 -0
- package/src/docker.ts +156 -0
- package/src/forecast.ts +68 -0
- package/src/github.ts +523 -0
- package/src/gitlab.ts +130 -0
- package/src/index.ts +12 -0
- package/src/init.ts +522 -0
- package/src/local.ts +256 -0
- package/src/messages.ts +219 -0
- package/src/noise.ts +248 -0
- package/src/orchestrator.ts +2359 -0
- package/src/outputs.ts +117 -0
- package/src/planner.ts +988 -0
- package/src/plugin.ts +1020 -0
- package/src/printable.ts +11 -0
- package/src/seal.ts +509 -0
- package/src/strategy.ts +50 -0
- package/src/trace.ts +288 -0
- package/src/tree.ts +72 -0
- package/src/usage.ts +135 -0
- package/src/warm-ups.ts +85 -0
- package/src/wire.ts +1060 -0
- package/src/worker.ts +1038 -0
package/src/outputs.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// The files a task's declared outputs match in a checkout: what a worker
|
|
2
|
+
// clears before it runs the task, so an earlier task's files under them
|
|
3
|
+
// are not saved as this one's.
|
|
4
|
+
|
|
5
|
+
import { lstat, readdir, realpath, stat } from 'node:fs/promises'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { outputDirPrefixes, restorableOutput } from '@vzn/vx'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The files declared outputs name, workspace-relative: `files` under the
|
|
11
|
+
* project, `workspaceFiles` under the root. A `!` glob drops what it matches.
|
|
12
|
+
*/
|
|
13
|
+
export async function outputFiles(
|
|
14
|
+
root: string,
|
|
15
|
+
project: string,
|
|
16
|
+
outputs: { readonly files: readonly string[]; readonly workspaceFiles: readonly string[] },
|
|
17
|
+
): Promise<string[]> {
|
|
18
|
+
const found = new Set<string>()
|
|
19
|
+
// Core's own matcher, so a glob means here what it means to a restore.
|
|
20
|
+
const scan = async (base: string, globs: readonly string[], scope: 'project' | 'workspace') => {
|
|
21
|
+
if (globs.length === 0) return
|
|
22
|
+
const wanted = restorableOutput(globs, scope)
|
|
23
|
+
const intoModules = globs.some((g) => g.includes('node_modules'))
|
|
24
|
+
const add = (rel: string) => found.add(base === '' ? rel : `${base}/${rel}`)
|
|
25
|
+
const walk = async (rel: string): Promise<void> => {
|
|
26
|
+
const at = path.join(root, base, rel)
|
|
27
|
+
const entries = await readdir(at, { withFileTypes: true }).catch(
|
|
28
|
+
async (err: NodeJS.ErrnoException) => {
|
|
29
|
+
// A literal output names a file: its prefix is that file. ENOTDIR
|
|
30
|
+
// also means a FILE above it (`dist/x.js` with `dist` a file): no
|
|
31
|
+
// path there, and clearing one threw (2026-10-25).
|
|
32
|
+
// quiet: a path that is not there is no output
|
|
33
|
+
if (err.code === 'ENOTDIR' && wanted(rel) && (await lstat(at).catch(() => null))) add(rel)
|
|
34
|
+
return []
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
for (const e of entries) {
|
|
38
|
+
const child = rel === '' ? e.name : `${rel}/${e.name}`
|
|
39
|
+
if (e.isDirectory()) {
|
|
40
|
+
if (e.name === '.git' || (e.name === 'node_modules' && !intoModules)) continue
|
|
41
|
+
await walk(child)
|
|
42
|
+
} else if ((e.isFile() || e.isSymbolicLink()) && wanted(child)) {
|
|
43
|
+
add(child)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the tree at `rel` is where it says, under `base`: the walk
|
|
49
|
+
* follows a link only at its start (a nested one is an entry), and a
|
|
50
|
+
* worker deletes what it names. `dist -> ../victim` for `dist/**`
|
|
51
|
+
* deleted a file out of the project, and `dist -> .git` the repository,
|
|
52
|
+
* as core measured (`containedIn`, `refuseDirLinkedOut`), which refuses
|
|
53
|
+
* the task's save there too.
|
|
54
|
+
*/
|
|
55
|
+
const lies = async (realBase: string | undefined, rel: string): Promise<boolean> => {
|
|
56
|
+
const at = path.join(root, base, rel)
|
|
57
|
+
// quiet: nothing there is nothing to walk
|
|
58
|
+
const st = await stat(at).catch(() => undefined)
|
|
59
|
+
if (st === undefined) return true
|
|
60
|
+
const dir = st.isDirectory() ? rel : path.dirname(rel)
|
|
61
|
+
// quiet: a tree that will not resolve is left alone
|
|
62
|
+
const real = await realpath(path.join(root, base, dir)).catch(() => undefined)
|
|
63
|
+
if (real === undefined || realBase === undefined) return false
|
|
64
|
+
if (real === path.join(realBase, dir)) return true
|
|
65
|
+
if (real !== realBase && !real.startsWith(realBase + path.sep)) return false
|
|
66
|
+
const parts = real.slice(realBase.length + 1).split(path.sep)
|
|
67
|
+
return !parts.some((p) => p.toLowerCase() === '.git') && parts[0]?.toLowerCase() !== '.vx'
|
|
68
|
+
}
|
|
69
|
+
// Only the trees the globs can reach: walking the whole project for
|
|
70
|
+
// each task cost a worker 450 ms a task in a project of 9000 output
|
|
71
|
+
// dirs (2026-10-25). Root-anchored or escaping globs keep the walk.
|
|
72
|
+
const dirs = outputDirPrefixes(globs.filter((g) => !g.startsWith('!'))) ?? ['']
|
|
73
|
+
// quiet: a base that will not resolve holds no outputs; `lies` refuses it
|
|
74
|
+
const realBase = await realpath(path.join(root, base)).catch(() => undefined)
|
|
75
|
+
await Promise.all(dirs.map(async (d) => (await lies(realBase, d)) && walk(d)))
|
|
76
|
+
}
|
|
77
|
+
await scan(project, outputs.files, 'project')
|
|
78
|
+
await scan('', outputs.workspaceFiles, 'workspace')
|
|
79
|
+
return [...found].sort()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Of the files under a task's outputs, those an earlier task left
|
|
84
|
+
* (`cleared`: neither tracked, a nested project's, nor an upstream's) and
|
|
85
|
+
* those an upstream's outputs name (`held`). The held ones were read
|
|
86
|
+
* against every cleared one: 20,000 stale files cost a worker a second a
|
|
87
|
+
* task (2026-10-25).
|
|
88
|
+
*/
|
|
89
|
+
export function leftUnder(
|
|
90
|
+
under: readonly string[],
|
|
91
|
+
tracked: ReadonlySet<string>,
|
|
92
|
+
nested: readonly string[],
|
|
93
|
+
upstream: readonly ((file: string) => boolean)[],
|
|
94
|
+
): { cleared: string[]; held: string[] } {
|
|
95
|
+
const cleared: string[] = []
|
|
96
|
+
const held: string[] = []
|
|
97
|
+
for (const f of under) {
|
|
98
|
+
if (upstream.some((up) => up(f))) held.push(f)
|
|
99
|
+
else if (!tracked.has(f) && !nested.some((n) => f.startsWith(`${n}/`))) cleared.push(f)
|
|
100
|
+
}
|
|
101
|
+
return { cleared, held }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether declared outputs name `file` (workspace-relative), as `outputFiles` would find it. */
|
|
105
|
+
export function names(
|
|
106
|
+
project: string,
|
|
107
|
+
outputs: { readonly files: readonly string[]; readonly workspaceFiles: readonly string[] },
|
|
108
|
+
): (file: string) => boolean {
|
|
109
|
+
const inProject =
|
|
110
|
+
outputs.files.length === 0 ? () => false : restorableOutput(outputs.files, 'project')
|
|
111
|
+
const inRoot =
|
|
112
|
+
outputs.workspaceFiles.length === 0
|
|
113
|
+
? () => false
|
|
114
|
+
: restorableOutput(outputs.workspaceFiles, 'workspace')
|
|
115
|
+
const prefix = project === '' ? '' : `${project}/`
|
|
116
|
+
return (file) => inRoot(file) || (file.startsWith(prefix) && inProject(file.slice(prefix.length)))
|
|
117
|
+
}
|