@vzn/vx 0.0.385 → 0.0.387

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/README.md CHANGED
@@ -27,6 +27,8 @@
27
27
 
28
28
  vx N% faster: vx takes N% less time than that tool (1 − vx ÷ theirs); N% slower: N% more (vx ÷ theirs − 1).
29
29
 
30
+ Nx's column ran every task through npm run (~200 ms of CPU each; Turbo ran bun run), a harness fault since fixed; with bun, on a 4-core Linux box, Nx took 6m 59s cold and 4.50 s fully cached (vx 47% and 92% faster).
31
+
30
32
  Time added is the wall time over the tasks' own ideal schedule (3 min 38 s).
31
33
  Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
32
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.385",
3
+ "version": "0.0.387",
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.385",
25
- "@vzn/vx-linux-arm64": "0.0.385",
26
- "@vzn/vx-darwin-x64": "0.0.385",
27
- "@vzn/vx-darwin-arm64": "0.0.385"
24
+ "@vzn/vx-linux-x64": "0.0.387",
25
+ "@vzn/vx-linux-arm64": "0.0.387",
26
+ "@vzn/vx-darwin-x64": "0.0.387",
27
+ "@vzn/vx-darwin-arm64": "0.0.387"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -25,7 +25,7 @@
25
25
  // this handle, declared by a plugin's `cache` hook. The contract every
26
26
  // layer speaks is `CacheLayer` in layer.ts; `plugin-host.ts` enforces it.
27
27
 
