@vzn/vx 0.0.389 → 0.0.391

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.389",
3
+ "version": "0.0.391",
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.389",
25
- "@vzn/vx-linux-arm64": "0.0.389",
26
- "@vzn/vx-darwin-x64": "0.0.389",
27
- "@vzn/vx-darwin-arm64": "0.0.389"
24
+ "@vzn/vx-linux-x64": "0.0.391",
25
+ "@vzn/vx-linux-arm64": "0.0.391",
26
+ "@vzn/vx-darwin-x64": "0.0.391",
27
+ "@vzn/vx-darwin-arm64": "0.0.391"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -7,6 +7,30 @@ import type { Database, SQLQueryBindings } from 'bun:sqlite'
7
7
  import { lazyStatement } from './schema.js'
8
8
  import type { InvocationRecord, RunRecord } from './layer.js'
9
9
 
10
+ const INSERT_RUNS = `
11
+ INSERT INTO runs(
12
+ hash, project, task, status, exit_code, duration_ms, forward_args,
13
+ started_at, ended_at,
14
+ run_id, cpu_ms, peak_rss_bytes, wallclock_start_ns, wallclock_end_ns,
15
+ cache_hit, attempts, cached,
16
+ blocked_by, timed_out, sandbox_violations, not_ready
17
+ )
18
+ VALUES `
19
+ /** One `runs` row's placeholders: the 21 columns `bindRun` fills. */
20
+ const RUN_TUPLE = `(${Array(21).fill('?').join(', ')})`
21
+
22
+ /** An INSERT of `n` `runs` rows. */
23
+ function insertRunsSql(n: number): string {
24
+ return INSERT_RUNS + Array(n).fill(RUN_TUPLE).join(', ')
25
+ }
26
+
27
+ /**
28
+ * Rows per INSERT: 40 × 21 variables stays under 999, the oldest default
29
+ * ceiling. A statement per row was 8.7 ms of 1,000 rows, 40 at a time 6.4
30
+ * (2026-10-03).
31
+ */
32
+ const RUNS_PER_INSERT = 40
33
+
10
34
  export class RunHistory {
11
35
  private readonly insertRun: ReturnType<Database['prepare']>
12
36
  private readonly insertInvocation: ReturnType<Database['prepare']>
@@ -16,19 +40,7 @@ export class RunHistory {
16
40
  /** The store's salted value digest (L-4). */
17
41
  private readonly digestValue: (value: string) => string,
18
42
  ) {
19
- this.insertRun = lazyStatement(
20
- this.db,
21
- `
22
- INSERT INTO runs(
23
- hash, project, task, status, exit_code, duration_ms, forward_args,
24
- started_at, ended_at,
25
- run_id, cpu_ms, peak_rss_bytes, wallclock_start_ns, wallclock_end_ns,
26
- cache_hit, attempts, cached,
27
- blocked_by, timed_out, sandbox_violations, not_ready
28
- )
29
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
30
- `,
31
- )
43
+ this.insertRun = lazyStatement(this.db, insertRunsSql(1))
32
44
  this.insertInvocation = lazyStatement(
33
45
  this.db,
34
46
  `
@@ -59,11 +71,24 @@ export class RunHistory {
59
71
  // `bun:sqlite`'s `transaction()` returns a callable that wraps the
60
72
  // body in BEGIN/COMMIT, fsyncing once at the end. For a 200-task
61
73
  // run that's one fsync instead of 200.
62
- const insert = this.insertRun
63
- const tx = this.db.transaction((batch: readonly RunRecord[]) => {
64
- for (const r of batch) insert.run(...bindRun(r, this.digestValue))
65
- })
66
- tx(runs)
74
+ this.db.transaction(() => this.insertRuns(runs))()
75
+ }
76
+
77
+ /** `runs` as few INSERTs; each distinct forward-args list digested once. */
78
+ private insertRuns(runs: readonly RunRecord[]): void {
79
+ const digests = new Map<string, string>()
80
+ const digest = (value: string): string => {
81
+ let d = digests.get(value)
82
+ if (d === undefined) digests.set(value, (d = this.digestValue(value)))
83
+ return d
84
+ }
85
+ for (let i = 0; i < runs.length; i += RUNS_PER_INSERT) {
86
+ const block = runs.slice(i, i + RUNS_PER_INSERT)
87
+ const params: SQLQueryBindings[] = []
88
+ for (const r of block) params.push(...bindRun(r, digest))
89
+ // `query` caches by text: one statement for every full block.
90
+ this.db.query(insertRunsSql(block.length)).run(...params)
91
+ }
67
92
  }
68
93
 
69
94
  recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
@@ -73,11 +98,9 @@ export class RunHistory {
73
98
  // the entry-save transaction (`save`/`ingest`) so a warm
74
99
  // all-cache-hit run — which writes no `runs`-vs-`entry_inputs`
75
100
  // mismatch — pays nothing for the moat it isn't refreshing.
76
- const insertRun = this.insertRun
77
- const insertInvocation = this.insertInvocation
78
101
  this.db.transaction(() => {
79
- for (const r of bundle.runs) insertRun.run(...bindRun(r, this.digestValue))
80
- insertInvocation.run(...bindInvocation(bundle.invocation))
102
+ this.insertRuns(bundle.runs)
103
+ this.insertInvocation.run(...bindInvocation(bundle.invocation))
81
104
  })()
82
105
  }
83
106
 
package/src/index.ts CHANGED
@@ -51,7 +51,7 @@ export type {
51
51
  SandboxDenials,
52
52
  } from './config.js'
53
53
  export { defineProject, defineWorkspace } from './config.js'
54
- export { definePlugin } from './orchestrator/index.js'
54
+ export { definePlugin, refuseUnknownOptions } from './orchestrator/index.js'
55
55
 
56
56
  // Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
57
57
  // API). The graph primitives, the cache-key hashing seam and the git / host
@@ -86,6 +86,7 @@ export {
86
86
  type FingerprintContext,
87
87
  type VxPlugin,
88
88
  definePlugin,
89
+ refuseUnknownOptions,
89
90
  type PluginHooks,
90
91
  type PluginOrigin,
91
92
  } from './plugin.js'
@@ -15,7 +15,7 @@ import type { Cache, CacheLayer, CachePolicy } from '../cache/index.js'
15
15
  import { PLUGIN_PACKAGE, type ProjectConfig, type WorkspaceConfig } from '../config.js'
16
16
  import type { TaskExecutor } from '../exec/index.js'
17
17
  import type { TaskNode, TaskOutcome } from '../graph/index.js'
18
- import { UserError } from '../util/index.js'
18
+ import { nearest, UserError } from '../util/index.js'
19
19
  import type { ProjectMeta } from '../workspace/index.js'
20
20
  import type { EventBus, RunStartInfo } from './events.js'
21
21
  import type { TelemetryContext, TelemetrySink } from './telemetry.js'
@@ -483,6 +483,32 @@ export function definePlugin(origin: PluginOrigin, hooks: PluginHooks): VxPlugin
483
483
  return { ...hooks, name, [PLUGIN_PACKAGE]: name } as VxPlugin
484
484
  }
485
485
 
486
+ /**
487
+ * Refuse an option a plugin factory does not take, as core refuses an
488
+ * unknown config field. Bun strips a config's types, so a misspelt option
489
+ * (`reapi({ endpont })`) reached the factory, which read it as unset and
490
+ * quietly declined: the run went local with no word. `factory` names the
491
+ * call in the message (`reapi()`), `known` is the options the factory reads.
492
+ */
493
+ export function refuseUnknownOptions(
494
+ factory: string,
495
+ options: unknown,
496
+ known: readonly string[],
497
+ ): void {
498
+ if (options === undefined) return
499
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) {
500
+ throw new UserError(`${factory}: options must be an object`)
501
+ }
502
+ for (const key of Object.keys(options)) {
503
+ if (known.includes(key)) continue
504
+ const near = nearest(key, known)
505
+ throw new UserError(
506
+ `${factory} has unknown option "${key}" (allowed: ${[...known].sort().join(', ')})` +
507
+ (near === undefined ? '' : ` — did you mean ${near}?`),
508
+ )
509
+ }
510
+ }
511
+
486
512
  export interface InstallPluginsArgs {
487
513
  plugins: readonly Plugin[]
488
514
  workspaceRoot: string
@@ -1270,12 +1270,26 @@ async function runOnBus(
1270
1270
  // for it, and an exit 1 with no word about why is a mystery in a log.
1271
1271
  if (keepAlive.children.length > 0 && hold) {
1272
1272
  const held = keepAlive.children
1273
+ // A held server that dies on its own is said, as the foreground's
1274
+ // keep-alive says it: `vx watch` sat on "watching" over a dead dev
1275
+ // server. The holder's own stop() is not such a death.
1276
+ let stopping = false
1277
+ keepAlive.nodes.forEach((n, i) => {
1278
+ // One that died during the graph was said then.
1279
+ if (hasEnded(held[i]!)) return
1280
+ void held[i]!.exited.then((code) => {
1281
+ if (!stopping && code !== 0) log.status(`vx: ${n.id} exited with code ${code}`)
1282
+ })
1283
+ })
1273
1284
  return {
1274
1285
  ok,
1275
1286
  outcomes: list,
1276
1287
  persistent: {
1277
1288
  ids: keepAlive.nodes.map((n) => n.id),
1278
- stop: (signal) => terminateChildren(() => held, signal),
1289
+ stop: (signal) => {
1290
+ stopping = true
1291
+ return terminateChildren(() => held, signal)
1292
+ },
1279
1293
  },
1280
1294
  }
1281
1295
  }