@vzn/vx-reapi 0.0.591 → 0.0.595

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
@@ -223,11 +223,12 @@ through; a Tree file's setuid and setgid bits are dropped.
223
223
  ## Artifacts stream
224
224
 
225
225
  An artifact up to 256 KiB is read whole and sent in one batch, with no
226
- probe first. Past that the cache layer never holds an artifact whole: `put` digests the file-backed
227
- `Blob` vx hands it in one pass over its stream, asks `FindMissingBlobs`, and
228
- uploads from a second pass: past the batch limit (about 4 MiB) the file is
229
- read `chunkBytes` at a time as the ByteStream write drains, identity-encoded
230
- (the artifact is zstd already). `get` returns the ByteStream read as a
226
+ probe first. Up to the batch limit (about 4 MiB) it is read whole once,
227
+ probed with `FindMissingBlobs`, and those bytes sent. Past that the cache
228
+ layer never holds an artifact whole: `put` digests the file-backed `Blob`
229
+ vx hands it in one pass over its stream, asks `FindMissingBlobs`, and
230
+ uploads from a second pass, read `chunkBytes` at a time as the ByteStream
231
+ write drains, identity-encoded (the artifact is zstd already). `get` returns the ByteStream read as a
231
232
  `Response`, each message taken from the call as vx writes the previous one
232
233
  to disk.
233
234
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-reapi",
3
- "version": "0.0.591",
3
+ "version": "0.0.595",
4
4
  "license": "MIT",
5
5
  "description": "Bazel Remote Execution API plugin for vx — remote cache (ActionCache + CAS) against NativeLink, BuildBuddy, Buildbarn or bazel-remote.",
6
6
  "keywords": [
@@ -33,14 +33,13 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@grpc/grpc-js": "^1.12.0",
36
- "@grpc/proto-loader": "^0.7.13",
37
- "protobufjs": "^7.4.0"
36
+ "@grpc/proto-loader": "^0.7.13"
38
37
  },
39
38
  "publishConfig": {
40
39
  "access": "public"
41
40
  },
42
41
  "peerDependencies": {
43
- "@vzn/vx": "^0.0.591"
42
+ "@vzn/vx": "^0.0.595"
44
43
  },
