@vzn/vx-migrate 0.0.609 → 0.0.611
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 +1 -1
- package/package.json +2 -2
- package/src/nx-cache/index.ts +19 -14
- package/src/remote-breaker.ts +34 -3
- package/src/turbo-cache/index.ts +29 -21
package/README.md
CHANGED
|
@@ -335,7 +335,7 @@ The Nx spec has no existence probe, so `has` (the `--dry` prediction; the prefet
|
|
|
335
335
|
- A remote error degrades to a **miss** and a warning that names the request, the artifact and the server — `vx/turbo-cache: upload 32248a2a7c89e241 to https://cache.example.com/v8/artifacts failed: no answer within 60000 ms` — never the token. The run never fails because of the cache.
|
|
336
336
|
- The same failure is said once per run: an unreachable server fails the probe, the download and the upload alike, and the run prints the first and, at its end, `vx/turbo-cache: 2 more requests failed the same way: Unable to connect. Is the computer able to access the url?` (core's `LayeredCache` does the counting, for every cache plugin).
|
|
337
337
|
- A request answered `429` or `5xx` (not `501`), or one that never connected (a refused port, an unresolved host), is sent again after 2 s — a `429` after its `Retry-After`, capped at 10 s — as Turbo's client does. A spent deadline is not: it has already cost its wait.
|
|
338
|
-
- Three outages in a row (no connection, no answer within the deadline, or a `5xx` once the resends are spent) open Turbo's outage breaker: no request is sent for 30 s, each lookup is a miss at once, then one request probes and its answer closes or reopens it. A hung server costs three deadlines, not one per task.
|
|
338
|
+
- Three outages in a row (no connection, no answer within the deadline, or a `5xx` once the resends are spent) open Turbo's outage breaker: no request is sent for 30 s, each lookup is a miss at once, then one request probes and its answer closes or reopens it. A hit is judged when its body ends, so a server that sends headers and then stalls the body counts the same way. A hung server costs three deadlines, not one per task.
|
|
339
339
|
- A refused token (`401`/`403`) warns **once** and turns the layer off for the rest of the process (a `403` on an upload is a read-only token — Nx's spec, or turborepo-remote-cache's `READ_ONLY` and write-less JWTs: it turns off uploads alone, and reads go on) — including the requests already in flight when the refusal lands, which degrade in silence rather than repeating it (a six-project run printed five identical lines before 2026-09-20).
|
|
340
340
|
- Policy (`--cache=remote:r`, …) is enforced by core's `LayeredCache`, which the plugins wrap — a read-only token pairs naturally with `remote:r`.
|
|
341
341
|
- Each tool's own switches narrow that policy, never widen it, as they do for the tool: `TURBO_CACHE` (`--cache`'s syntax, an omitted source off) and `TURBO_REMOTE_CACHE_READ_ONLY` for `turboCache()`, `NX_SKIP_REMOTE_CACHE` / `NX_DISABLE_REMOTE_CACHE` (`true`) for `nxCache()`. A CI that keeps untrusted pull requests off the shared cache that way keeps vx off it too.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.611",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Adopt @vzn/vx from Turborepo or Nx: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@vzn/vx": "^0.0.
|
|
42
|
+
"@vzn/vx": "^0.0.611"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/nx-cache/index.ts
CHANGED
|
@@ -142,18 +142,20 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
142
142
|
// every hit read as a corrupt artifact (nx#33092).
|
|
143
143
|
headers['Accept'] = 'application/octet-stream'
|
|
144
144
|
}
|
|
145
|
-
const res = await this.breaker.send(
|
|
146
|
-
|
|
147
|
-
(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
const res = await this.breaker.send(
|
|
146
|
+
() =>
|
|
147
|
+
withRetry(
|
|
148
|
+
() =>
|
|
149
|
+
this.fetchImpl(`${this.config.server}/v1/cache/${hash}`, {
|
|
150
|
+
method,
|
|
151
|
+
headers,
|
|
152
|
+
...(body === undefined ? {} : { body }),
|
|
153
|
+
signal: deadline(this.config.timeoutMs),
|
|
154
|
+
}),
|
|
155
|
+
this.config.retries,
|
|
156
|
+
this.wait,
|
|
157
|
+
),
|
|
158
|
+
method === 'GET',
|
|
157
159
|
)
|
|
158
160
|
if (method === 'PUT' && res.status === 403) {
|
|
159
161
|
const first = !this.writesRefused
|
|
@@ -184,8 +186,11 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
184
186
|
if (this.disabled) return null
|
|
185
187
|
const res = await this.request('GET', hash)
|
|
186
188
|
if (res === undefined) return null
|
|
187
|
-
if (res.status
|
|
188
|
-
|
|
189
|
+
if (res.status !== 200) {
|
|
190
|
+
await res.body?.cancel()
|
|
191
|
+
if (res.status === 404) return null
|
|
192
|
+
throw new Error(`HTTP ${res.status}`)
|
|
193
|
+
}
|
|
189
194
|
return res
|
|
190
195
|
}
|
|
191
196
|
|
package/src/remote-breaker.ts
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* task its full deadline. A result that lands after the breaker moved on
|
|
8
8
|
* (a request already in flight when it opened) changes nothing. Shared by
|
|
9
9
|
* `turboCache()` and `nxCache()`.
|
|
10
|
+
*
|
|
11
|
+
* A hit's body (`settleOnBody`, a 200 with a body) is judged when the
|
|
12
|
+
* body ends, not when its headers land: a server that sent headers in
|
|
13
|
+
* time and then stalled the body was judged a success, reset the count,
|
|
14
|
+
* and cost every task its full deadline. The body's error is the outage;
|
|
15
|
+
* its end or a cancel is the success. A body nobody reads or cancels
|
|
16
|
+
* leaves its verdict open.
|
|
10
17
|
*/
|
|
11
18
|
export class OutageBreaker {
|
|
12
19
|
private failures = 0
|
|
@@ -15,7 +22,7 @@ export class OutageBreaker {
|
|
|
15
22
|
private generation = 0
|
|
16
23
|
constructor(private readonly now: () => number = Date.now) {}
|
|
17
24
|
|
|
18
|
-
async send(request: () => Promise<Response
|
|
25
|
+
async send(request: () => Promise<Response>, settleOnBody = false): Promise<Response> {
|
|
19
26
|
let probe = false
|
|
20
27
|
if (this.openUntil !== undefined) {
|
|
21
28
|
if (this.now() < this.openUntil || this.probing)
|
|
@@ -32,8 +39,32 @@ export class OutageBreaker {
|
|
|
32
39
|
this.finish(generation, probe, true)
|
|
33
40
|
throw err
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
if (!settleOnBody || res.status !== 200 || res.body === null) {
|
|
43
|
+
this.finish(generation, probe, res.status >= 500)
|
|
44
|
+
return res
|
|
45
|
+
}
|
|
46
|
+
const reader = res.body.getReader()
|
|
47
|
+
const settle = (outage: boolean) => this.finish(generation, probe, outage)
|
|
48
|
+
return new Response(
|
|
49
|
+
new ReadableStream<Uint8Array>({
|
|
50
|
+
async pull(controller) {
|
|
51
|
+
const chunk = await reader.read().catch((err: unknown) => {
|
|
52
|
+
settle(true)
|
|
53
|
+
controller.error(err)
|
|
54
|
+
})
|
|
55
|
+
if (chunk === undefined) return
|
|
56
|
+
if (chunk.done) {
|
|
57
|
+
settle(false)
|
|
58
|
+
controller.close()
|
|
59
|
+
} else controller.enqueue(chunk.value)
|
|
60
|
+
},
|
|
61
|
+
async cancel(reason) {
|
|
62
|
+
settle(false)
|
|
63
|
+
await reader.cancel(reason)
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
{ status: res.status, statusText: res.statusText, headers: res.headers },
|
|
67
|
+
)
|
|
37
68
|
}
|
|
38
69
|
|
|
39
70
|
private finish(generation: number, probe: boolean, outage: boolean): void {
|
package/src/turbo-cache/index.ts
CHANGED
|
@@ -471,24 +471,26 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
471
471
|
const signal = () => (timeoutMs === 0 ? undefined : deadline(timeoutMs))
|
|
472
472
|
// The batch query is never preflighted, as in Turbo.
|
|
473
473
|
const preflight = this.config.preflight && method !== 'POST'
|
|
474
|
-
const res = await this.breaker.send(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
474
|
+
const res = await this.breaker.send(
|
|
475
|
+
() =>
|
|
476
|
+
withRetry(
|
|
477
|
+
async () => {
|
|
478
|
+
const extra = init.headers ?? {}
|
|
479
|
+
const to = preflight
|
|
480
|
+
? await this.preflight(this.url(pathname), method, extra, signal)
|
|
481
|
+
: { url: this.url(pathname), auth: true }
|
|
482
|
+
const s = signal()
|
|
483
|
+
return this.fetchImpl(to.url, {
|
|
484
|
+
method,
|
|
485
|
+
headers: this.headers(extra, to.auth),
|
|
486
|
+
...(init.body === undefined ? {} : { body: init.body }),
|
|
487
|
+
...(s === undefined ? {} : { signal: s }),
|
|
488
|
+
})
|
|
489
|
+
},
|
|
490
|
+
this.config.retries,
|
|
491
|
+
this.wait,
|
|
492
|
+
),
|
|
493
|
+
method === 'GET',
|
|
492
494
|
)
|
|
493
495
|
if (method === 'PUT' && res.status === 403) {
|
|
494
496
|
const first = !this.writesRefused
|
|
@@ -549,8 +551,11 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
549
551
|
headers: { Accept: 'application/octet-stream' },
|
|
550
552
|
})
|
|
551
553
|
if (res === undefined) return null
|
|
552
|
-
if (res.status
|
|
553
|
-
|
|
554
|
+
if (res.status !== 200) {
|
|
555
|
+
await res.body?.cancel()
|
|
556
|
+
if (res.status === 404) return null
|
|
557
|
+
throw new Error(`HTTP ${res.status}`)
|
|
558
|
+
}
|
|
554
559
|
const duration = Number(res.headers.get('x-artifact-duration'))
|
|
555
560
|
const durationMs = Number.isFinite(duration) && duration > 0 ? duration : undefined
|
|
556
561
|
if (this.key === undefined) return { body: res, durationMs }
|
|
@@ -574,7 +579,10 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
574
579
|
try {
|
|
575
580
|
const max = this.maxSignedBody
|
|
576
581
|
const past = () => new Error(`the signed artifact runs past ${max} bytes — treated as a miss`)
|
|
577
|
-
if (Number(res.headers.get('content-length') ?? 0) > max)
|
|
582
|
+
if (Number(res.headers.get('content-length') ?? 0) > max) {
|
|
583
|
+
await res.body?.cancel()
|
|
584
|
+
throw past()
|
|
585
|
+
}
|
|
578
586
|
let n = 0
|
|
579
587
|
const counted = res.body?.pipeThrough(
|
|
580
588
|
new TransformStream<Uint8Array, Uint8Array>({
|