28
- import { Database, type SQLQueryBindings } from 'bun:sqlite'
28
+ import { Database } from 'bun:sqlite'
29
29
  import {
30
30
  accessSync,
31
31
  closeSync,
@@ -38,7 +38,7 @@ import {
38
38
  } from 'node:fs'
39
39
  import { readdir, rm, stat, unlink, writeFile } from 'node:fs/promises'
40
40
  import path from 'node:path'
41
- import { createTables, lazyStatement } from './schema.js'
41
+ import { createTables, inHashes, lazyStatement } from './schema.js'
42
42
  import {
43
43
  UserError,
44
44
  formatBytes,
@@ -973,16 +973,8 @@ export class Cache implements CacheLayer {
973
973
  private async getManyEntries(hashes: readonly string[]): Promise<Map<string, CacheEntry>> {
974
974
  const out = new Map<string, CacheEntry>()
975
975
  if (!this.read || hashes.length === 0) return out
976
- const rows: EntryRow[] = []
977
- for (let i = 0; i < hashes.length; i += 900) {
978
- const chunk = hashes.slice(i, i + 900)
979
- const placeholders = chunk.map(() => '?').join(',')
980
- rows.push(
981
- ...(this.db
982
- .query(`${SELECT_ENTRY} WHERE e.hash IN (${placeholders})`)
983
- .all(...(chunk as readonly SQLQueryBindings[])) as EntryRow[]),
984
- )
985
- }
976
+ const { test, params } = inHashes(hashes)
977
+ const rows = this.db.query(`${SELECT_ENTRY} WHERE e.hash ${test}`).all(...params) as EntryRow[]
986
978
  if (rows.length === 0) return out
987
979
  const present = rows.map((r) => existsSync(this.tarPath(r.hash)))
988
980
  const live = rows.filter((_r, i) => present[i])
@@ -1696,15 +1688,8 @@ export class Cache implements CacheLayer {
1696
1688
  }
1697
1689
 
1698
1690
  private writeAccessed(hashes: readonly string[], now: number): void {
1699
- // Chunked: SQLite's bound-parameter ceiling is 32k on modern
1700
- // builds, but 900 keeps us safe on any build at negligible cost.
1701
- for (let i = 0; i < hashes.length; i += 900) {
1702
- const chunk = hashes.slice(i, i + 900)
1703
- const placeholders = chunk.map(() => '?').join(',')
1704
- this.db
1705
- .prepare(`UPDATE entries SET accessed_at = ? WHERE hash IN (${placeholders})`)
1706
- .run(now, ...chunk)
1707
- }
1691
+ const { test, params } = inHashes(hashes)
1692
+ this.db.prepare(`UPDATE entries SET accessed_at = ? WHERE hash ${test}`).run(now, ...params)
1708
1693
  }
1709
1694
 
1710
1695
  /**
@@ -1900,13 +1885,9 @@ export class Cache implements CacheLayer {
1900
1885
  // space the unlinks free. Rows-first failed there before any file went,
1901
1886
  // and the one verb meant to free a full disk freed nothing (A-14).
1902
1887
  await Promise.all(hashes.map((h) => rm(this.tarPath(h), { force: true })))
1888
+ const { test, params } = inHashes(rows)
1903
1889
  const deleteRows = this.db.transaction(() => {
1904
- for (let i = 0; i < rows.length; i += 900) {
1905
- const chunk = rows.slice(i, i + 900)
1906
- this.db
1907
- .prepare(`DELETE FROM entries WHERE hash IN (${chunk.map(() => '?').join(',')})`)
1908
- .run(...(chunk as readonly SQLQueryBindings[]))
1909
- }
1890
+ this.db.prepare(`DELETE FROM entries WHERE hash ${test}`).run(...params)
1910
1891
  })
1911
1892
  try {
1912
1893
  deleteRows()
@@ -8,8 +8,8 @@
8
8
  /** `mtime_ms` of a recorded output prefix that did not exist when the snapshot was taken. */
9
9
  const ABSENT_DIR_MTIME = -1
10
10
 
11
- import type { Database, SQLQueryBindings } from 'bun:sqlite'
12
- import { lazyStatement } from './schema.js'
11
+ import type { Database } from 'bun:sqlite'
12
+ import { inHashes, lazyStatement } from './schema.js'
13
13
  import { lstatSync, statSync } from 'node:fs'
14
14
  import { lstat, readdir } from 'node:fs/promises'
15
15
  import path from 'node:path'
@@ -84,21 +84,18 @@ export class OutputIndex {
84
84
  loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> {
85
85
  const out = new Map<string, OutputFileRow[]>()
86
86
  if (hashes.length === 0) return out
87
- // Inline placeholders for an IN-list — bun:sqlite doesn't ship
88
- // rarray, but `IN (?, ?, …)` with N≤~999 is fast and avoids per-
89
- // hash select.get() overhead. `db.query` (not `db.prepare`) caches the
90
- // compiled statement keyed by the SQL text — so the dominant single-hash
91
- // warm-hit path (called up to 3× per hit) reuses one statement instead of
92
- // recompiling on every call.
87
+ // `db.query` (not `db.prepare`) caches the compiled statement keyed by
88
+ // the SQL text, which `inHashes` keeps to two: the single-hash warm-hit
89
+ // path (called up to 3× per hit) reuses one instead of recompiling.
93
90
  // A reader in the same process (`vx watch`'s next cycle, a test) sees
94
91
  // the stamps taken, not only the ones flushed: overlaid below from
95
92
  // memory. A flush here committed a transaction per read, and a restore
96
93
  // reads its rows twice.
97
- const placeholders = hashes.map(() => '?').join(',')
94
+ const { test, params } = inHashes(hashes)
98
95
  const stmt = this.db.query(
99
- `SELECT entry_hash, path, size_bytes, mode, mtime_ms, ino, ctime_ms FROM output_files WHERE entry_hash IN (${placeholders})`,
96
+ `SELECT entry_hash, path, size_bytes, mode, mtime_ms, ino, ctime_ms FROM output_files WHERE entry_hash ${test}`,
100
97
  )
101
- const rows = stmt.all(...(hashes as readonly SQLQueryBindings[])) as Array<{
98
+ const rows = stmt.all(...params) as Array<{
102
99
  entry_hash: string
103
100
  path: string
104
101
  size_bytes: number
@@ -324,12 +321,10 @@ export class OutputIndex {
324
321
  // A reader in the same process (`vx watch`'s next cycle, a test) sees
325
322
  // what was snapshotted, not what was flushed.
326
323
  this.flushOutputDirs()
327
- const placeholders = hashes.map(() => '?').join(',')
324
+ const { test, params } = inHashes(hashes)
328
325
  const rows = this.db
329
- .query(
330
- `SELECT entry_hash, path, mtime_ms FROM output_dirs WHERE entry_hash IN (${placeholders})`,
331
- )
332
- .all(...(hashes as readonly SQLQueryBindings[])) as Array<{
326
+ .query(`SELECT entry_hash, path, mtime_ms FROM output_dirs WHERE entry_hash ${test}`)
327
+ .all(...params) as Array<{
333
328
  entry_hash: string
334
329
  path: string
335
330
  mtime_ms: number
@@ -242,3 +242,16 @@ export function createTables(db: Database): void {
242
242
  );
243
243
  `)
244
244
  }
245
+
246
+ /**
247
+ * A `WHERE <column> …` test against many hashes, as one statement whatever
248
+ * their number. A list of `?` is a new statement per length, compiled each
249
+ * run: 2.0 ms against 1.4 for 900 hashes through `json_each` (2026-10-03).
250
+ * One hash keeps `= ?`, ~3 µs cheaper than parsing a one-element array, on
251
+ * the per-hit path that asks for one at a time.
252
+ */
253
+ export function inHashes(hashes: readonly string[]): { test: string; params: string[] } {
254
+ return hashes.length === 1
255
+ ? { test: '= ?', params: [hashes[0]!] }
256
+ : { test: 'IN (SELECT value FROM json_each(?))', params: [JSON.stringify(hashes)] }
257
+ }