45
44
  "repository": {
46
45
  "type": "git",
package/src/cache.ts CHANGED
@@ -159,32 +159,32 @@ export class ReapiRemoteCache {
159
159
  }
160
160
 
161
161
  /**
162
- * A large artifact takes two passes over `body`, never one in memory: the
163
- * digest first (the CAS address must be known before the server is asked),
164
- * then the upload from a second read of the stream.
162
+ * An artifact past `writeBlob`'s whole read takes two passes over `body`,
163
+ * never one in memory: the digest first (the CAS address must be known
164
+ * before the server is asked), then the upload from a second read of the
165
+ * stream.
165
166
  */
166
167
  async put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void> {
167
- let digest: Digest
168
- if (body.size <= SMALL_PUT_BYTES) {
168
+ // Read once, and those bytes hashed and sent, wherever `writeBlob` would
169
+ // read the Blob whole anyway: a second writer of the key renames its
170
+ // artifact over the file, and two reads sent its bytes under this one's
171
+ // digest (F-50).
172
+ const data = body.size <= this.client.wholeWriteBytes ? await body.bytes() : undefined
173
+ const digest = data === undefined ? await streamedDigestOf(body) : digestOf(data)
174
+ if (data !== undefined && data.length <= SMALL_PUT_BYTES) {
169
175
  // A small artifact is sent in one batch without asking first: the
170
176
  // probe was a round trip of its own to save an upload no larger than
171
177
  // it (102 → 71 ms per save at 15 ms one-way, F-26). Content-addressed,
172
178
  // so a re-send is harmless.
173
- // Read once, and those bytes hashed and sent: a second writer of the
174
- // key renames its artifact over the file, and two reads sent its bytes
175
- // under this one's digest (F-50).
176
179
  // A server whose batch limit is smaller refuses it; stream instead.
177
- const data = await body.bytes()
178
- digest = digestOf(data)
179
180
  await this.client
180
181
  .batchUpdateBlobs([{ digest, data }])
181
182
  .catch(() => this.client.writeBlob(digest, data))
182
183
  } else {
183
- digest = await streamedDigestOf(body)
184
184
  // Upload only what the server lacks: for a large artifact the probe is
185
185
  // cheap next to the bytes it can skip.
186
186
  const missing = await this.client.findMissingBlobs([digest])
187
- if (missing.length > 0) await this.client.writeBlob(digest, body)
187
+ if (missing.length > 0) await this.client.writeBlob(digest, data ?? body)
188
188
  }
189
189
  await this.client.updateActionResult(actionDigestFor(hash), {
190
190
  exit_code: 0,
package/src/executor.ts CHANGED
@@ -12,7 +12,14 @@
12
12
  import { mkdir, writeFile, chmod, readlink, realpath, rm, symlink, unlink } from 'node:fs/promises'
13
13
  import { constants, existsSync } from 'node:fs'
14
14
  import path from 'node:path'
15
- import { executorFallback, isLiteralPattern, isUserError, normalizeGlob, UserError } from '@vzn/vx'
15
+ import {
16
+ executorFallback,
17
+ isLiteralPattern,
18
+ isUserError,
19
+ normalizeGlob,
20
+ UserError,
21
+ withForwardArgs,
22
+ } from '@vzn/vx'
16
23
  import type { ExecuteRequest, ExecuteResult, TaskExecutor, TaskPlacement } from '@vzn/vx'
17
24
  import {
18
25
  buildInputTree,
@@ -30,6 +37,9 @@ import {
30
37
  import { execDigestFor } from './cache.js'
31
38
  import type { ActionResult, Digest, Directory, Operation, ReapiClient } from './wire.js'
32
39
 
40
+ // A default decoder strips a leading U+FEFF, which is part of a path.
41
+ const PATH_UTF8 = new TextDecoder('utf-8', { ignoreBOM: true })
42
+
33
43
  /**
34
44
  * Split a coarse output directory into the paths its GLOB actually names.
35
45
  *
@@ -346,7 +356,7 @@ function decodeOutputFile(buf: Uint8Array): {
346
356
  i = l
347
357
  const slice = buf.subarray(i, i + len)
348
358
  i += len
349
- if (field === 1) out.path = new TextDecoder().decode(slice)
359
+ if (field === 1) out.path = PATH_UTF8.decode(slice)
350
360
  else if (field === 2) out.digest = decodeDigest(slice)
351
361
  else if (field === 5 && len > 0) out.contents = slice
352
362
  } else if (wire === 0) {
@@ -370,8 +380,8 @@ function decodeOutputSymlink(buf: Uint8Array): { path: string; target: string }
370
380
  i = l
371
381
  const slice = buf.subarray(i, i + len)
372
382
  i += len
373
- if (key >>> 3 === 1) out.path = new TextDecoder().decode(slice)
374
- else if (key >>> 3 === 2) out.target = new TextDecoder().decode(slice)
383
+ if (key >>> 3 === 1) out.path = PATH_UTF8.decode(slice)
384
+ else if (key >>> 3 === 2) out.target = PATH_UTF8.decode(slice)
375
385
  }
376
386
  return out
377
387
  }
@@ -445,7 +455,7 @@ function decodeOutputDirectory(buf: Uint8Array): { path: string; tree_digest: Di
445
455
  i = l
446
456
  const slice = buf.subarray(i, i + len)
447
457
  i += len
448
- if (field === 1) out.path = new TextDecoder().decode(slice)
458
+ if (field === 1) out.path = PATH_UTF8.decode(slice)
449
459
  else if (field === 3) out.tree_digest = decodeDigest(slice)
450
460
  }
451
461
  return out
@@ -1249,12 +1259,9 @@ async function this_readStream(
1249
1259
  return ''
1250
1260
  }
1251
1261
 
1252
- /** Forwarded args are appended shell-quoted, exactly as the local executor does. */
1262
+ /** Forwarded args are placed by core's own `withForwardArgs`, as the local executor's are. */
1253
1263
  function fullCommand(req: ExecuteRequest, cdInto: string, projectRel: string): string {
1254
- const quoted =
1255
- req.forwardArgs.length === 0
1256
- ? req.command
1257
- : `${req.command} ${req.forwardArgs.map((a) => `'${a.replaceAll("'", `'\\''`)}'`).join(' ')}`
1264
+ const command = withForwardArgs(req.command, req.forwardArgs)
1258
1265
  // A remote action gets NO PATH from this machine — sending one would put a
1259
1266
  // host path in the action digest and split every laptop from every runner.
1260
1267
  // But a task's command is normally a package binary (`oxlint`, `tsc`), and
@@ -1282,7 +1289,7 @@ function fullCommand(req: ExecuteRequest, cdInto: string, projectRel: string): s
1282
1289
  return (
1283
1290
  `VX_ROOT=${root}; ${cd}` +
1284
1291
  `export PATH="$VX_ROOT/node_modules/.bin:$PWD/node_modules/.bin:$PATH"; ` +
1285
- quoted
1292
+ command
1286
1293
  )
1287
1294
  }
1288
1295
 
@@ -1342,7 +1349,7 @@ export function commandEnvironment(
1342
1349
  return [...merged].map(([name, value]) => ({ name, value }))
1343
1350
  }
1344
1351
 
1345
- export interface OutputPathSets {
1352
+ interface OutputPathSets {
1346
1353
  /** v2.1+ `output_paths` — deduped, sorted. */
1347
1354
  outputPaths: string[]
1348
1355
  /** v2.0 legacy split: wildcard-free globs are files, prefix-derived are directories. */
package/src/merkle.ts CHANGED
@@ -14,12 +14,15 @@ import { lstat, readlink } from 'node:fs/promises'
14
14
  import path from 'node:path'
15
15
  import type { Digest, Directory, DirectoryNode, FileNode, SymlinkNode } from './wire.js'
16
16
 
17
+ // A default decoder strips a leading U+FEFF, which is part of a name.
18
+ const PATH_UTF8 = new TextDecoder('utf-8', { ignoreBOM: true })
19
+
17
20
  export interface Blob {
18
21
  digest: Digest
19
22
  data: Uint8Array
20
23
  }
21
24
 
22
- export interface InputTree {
25
+ interface InputTree {
23
26
  /** Digest of the root `Directory` — goes in `Action.input_root_digest`. */
24
27
  root: Digest
25
28
  /** Every blob the server needs: file contents plus the Directory nodes. */
@@ -740,7 +743,7 @@ function decodeFileNode(buf: Uint8Array): FileNode {
740
743
  i = l
741
744
  const slice = buf.subarray(i, i + len)
742
745
  i += len
743
- if (field === 1) f.name = new TextDecoder().decode(slice)
746
+ if (field === 1) f.name = PATH_UTF8.decode(slice)
744
747
  else if (field === 2) f.digest = decodeDigestBytes(slice)
745
748
  else if (field === 6) f.node_properties = decodeNodeProperties(slice)
746
749
  } else if (wire === 0) {
@@ -802,7 +805,7 @@ function decodeDirectoryNode(buf: Uint8Array): DirectoryNode {
802
805
  i = l
803
806
  const slice = buf.subarray(i, i + len)
804
807
  i += len
805
- if (key >>> 3 === 1) d.name = new TextDecoder().decode(slice)
808
+ if (key >>> 3 === 1) d.name = PATH_UTF8.decode(slice)
806
809
  else if (key >>> 3 === 2) d.digest = decodeDigestBytes(slice)
807
810
  }
808
811
  return d
@@ -819,8 +822,8 @@ function decodeSymlinkNode(buf: Uint8Array): SymlinkNode {
819
822
  i = l
820
823
  const slice = buf.subarray(i, i + len)
821
824
  i += len
822
- if (key >>> 3 === 1) sl.name = new TextDecoder().decode(slice)
823
- else if (key >>> 3 === 2) sl.target = new TextDecoder().decode(slice)
825
+ if (key >>> 3 === 1) sl.name = PATH_UTF8.decode(slice)
826
+ else if (key >>> 3 === 2) sl.target = PATH_UTF8.decode(slice)
824
827
  }
825
828
  return sl
826
829
  }
package/src/wire.ts CHANGED
@@ -82,7 +82,7 @@ export interface Digest {
82
82
  size_bytes: number
83
83
  }
84
84
 
85
- export interface ServerCapabilities {
85
+ interface ServerCapabilities {
86
86
  digestFunctions: string[]
87
87
  maxBatchBytes: number
88
88
  acUpdateEnabled: boolean
@@ -1002,6 +1002,11 @@ export class ReapiClient {
1002
1002
  )
1003
1003
  }
1004
1004
 
1005
+ /** The largest `Blob` `writeBlob` reads whole rather than streams. */
1006
+ get wholeWriteBytes(): number {
1007
+ return this.negotiatedBatchBytes || SAFE_BATCH_BYTES
1008
+ }
1009
+
1005
1010
  /**
1006
1011
  * Upload a blob via ByteStream, chunked at `chunkBytes`, RESUMING an
1007
1012
  * interrupted identity upload from the server's committed offset
@@ -1017,8 +1022,7 @@ export class ReapiClient {
1017
1022
  * zstd artifact already. A Blob under the limit is small enough to read.
1018
1023
  */
1019
1024
  async writeBlob(digest: Digest, source: Uint8Array | Blob): Promise<void> {
1020
- const streamed =
1021
- source instanceof Blob && source.size > (this.negotiatedBatchBytes || SAFE_BATCH_BYTES)
1025
+ const streamed = source instanceof Blob && source.size > this.wholeWriteBytes
1022
1026
  const body = source instanceof Blob && !streamed ? await source.bytes() : source
1023
1027
  const compressed = this.compression && body instanceof Uint8Array
1024
1028
  // REAPI carries compression in the RESOURCE NAME:
@@ -1566,7 +1570,7 @@ export interface Operation {
1566
1570
  metadata?: { type_url?: string; value?: Uint8Array }
1567
1571
  }
1568
1572
 
1569
- export interface ExecuteOptions {
1573
+ interface ExecuteOptions {
1570
1574
  /** The operation's name, once the server has given one. */
1571
1575
  onOperation?: (name: string) => void
1572
1576
  /** Default TRUE: vx owns the cache decision, so the server must not re-check its own AC. */