@vzn/vx 0.0.390 → 0.0.392

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.390",
3
+ "version": "0.0.392",
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.390",
25
- "@vzn/vx-linux-arm64": "0.0.390",
26
- "@vzn/vx-darwin-x64": "0.0.390",
27
- "@vzn/vx-darwin-arm64": "0.0.390"
24
+ "@vzn/vx-linux-x64": "0.0.392",
25
+ "@vzn/vx-linux-arm64": "0.0.392",
26
+ "@vzn/vx-darwin-x64": "0.0.392",
27
+ "@vzn/vx-darwin-arm64": "0.0.392"
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