@vzn/vx 0.0.618 → 0.0.620
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 +5 -5
- package/src/cli/watch-judge.ts +1 -1
- package/src/cli/watch.ts +14 -7
- package/src/orchestrator/options.ts +25 -1
- package/src/orchestrator/persistent.ts +36 -0
- package/src/orchestrator/prepare.ts +14 -7
- package/src/orchestrator/run.ts +77 -9
- package/src/workspace/lockfile.ts +18 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.620",
|
|
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.
|
|
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.620",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.620",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.620",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.620"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/watch-judge.ts
CHANGED
|
@@ -218,7 +218,7 @@ export class ChangeJudge {
|
|
|
218
218
|
this.noticed.add(firstAbs)
|
|
219
219
|
process.stdout.write(
|
|
220
220
|
byServer
|
|
221
|
-
? `vx watch: ${first} has started 3 cycles in a row, written while a server
|
|
221
|
+
? `vx watch: ${first} has started 3 cycles in a row, written while a server was running — a persistent task rewrites it. Add it to .gitignore (a git-ignored path never starts a cycle); until then every write starts a cycle.\n`
|
|
222
222
|
: `vx watch: ${first} has started 3 cycles in a row, written by the cycle before each — a task rewrites it every run. Declare it in cache.outputs (an output never starts a cycle) or add it to .gitignore (a git-ignored path never does); until then every run re-runs.\n`,
|
|
223
223
|
)
|
|
224
224
|
}
|
package/src/cli/watch.ts
CHANGED
|
@@ -409,8 +409,9 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
409
409
|
let claimedRootFiles = args.claimedRootFiles
|
|
410
410
|
let configImportFiles = args.configImports
|
|
411
411
|
let wsConfigImportFiles = args.workspaceConfigImports
|
|
412
|
-
// A dev server stays up while the loop idles
|
|
413
|
-
//
|
|
412
|
+
// A dev server stays up while the loop idles and across cycles; a cycle
|
|
413
|
+
// that must restart it (its config changed, it died) stops it before its
|
|
414
|
+
// graph, so the new one never meets the old one's port.
|
|
414
415
|
let held = args.held
|
|
415
416
|
|
|
416
417
|
// Reentrancy guard — never two orchestrator runs in flight. Events that
|
|
@@ -480,20 +481,26 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
480
481
|
// after the run stays, for what the run itself changed.
|
|
481
482
|
if (reread) await rearm(false)
|
|
482
483
|
process.stdout.write(`\nvx watch: ${label}; re-running...\n\n`)
|
|
483
|
-
await held?.stop()
|
|
484
|
-
held = undefined
|
|
485
|
-
// A Ctrl-C while the old server shut down ran a cycle anyway: a
|
|
486
|
-
// `not run` row and a footer printed above `stopped` (WD-22).
|
|
487
484
|
if (stop.aborted) break
|
|
488
485
|
restartTimings()
|
|
489
486
|
// On the mtime clock, as the arm is: from `Date.now()` a write the
|
|
490
487
|
// run made within a tick of it carried an earlier mtime and read as
|
|
491
488
|
// an edit (WD-20). The end needs no stamp: an mtime never leads it.
|
|
492
489
|
const start = fsClockNow(cacheDir)
|
|
493
|
-
|
|
490
|
+
// A server still up whose task is unchanged stays up: the cycle
|
|
491
|
+
// rebuilds what it depends on, and the dev tool's own reload
|
|
492
|
+
// takes it from there. The run stops the others before its graph.
|
|
493
|
+
const before = held
|
|
494
|
+
current = watchCycle(
|
|
495
|
+
{ ...opts, ...(before ? { keep: before } : {}) },
|
|
496
|
+
stop,
|
|
497
|
+
interruptIfWaiting,
|
|
498
|
+
)
|
|
494
499
|
const cycle = await runOrchestrator(current.opts)
|
|
495
500
|
current = undefined
|
|
496
501
|
held = cycle.persistent
|
|
502
|
+
// What this cycle did not take (a server whose task did not run).
|
|
503
|
+
await before?.stop()
|
|
497
504
|
if (cycle.refused !== undefined) process.stderr.write(`vx watch: ${cycle.refused}\n`)
|
|
498
505
|
changes.lastCycle = { start, end: Date.now() }
|
|
499
506
|
} catch (err) {
|
|
@@ -214,6 +214,18 @@ export interface RunOptions {
|
|
|
214
214
|
* is replaced only when the next cycle starts.
|
|
215
215
|
*/
|
|
216
216
|
holdPersistent?: boolean
|
|
217
|
+
/**
|
|
218
|
+
* The servers an earlier `holdPersistent` run handed back. Each one still
|
|
219
|
+
* up whose task this run would start again, with the same resolved
|
|
220
|
+
* config, is kept running and reported ready instead of restarted; what
|
|
221
|
+
* it writes reaches this run's renderer from then on. The rest are
|
|
222
|
+
* stopped before the graph starts, so a new one never meets the old
|
|
223
|
+
* one's port. The watch loop sets it: `vx watch dev` rebuilds what the
|
|
224
|
+
* server depends on and leaves the server up. The caller still stops
|
|
225
|
+
* the earlier hand-back afterwards; that stops only what this run did
|
|
226
|
+
* not take.
|
|
227
|
+
*/
|
|
228
|
+
keep?: HeldPersistent
|
|
217
229
|
/**
|
|
218
230
|
* Print the per-task table (`--verbosity 1`) just above the footer: the
|
|
219
231
|
* footer is the run's last word, so the caller cannot print it after.
|
|
@@ -305,6 +317,18 @@ export interface RunSummary {
|
|
|
305
317
|
export interface HeldPersistent {
|
|
306
318
|
/** The held tasks' ids, `pkg#task`. */
|
|
307
319
|
ids: readonly string[]
|
|
308
|
-
/** Send every held task `signal` (default SIGTERM), then SIGKILL after the kill grace; resolves once each is gone. */
|
|
320
|
+
/** Send every held task a later run did not take `signal` (default SIGTERM), then SIGKILL after the kill grace; resolves once each is gone. */
|
|
309
321
|
stop(signal?: 'SIGINT' | 'SIGTERM'): Promise<void>
|
|
322
|
+
/** Each held server by id, for a later run's `RunOptions.keep`. */
|
|
323
|
+
servers: ReadonlyMap<string, HeldServer>
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface HeldServer {
|
|
327
|
+
child: ReturnType<typeof Bun.spawn>
|
|
328
|
+
/** The task's resolved config as the run that started it saw it: a later run keeps the server only while its own reads the same. */
|
|
329
|
+
config: string
|
|
330
|
+
/** Where what the server writes goes; a run that keeps it points this at its own bus. */
|
|
331
|
+
out: { bus: EventBus }
|
|
332
|
+
/** A later run took the server (kept or stopped it): the hand-back's `stop()` and exit notice leave it alone. */
|
|
333
|
+
handOver(): void
|
|
310
334
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { isGroupTask, type TaskNode } from '../graph/index.js'
|
|
7
7
|
import { killGraceMs } from '../util/index.js'
|
|
8
8
|
import { holdGroups, killTree, untilGroupsGone } from '../exec/index.js'
|
|
9
|
+
import type { HeldServer } from './options.js'
|
|
9
10
|
|
|
10
11
|
type Child = ReturnType<typeof Bun.spawn>
|
|
11
12
|
|
|
@@ -116,3 +117,38 @@ export interface CrashedPersistent {
|
|
|
116
117
|
export function hasEnded(child: Child): boolean {
|
|
117
118
|
return child.exitCode !== null || child.signalCode !== null
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
/** A server's identity across watch cycles: its resolved config and the args forwarded to it. */
|
|
122
|
+
export function serverConfig(node: TaskNode, forwardArgs: readonly string[] | undefined): string {
|
|
123
|
+
return JSON.stringify([node.config, forwardArgs ?? []])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Split an earlier run's held servers into the ones this graph may keep
|
|
128
|
+
* (still up, a node of this graph, same config) and the ones it stops. A
|
|
129
|
+
* stopped one is handed over now, so the earlier run names no exit; a kept
|
|
130
|
+
* one only when its task is reached, and one never reached (a dependency
|
|
131
|
+
* failed) is the earlier run's to stop.
|
|
132
|
+
*/
|
|
133
|
+
export function takeHeldServers(
|
|
134
|
+
held: ReadonlyMap<string, HeldServer> | undefined,
|
|
135
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
136
|
+
forwardArgs: readonly string[] | undefined,
|
|
137
|
+
): { kept: Map<string, HeldServer>; stale: Child[] } {
|
|
138
|
+
const kept = new Map<string, HeldServer>()
|
|
139
|
+
const stale: Child[] = []
|
|
140
|
+
for (const [id, server] of held ?? []) {
|
|
141
|
+
if (hasEnded(server.child)) continue
|
|
142
|
+
const node = nodes.get(id)
|
|
143
|
+
if (
|
|
144
|
+
node?.config.exec?.persistent !== undefined &&
|
|
145
|
+
serverConfig(node, forwardArgs) === server.config
|
|
146
|
+
)
|
|
147
|
+
kept.set(id, server)
|
|
148
|
+
else {
|
|
149
|
+
server.handOver()
|
|
150
|
+
stale.push(server.child)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { kept, stale }
|
|
154
|
+
}
|
|
@@ -189,6 +189,18 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
189
189
|
// fingerprint. A watch cycle is a new run and reads it afresh.
|
|
190
190
|
const reads: LoadReads = new Map()
|
|
191
191
|
const workspaceRoot = await findWorkspaceRoot(options.cwd, reads)
|
|
192
|
+
// A frozen run with nothing staged reads the lock whatever discovery
|
|
193
|
+
// finds, so the read (1.1 MB at 1,000 projects, ~10 ms to read and
|
|
194
|
+
// parse) starts here and overlaps discovery's I/O instead of following
|
|
195
|
+
// the cache open (X-181). A missing or malformed lock still refuses at
|
|
196
|
+
// the config load, after any earlier refusal.
|
|
197
|
+
let lockRead: Promise<Lockfile> | undefined
|
|
198
|
+
const readLock = (): Promise<Lockfile> =>
|
|
199
|
+
(lockRead ??= readLockfile(workspaceRoot).then((read) => {
|
|
200
|
+
if (read === null) throw new UserError(FROZEN_WITHOUT_LOCK)
|
|
201
|
+
return read
|
|
202
|
+
}))
|
|
203
|
+
if (options.frozen === true && options.staged === undefined) void readLock().catch(() => {})
|
|
192
204
|
// An UNSCOPED run (no explicit scope, at least one bare task name)
|
|
193
205
|
// enumerates the whole tree whatever the configs say, so git starts
|
|
194
206
|
// HERE — the walk needs only the root — and overlaps the workspace
|
|
@@ -313,13 +325,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
313
325
|
// Read once, and only when a config is to be read from it: the CLI's
|
|
314
326
|
// selection pass may have staged every config (`options.staged`), and
|
|
315
327
|
// its load refused a frozen run without a lock. A 1,000-project lock is
|
|
316
|
-
// 1.1 MB of JSON (I-27)
|
|
317
|
-
|
|
318
|
-
const readLock = (): Promise<Lockfile> =>
|
|
319
|
-
(lockRead ??= readLockfile(workspaceRoot).then((read) => {
|
|
320
|
-
if (read === null) throw new UserError(FROZEN_WITHOUT_LOCK)
|
|
321
|
-
return read
|
|
322
|
-
}))
|
|
328
|
+
// 1.1 MB of JSON (I-27); a run with nothing staged started its read
|
|
329
|
+
// beside discovery, above.
|
|
323
330
|
const staged = options.staged
|
|
324
331
|
const allStaged =
|
|
325
332
|
staged !== undefined &&
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -63,7 +63,7 @@ import { buildAdmission, executorLabel, resolveExecutors, teardownPlugins } from
|
|
|
63
63
|
import { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
64
64
|
import { assembleRunSummary, isPassStatus } from './telemetry.js'
|
|
65
65
|
import type { RunContextRecord } from './telemetry.js'
|
|
66
|
-
import { defaultLogger, resolveOutputView } from './logger.js'
|
|
66
|
+
import { defaultLogger, resolveOutputView, type Logger } from './logger.js'
|
|
67
67
|
import { detectColors, type ColorSupport } from './colors.js'
|
|
68
68
|
import { plainOutput } from './plain-output.js'
|
|
69
69
|
import { formatPersistentList } from './framed-output.js'
|
|
@@ -103,13 +103,19 @@ import {
|
|
|
103
103
|
import { deriveStableKeys, probesAfterWrites } from './stable-keys.js'
|
|
104
104
|
|
|
105
105
|
import { assembleRunRecords } from './run-records.js'
|
|
106
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
hasEnded,
|
|
108
|
+
selectKeepAlive,
|
|
109
|
+
serverConfig,
|
|
110
|
+
shutdownPersistent,
|
|
111
|
+
takeHeldServers,
|
|
112
|
+
} from './persistent.js'
|
|
107
113
|
import { writeRunProfile, writeRunSummary } from './run-artifacts.js'
|
|
108
114
|
import { createSaveLane } from './save-lane.js'
|
|
109
115
|
import { formatOutcomeTable, formatRunSummary } from './summary.js'
|
|
110
116
|
import { detectFlaky, type FlakyCandidate, type FlakyFinding } from './failure-mode.js'
|
|
111
117
|
import { createMissExplainer } from './miss-reason.js'
|
|
112
|
-
import type { RunOptions, RunSummary } from './options.js'
|
|
118
|
+
import type { HeldServer, RunOptions, RunSummary } from './options.js'
|
|
113
119
|
|
|
114
120
|
// Per run, never shared: a `vx watch` process runs many, and a shared map
|
|
115
121
|
// is one `preProbed.set` away from leaking a hit across cycles.
|
|
@@ -358,6 +364,18 @@ async function runOnBus(
|
|
|
358
364
|
colors: ColorSupport,
|
|
359
365
|
): Promise<RunSummary> {
|
|
360
366
|
const log = busLogger(bus)
|
|
367
|
+
// A server's output goes through a cell of its own: a later watch cycle
|
|
368
|
+
// that keeps the server points it at that cycle's bus (`RunOptions.keep`).
|
|
369
|
+
const serverOuts = new Map<string, { bus: EventBus }>()
|
|
370
|
+
const serverLog = (id: string): Logger => {
|
|
371
|
+
const out = { bus }
|
|
372
|
+
serverOuts.set(id, out)
|
|
373
|
+
return {
|
|
374
|
+
...log,
|
|
375
|
+
taskStdout: (node, chunk) => out.bus.emit({ kind: 'task:stdout', node, chunk }),
|
|
376
|
+
taskStderr: (node, chunk) => out.bus.emit({ kind: 'task:stderr', node, chunk }),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
361
379
|
|
|
362
380
|
const prepared = await prepareRun(options, log)
|
|
363
381
|
mark('plugin stages')
|
|
@@ -469,6 +487,20 @@ async function runOnBus(
|
|
|
469
487
|
workspaceProjectCount,
|
|
470
488
|
} = prepared
|
|
471
489
|
const concurrency = options.concurrency ?? workspaceConfig?.concurrency ?? machineParallelism()
|
|
490
|
+
// An earlier cycle's servers: the ones this graph starts again unchanged
|
|
491
|
+
// stay up; the others go now, before anything binds their ports. A stop
|
|
492
|
+
// that lands meanwhile ends the run here, having run nothing: an aborted
|
|
493
|
+
// graph printed a `not run` row and a footer above `stopped` (WD-22).
|
|
494
|
+
const heldBefore = takeHeldServers(options.keep?.servers, nodes, options.forwardArgs)
|
|
495
|
+
if (heldBefore.stale.length > 0) {
|
|
496
|
+
await terminateChildren(() => heldBefore.stale)
|
|
497
|
+
if (options.signal?.aborted === true) {
|
|
498
|
+
disposePlugins?.()
|
|
499
|
+
await teardown()
|
|
500
|
+
prepared.cache.close()
|
|
501
|
+
return { ok: true, outcomes: [] }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
472
504
|
|
|
473
505
|
// Resolved ONCE per run, in declaration order, the local executor last.
|
|
474
506
|
// A broken factory aborts here, before any task starts.
|
|
@@ -968,7 +1000,7 @@ async function runOnBus(
|
|
|
968
1000
|
cache,
|
|
969
1001
|
cachePolicy: policy,
|
|
970
1002
|
forwardArgs: options.forwardArgs,
|
|
971
|
-
log,
|
|
1003
|
+
log: node.config.exec?.persistent !== undefined ? serverLog(node.id) : log,
|
|
972
1004
|
executor: placements.executors.get(node.id) ?? UNPLACED_EXECUTOR,
|
|
973
1005
|
deferred: deferredOutputs,
|
|
974
1006
|
nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
|
|
@@ -1041,6 +1073,25 @@ async function runOnBus(
|
|
|
1041
1073
|
// History is observability: an unreadable one judges nothing.
|
|
1042
1074
|
}
|
|
1043
1075
|
}
|
|
1076
|
+
const keepServer = (node: TaskNode): TaskOutcome | undefined => {
|
|
1077
|
+
const server = heldBefore.kept.get(node.id)
|
|
1078
|
+
if (server === undefined) return undefined
|
|
1079
|
+
heldBefore.kept.delete(node.id)
|
|
1080
|
+
if (hasEnded(server.child)) return undefined
|
|
1081
|
+
server.handOver()
|
|
1082
|
+
server.out.bus = bus
|
|
1083
|
+
serverOuts.set(node.id, server.out)
|
|
1084
|
+
persistentRegistry.set(node.id, server.child)
|
|
1085
|
+
const at = process.hrtime.bigint() - runStartHrTimeNs
|
|
1086
|
+
return {
|
|
1087
|
+
node,
|
|
1088
|
+
status: 'success',
|
|
1089
|
+
exitCode: 0,
|
|
1090
|
+
durationMs: 0,
|
|
1091
|
+
wallclockStartNs: at,
|
|
1092
|
+
wallclockEndNs: at,
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1044
1095
|
const outcomes = await runGraph({
|
|
1045
1096
|
nodes,
|
|
1046
1097
|
concurrency,
|
|
@@ -1063,7 +1114,12 @@ async function runOnBus(
|
|
|
1063
1114
|
narrowDemand(o.node.id)
|
|
1064
1115
|
},
|
|
1065
1116
|
onError: (node, line) => log.taskStderr(node, line),
|
|
1066
|
-
execute: (node, upstream) =>
|
|
1117
|
+
execute: (node, upstream) => {
|
|
1118
|
+
const kept = keepServer(node)
|
|
1119
|
+
return kept !== undefined
|
|
1120
|
+
? Promise.resolve(kept)
|
|
1121
|
+
: executeWithDedup(node, keyUpstream(node, upstream))
|
|
1122
|
+
},
|
|
1067
1123
|
// A `schedule` plugin's weights; the scheduler keeps its structural
|
|
1068
1124
|
// baseline as the tie-break. Empty map → baseline only.
|
|
1069
1125
|
...(prepared.priorities.size > 0 ? { priorities: prepared.priorities } : {}),
|
|
@@ -1384,11 +1440,22 @@ async function runOnBus(
|
|
|
1384
1440
|
// keep-alive says it: `vx watch` sat on "watching" over a dead dev
|
|
1385
1441
|
// server. The holder's own stop() is not such a death.
|
|
1386
1442
|
let stopping = false
|
|
1443
|
+
// Taken by a later run (`RunOptions.keep`): kept or stopped there.
|
|
1444
|
+
const handed = new Set<ReturnType<typeof Bun.spawn>>()
|
|
1445
|
+
const servers = new Map<string, HeldServer>()
|
|
1387
1446
|
keepAlive.nodes.forEach((n, i) => {
|
|
1447
|
+
const child = held[i]!
|
|
1448
|
+
servers.set(n.id, {
|
|
1449
|
+
child,
|
|
1450
|
+
config: serverConfig(n, options.forwardArgs),
|
|
1451
|
+
out: serverOuts.get(n.id) ?? { bus },
|
|
1452
|
+
handOver: () => void handed.add(child),
|
|
1453
|
+
})
|
|
1388
1454
|
// One that died during the graph was said then.
|
|
1389
|
-
if (hasEnded(
|
|
1390
|
-
void
|
|
1391
|
-
if (!stopping &&
|
|
1455
|
+
if (hasEnded(child)) return
|
|
1456
|
+
void child.exited.then((code) => {
|
|
1457
|
+
if (!stopping && !handed.has(child) && code !== 0)
|
|
1458
|
+
log.status(`vx: ${n.id} exited with code ${code}`)
|
|
1392
1459
|
})
|
|
1393
1460
|
})
|
|
1394
1461
|
return {
|
|
@@ -1396,9 +1463,10 @@ async function runOnBus(
|
|
|
1396
1463
|
outcomes: list,
|
|
1397
1464
|
persistent: {
|
|
1398
1465
|
ids: keepAlive.nodes.map((n) => n.id),
|
|
1466
|
+
servers,
|
|
1399
1467
|
stop: (signal) => {
|
|
1400
1468
|
stopping = true
|
|
1401
|
-
return terminateChildren(() => held, signal)
|
|
1469
|
+
return terminateChildren(() => held.filter((c) => !handed.has(c)), signal)
|
|
1402
1470
|
},
|
|
1403
1471
|
},
|
|
1404
1472
|
}
|
|
@@ -179,13 +179,18 @@ export async function frozenProjectConfigs(
|
|
|
179
179
|
// The lock is hand-editable; the stored config crosses the same
|
|
180
180
|
// boundary a freshly evaluated one does.
|
|
181
181
|
const digest = store === undefined ? undefined : lockDigests.get(lock)
|
|
182
|
-
const
|
|
182
|
+
const prefix =
|
|
183
183
|
digest === undefined
|
|
184
184
|
? undefined
|
|
185
|
-
:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
: `vx-lock-valid-v${CONFIG_EVAL_VERSION}\0${VERSION}\0${Bun.version}\0${digest}`
|
|
186
|
+
// One row for a lock whose every entry was accepted: a warm `--all` run
|
|
187
|
+
// asked for 1,090 per-project rows in one `IN` query, ~10 ms of its
|
|
188
|
+
// config load, where the eval-cache path it replaces is cheaper (X-181).
|
|
189
|
+
// A scoped run still validates, and remembers, only what it loads.
|
|
190
|
+
if (prefix !== undefined && store !== undefined && store.getConfigEval(prefix) !== null) {
|
|
191
|
+
return entries.map((e) => e.config)
|
|
192
|
+
}
|
|
193
|
+
const keys = prefix === undefined ? undefined : metas.map((m) => `${prefix}\0${m.name}`)
|
|
189
194
|
const known =
|
|
190
195
|
keys === undefined || store === undefined
|
|
191
196
|
? new Map<string, string>()
|
|
@@ -199,6 +204,14 @@ export async function frozenProjectConfigs(
|
|
|
199
204
|
validateProjectConfig(entries[i]!.config, `${LOCKFILE_NAME} (${metas[i]!.name})`)
|
|
200
205
|
if (key !== undefined) accepted.push([key, '1'])
|
|
201
206
|
}
|
|
207
|
+
// Every meta found its entry above, so as many distinct names as the
|
|
208
|
+
// lock holds is every entry of it.
|
|
209
|
+
if (
|
|
210
|
+
prefix !== undefined &&
|
|
211
|
+
new Set(metas.map((m) => m.name)).size === Object.keys(lock.projects).length
|
|
212
|
+
) {
|
|
213
|
+
accepted.push([prefix, '1'])
|
|
214
|
+
}
|
|
202
215
|
if (store !== undefined && accepted.length > 0) {
|
|
203
216
|
if (store.putConfigEvals !== undefined) store.putConfigEvals(accepted)
|
|
204
217
|
else for (const [k, v] of accepted) store.putConfigEval(k, v)
|