@qoretechnologies/qlip 0.1.3-pr.31.g4b62f97 → 0.1.3-pr.32.g3ecd7a4
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 +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/test-runner/cli/upload.d.ts.map +1 -1
- package/dist/test-runner/cli/upload.js +17 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/upload/processing-request.d.ts +15 -0
- package/dist/upload/processing-request.d.ts.map +1 -0
- package/dist/upload/processing-request.js +89 -0
- package/dist/upload/upload.d.ts +24 -2
- package/dist/upload/upload.d.ts.map +1 -1
- package/dist/upload/upload.js +88 -10
- package/dist-cjs/test-runner/cli/upload.js +17 -0
- package/dist-cjs/upload/processing-request.js +96 -0
- package/dist-cjs/upload/upload.js +90 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -317,6 +317,10 @@ Several `vitest run` invocations (or `test-storybook` shards) can capture into o
|
|
|
317
317
|
|
|
318
318
|
Add `--fail-on-partial-build` to `qlip-upload` (or `upload.failOnPartialBuild` on the plugin) to turn "screenshots on disk that never reached the manifest" from a warning into a red build.
|
|
319
319
|
|
|
320
|
+
### Large builds: processing timeout
|
|
321
|
+
|
|
322
|
+
The server diffs the whole build before it answers the final upload request, which takes minutes for a large Storybook (about 8–12 minutes for ~2,200 snapshots). `qlip-upload` waits up to 30 minutes for that answer; raise it with `--processing-timeout <seconds>` (or `$QLIP_PROCESSING_TIMEOUT`, or `upload.processingTimeoutMs` on the plugin). A timeout or network failure is reported with the failing phase and its cause; add `--fail-on-upload-error` (or `upload.failOnUploadError`) to make it fail the CI step instead of logging it.
|
|
323
|
+
|
|
320
324
|
## Manifest
|
|
321
325
|
|
|
322
326
|
Each run writes `manifest.json` inside the build folder with tool metadata, defaults, stats, and per-screenshot entries (auto + manual).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { qlipVitestPlugin } from './plugin/vitestPlugin.js';
|
|
2
2
|
export { screenshot } from './runtime/screenshot.js';
|
|
3
|
-
export { uploadBuild, QlipUploadError, type QlipUploadInput, type QlipUploadResult, } from './upload/upload.js';
|
|
3
|
+
export { uploadBuild, QlipUploadError, QlipUploadNetworkError, DEFAULT_PROCESSING_TIMEOUT_MS, type QlipUploadInput, type QlipUploadResult, } from './upload/upload.js';
|
|
4
4
|
export { autodetectBranch, autodetectCommit, autodetectBaseBranch, autodetectAncestorCommits, } from './upload/autodetect.js';
|
|
5
5
|
export type { QlipConsoleLevel, QlipConsoleMessage, QlipManifest, QlipManifestEntry, QlipParameters, QlipPluginOptions, QlipResolvedDefaults, QlipScreenshotOptions, QlipUploadOptions, QlipViewport, } from './types.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EACL,WAAW,EACX,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,YAAY,GACb,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EACL,WAAW,EACX,eAAe,EACf,sBAAsB,EACtB,6BAA6B,EAC7B,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,YAAY,GACb,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { qlipVitestPlugin } from './plugin/vitestPlugin.js';
|
|
2
2
|
export { screenshot } from './runtime/screenshot.js';
|
|
3
|
-
export { uploadBuild, QlipUploadError, } from './upload/upload.js';
|
|
3
|
+
export { uploadBuild, QlipUploadError, QlipUploadNetworkError, DEFAULT_PROCESSING_TIMEOUT_MS, } from './upload/upload.js';
|
|
4
4
|
export { autodetectBranch, autodetectCommit, autodetectBaseBranch, autodetectAncestorCommits, } from './upload/autodetect.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/test-runner/cli/upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/test-runner/cli/upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA2IH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GACpB,MAAM,MAAM,EAAE,EACd,MAAK,MAAM,CAAC,UAAwB,EACpC,SAAQ;IAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAY,KAC7E,OAAO,CAAC,gBAAgB,CAqF1B,CAAC"}
|
|
@@ -52,6 +52,9 @@ Options:
|
|
|
52
52
|
--token <token> Bearer token. Default: \$QLIP_UPLOAD_TOKEN.
|
|
53
53
|
--branch <branch> Override branch detection.
|
|
54
54
|
--commit <sha> Override commit detection.
|
|
55
|
+
--processing-timeout <s> Seconds to wait for the server to process
|
|
56
|
+
the build. Default:
|
|
57
|
+
\$QLIP_PROCESSING_TIMEOUT or 1800.
|
|
55
58
|
--fail-on-upload-error Exit non-zero on upload failure (default:
|
|
56
59
|
log error + exit 0).
|
|
57
60
|
--fail-on-partial-build Exit non-zero when screenshots on disk are
|
|
@@ -95,6 +98,9 @@ const parseArgs = (argv) => {
|
|
|
95
98
|
case '--commit':
|
|
96
99
|
args.commit = next();
|
|
97
100
|
break;
|
|
101
|
+
case '--processing-timeout':
|
|
102
|
+
args.processingTimeout = next();
|
|
103
|
+
break;
|
|
98
104
|
case '--fail-on-upload-error':
|
|
99
105
|
args.failOnUploadError = true;
|
|
100
106
|
break;
|
|
@@ -172,6 +178,16 @@ export const runUpload = async (argv, env = process.env, logger = console) => {
|
|
|
172
178
|
logger.error(`qlip-upload: ${err.message}`);
|
|
173
179
|
return { exitCode: 1 };
|
|
174
180
|
}
|
|
181
|
+
const processingTimeout = parsed.processingTimeout ?? env['QLIP_PROCESSING_TIMEOUT'];
|
|
182
|
+
let processingTimeoutMs;
|
|
183
|
+
if (processingTimeout !== undefined && processingTimeout !== '') {
|
|
184
|
+
const seconds = Number(processingTimeout);
|
|
185
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
186
|
+
logger.error(`qlip-upload: --processing-timeout must be a positive number of seconds, got "${processingTimeout}"`);
|
|
187
|
+
return { exitCode: 1 };
|
|
188
|
+
}
|
|
189
|
+
processingTimeoutMs = seconds * 1000;
|
|
190
|
+
}
|
|
175
191
|
const project = parsed.project ?? env['QLIP_PROJECT'] ?? 'default';
|
|
176
192
|
const token = parsed.token ?? env['QLIP_UPLOAD_TOKEN'];
|
|
177
193
|
const upload = {
|
|
@@ -180,6 +196,7 @@ export const runUpload = async (argv, env = process.env, logger = console) => {
|
|
|
180
196
|
...(token !== undefined && token !== '' ? { uploadToken: token } : {}),
|
|
181
197
|
...(parsed.branch !== undefined ? { branch: parsed.branch } : {}),
|
|
182
198
|
...(parsed.commit !== undefined ? { commit: parsed.commit } : {}),
|
|
199
|
+
...(processingTimeoutMs !== undefined ? { processingTimeoutMs } : {}),
|
|
183
200
|
failOnUploadError: parsed.failOnUploadError,
|
|
184
201
|
failOnPartialBuild: parsed.failOnPartialBuild,
|
|
185
202
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -170,6 +170,12 @@ export interface QlipUploadOptions {
|
|
|
170
170
|
* Set to `true` in CI when you want red builds on upload errors.
|
|
171
171
|
*/
|
|
172
172
|
failOnUploadError?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* How long to wait for the server to answer the requests it answers only
|
|
175
|
+
* after processing the build (finalize, or the legacy upload), in ms.
|
|
176
|
+
* Default: 30 minutes. See `design/UPLOAD.md` "Long-running requests".
|
|
177
|
+
*/
|
|
178
|
+
processingTimeoutMs?: number;
|
|
173
179
|
/**
|
|
174
180
|
* Whether a partial build should fail the run. A build is partial
|
|
175
181
|
* when PNGs exist on disk that no manifest entry references — i.e.
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport for the upload requests qlip-server answers only once it
|
|
3
|
+
* has processed the whole build: `finalize` and the legacy multipart POST.
|
|
4
|
+
*
|
|
5
|
+
* Node's global `fetch` (undici) fails any request whose response headers
|
|
6
|
+
* take longer than 300 s (`headersTimeout`) with a bare "fetch failed", and
|
|
7
|
+
* the limit can't be raised without depending on the `undici` package. A
|
|
8
|
+
* large build's diff runs longer than that, so these requests go through
|
|
9
|
+
* `node:http`, which imposes no response deadline: the caller's `signal` is
|
|
10
|
+
* the only one. See design/UPLOAD.md "Long-running requests".
|
|
11
|
+
*/
|
|
12
|
+
/** The subset of `fetch` the upload client uses; `fetch` itself satisfies it. */
|
|
13
|
+
export type TFetchLike = (url: string, init: RequestInit) => Promise<Response>;
|
|
14
|
+
export declare const fetchAwaitingProcessing: TFetchLike;
|
|
15
|
+
//# sourceMappingURL=processing-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processing-request.d.ts","sourceRoot":"","sources":["../../src/upload/processing-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAqC/E,eAAO,MAAM,uBAAuB,EAAE,UAsDrC,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport for the upload requests qlip-server answers only once it
|
|
3
|
+
* has processed the whole build: `finalize` and the legacy multipart POST.
|
|
4
|
+
*
|
|
5
|
+
* Node's global `fetch` (undici) fails any request whose response headers
|
|
6
|
+
* take longer than 300 s (`headersTimeout`) with a bare "fetch failed", and
|
|
7
|
+
* the limit can't be raised without depending on the `undici` package. A
|
|
8
|
+
* large build's diff runs longer than that, so these requests go through
|
|
9
|
+
* `node:http`, which imposes no response deadline: the caller's `signal` is
|
|
10
|
+
* the only one. See design/UPLOAD.md "Long-running requests".
|
|
11
|
+
*/
|
|
12
|
+
import http from 'node:http';
|
|
13
|
+
import https from 'node:https';
|
|
14
|
+
const NULL_BODY_STATUSES = new Set([204, 205, 304]);
|
|
15
|
+
const encodeBody = async (body) => {
|
|
16
|
+
if (body === undefined || body === null) {
|
|
17
|
+
return { bytes: new Uint8Array(0), contentType: null };
|
|
18
|
+
}
|
|
19
|
+
// Response serializes every BodyInit (FormData gets its multipart boundary).
|
|
20
|
+
const encoded = new Response(body);
|
|
21
|
+
return {
|
|
22
|
+
bytes: new Uint8Array(await encoded.arrayBuffer()),
|
|
23
|
+
contentType: encoded.headers.get('content-type'),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const toResponse = (res, body) => {
|
|
27
|
+
const headers = new Headers();
|
|
28
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
32
|
+
headers.append(name, item);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const status = res.statusCode ?? 0;
|
|
36
|
+
return new Response(NULL_BODY_STATUSES.has(status) ? null : new Uint8Array(body), {
|
|
37
|
+
status,
|
|
38
|
+
statusText: res.statusMessage ?? '',
|
|
39
|
+
headers,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
export const fetchAwaitingProcessing = async (url, init) => {
|
|
43
|
+
const target = new URL(url);
|
|
44
|
+
const transport = target.protocol === 'https:'
|
|
45
|
+
? https
|
|
46
|
+
: target.protocol === 'http:'
|
|
47
|
+
? http
|
|
48
|
+
: undefined;
|
|
49
|
+
if (transport === undefined) {
|
|
50
|
+
throw new TypeError(`unsupported protocol: ${target.protocol}`);
|
|
51
|
+
}
|
|
52
|
+
const { bytes, contentType } = await encodeBody(init.body);
|
|
53
|
+
const headers = {};
|
|
54
|
+
if (contentType !== null)
|
|
55
|
+
headers['content-type'] = contentType;
|
|
56
|
+
new Headers(init.headers).forEach((value, name) => {
|
|
57
|
+
headers[name] = value;
|
|
58
|
+
});
|
|
59
|
+
headers['content-length'] = String(bytes.byteLength);
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const req = transport.request(target, {
|
|
62
|
+
method: init.method ?? 'GET',
|
|
63
|
+
headers,
|
|
64
|
+
...(init.signal ? { signal: init.signal } : {}),
|
|
65
|
+
}, (res) => {
|
|
66
|
+
const chunks = [];
|
|
67
|
+
res.on('data', (chunk) => {
|
|
68
|
+
chunks.push(chunk);
|
|
69
|
+
});
|
|
70
|
+
res.on('error', reject);
|
|
71
|
+
// A connection dropped mid-body ends the stream without `end`.
|
|
72
|
+
res.on('close', () => {
|
|
73
|
+
if (!res.complete) {
|
|
74
|
+
reject(new Error('connection closed before the response completed'));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
res.on('end', () => {
|
|
78
|
+
try {
|
|
79
|
+
resolve(toResponse(res, Buffer.concat(chunks)));
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
req.on('error', reject);
|
|
87
|
+
req.end(bytes);
|
|
88
|
+
});
|
|
89
|
+
};
|
package/dist/upload/upload.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Wire contract: `qlip-server/design/UPLOAD.md §0`.
|
|
4
4
|
*/
|
|
5
5
|
import type { QlipUploadOptions } from '../types.js';
|
|
6
|
+
import type { TFetchLike } from './processing-request.js';
|
|
6
7
|
export interface QlipUploadResult {
|
|
7
8
|
/** Build ID echoed by the server (matches the manifest's buildId). */
|
|
8
9
|
buildId: string;
|
|
@@ -20,13 +21,24 @@ export interface QlipUploadInput {
|
|
|
20
21
|
buildDir: string;
|
|
21
22
|
/** Resolved upload options (branch/commit already auto-detected). */
|
|
22
23
|
options: QlipUploadOptions;
|
|
23
|
-
/**
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Override for tests. Defaults to global fetch for create + blobs, and to
|
|
26
|
+
* a `node:http` transport for the finalize / legacy requests the server
|
|
27
|
+
* answers only after processing (see `processing-request.ts`). An
|
|
28
|
+
* override is used for every phase, so its own response timeouts apply.
|
|
29
|
+
*/
|
|
30
|
+
fetchImpl?: TFetchLike;
|
|
25
31
|
/** Base backoff (ms) between blob-PUT retries; test seam, attempt N waits N×. */
|
|
26
32
|
retryBaseDelayMs?: number;
|
|
27
33
|
}
|
|
28
34
|
/** Default server when `upload.serverUrl` is omitted: the hosted instance. */
|
|
29
35
|
export declare const DEFAULT_SERVER_URL = "https://qlip.qoretechnologies.com";
|
|
36
|
+
/**
|
|
37
|
+
* Default deadline for the finalize / legacy response. qorus-ide builds of
|
|
38
|
+
* ~2.1k snapshots measured 470–713 s of server processing (2026-10, see
|
|
39
|
+
* design/UPLOAD.md "Long-running requests"); 30 min leaves 2.5× headroom.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_PROCESSING_TIMEOUT_MS: number;
|
|
30
42
|
export type TUploadPhase = 'create' | 'blob' | 'finalize' | 'legacy';
|
|
31
43
|
/**
|
|
32
44
|
* Resolve the effective server base URL (default applied, trailing slash
|
|
@@ -39,5 +51,15 @@ export declare class QlipUploadError extends Error {
|
|
|
39
51
|
readonly phase: TUploadPhase;
|
|
40
52
|
constructor(status: number, statusText: string, body: string, phase?: TUploadPhase);
|
|
41
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The request never produced an HTTP response: a network error, or no
|
|
56
|
+
* response before the client's deadline. `status`-less, unlike
|
|
57
|
+
* `QlipUploadError`.
|
|
58
|
+
*/
|
|
59
|
+
export declare class QlipUploadNetworkError extends Error {
|
|
60
|
+
readonly phase: TUploadPhase;
|
|
61
|
+
readonly cause: unknown;
|
|
62
|
+
constructor(phase: TUploadPhase, cause: unknown, detail?: string);
|
|
63
|
+
}
|
|
42
64
|
export declare const uploadBuild: (input: QlipUploadInput) => Promise<QlipUploadResult>;
|
|
43
65
|
//# sourceMappingURL=upload.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/upload/upload.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGnE,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,QAAQ,EAAE,IAAI,GAAG,QAAQ,CAAC;IAC1B,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,OAAO,EAAE,iBAAiB,CAAC;IAC3B
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/upload/upload.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,QAAQ,EAAE,IAAI,GAAG,QAAQ,CAAC;IAC1B,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,OAAO,EAAE,iBAAiB,CAAC;IAC3B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,sCAAsC,CAAC;AAMtE;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,QAAc,CAAC;AAEzD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,KAC5C,MAAsE,CAAC;AAE1E,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;gBAG3B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,YAAuB;CAUjC;AA4BD;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;gBAEZ,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM;CAQjE;AAiND,eAAO,MAAM,WAAW,GACtB,OAAO,eAAe,KACrB,OAAO,CAAC,gBAAgB,CAgH1B,CAAC"}
|
package/dist/upload/upload.js
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { hashFile } from './hash.js';
|
|
8
|
+
import { fetchAwaitingProcessing } from './processing-request.js';
|
|
8
9
|
/** Default server when `upload.serverUrl` is omitted: the hosted instance. */
|
|
9
10
|
export const DEFAULT_SERVER_URL = 'https://qlip.qoretechnologies.com';
|
|
10
11
|
const TRANSFER_CONCURRENCY = 10;
|
|
11
12
|
const BLOB_RETRIES = 2;
|
|
12
13
|
const BLOB_TIMEOUT_MS = 30000;
|
|
13
14
|
const DEFAULT_RETRY_BASE_DELAY_MS = 500;
|
|
15
|
+
/**
|
|
16
|
+
* Default deadline for the finalize / legacy response. qorus-ide builds of
|
|
17
|
+
* ~2.1k snapshots measured 470–713 s of server processing (2026-10, see
|
|
18
|
+
* design/UPLOAD.md "Long-running requests"); 30 min leaves 2.5× headroom.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_PROCESSING_TIMEOUT_MS = 30 * 60000;
|
|
14
21
|
/**
|
|
15
22
|
* Resolve the effective server base URL (default applied, trailing slash
|
|
16
23
|
* stripped). Exported so log lines report the URL the upload actually hits.
|
|
@@ -25,6 +32,76 @@ export class QlipUploadError extends Error {
|
|
|
25
32
|
this.phase = phase;
|
|
26
33
|
}
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* `fetch failed` alone hides why: the reason (a timeout, a reset, a DNS
|
|
37
|
+
* failure) lives on the `cause` chain, so the message spells that out.
|
|
38
|
+
*/
|
|
39
|
+
const describeCause = (err) => {
|
|
40
|
+
const parts = [];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
let current = err;
|
|
43
|
+
while (current !== undefined && current !== null && !seen.has(current)) {
|
|
44
|
+
seen.add(current);
|
|
45
|
+
if (current instanceof Error) {
|
|
46
|
+
const code = current.code;
|
|
47
|
+
parts.push(typeof code === 'string' && code !== ''
|
|
48
|
+
? `${current.message} (${code})`
|
|
49
|
+
: current.message);
|
|
50
|
+
current = current.cause;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
parts.push(String(current));
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return parts.join(': ');
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The request never produced an HTTP response: a network error, or no
|
|
61
|
+
* response before the client's deadline. `status`-less, unlike
|
|
62
|
+
* `QlipUploadError`.
|
|
63
|
+
*/
|
|
64
|
+
export class QlipUploadNetworkError extends Error {
|
|
65
|
+
constructor(phase, cause, detail) {
|
|
66
|
+
super(`qlip upload failed [${phase}]: ${detail ?? `network error — ${describeCause(cause)}`}`);
|
|
67
|
+
this.name = 'QlipUploadNetworkError';
|
|
68
|
+
this.phase = phase;
|
|
69
|
+
this.cause = cause;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** `fetchImpl`, with a transport failure reported as a phase-tagged error. */
|
|
73
|
+
const withPhase = (phase, fetchImpl) => async (url, init) => {
|
|
74
|
+
try {
|
|
75
|
+
return await fetchImpl(url, init);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw new QlipUploadNetworkError(phase, err);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Send a request the server answers only after processing the build. No
|
|
83
|
+
* retry: the server keeps processing after a dropped connection, and a
|
|
84
|
+
* second finalize would race the first.
|
|
85
|
+
*/
|
|
86
|
+
const sendAwaitingProcessing = async (phase, fetchImpl, url, init, timeoutMs) => {
|
|
87
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
return await fetchImpl(url, { ...init, signal });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (signal.aborted) {
|
|
93
|
+
throw new QlipUploadNetworkError(phase, err, `no response within ${String(timeoutMs / 1000)} s; the server may still be processing the build — check it in the dashboard, and raise the processing timeout if builds this size take longer`);
|
|
94
|
+
}
|
|
95
|
+
throw new QlipUploadNetworkError(phase, err);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const resolveProcessingTimeoutMs = (options) => {
|
|
99
|
+
const value = options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
100
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
101
|
+
throw new RangeError(`upload.processingTimeoutMs must be a positive number of milliseconds, got ${String(value)}`);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
};
|
|
28
105
|
/** Contextual hint appended to 413s, tailored to the phase that hit it. */
|
|
29
106
|
const hintFor = (status, phase) => {
|
|
30
107
|
if (status !== 413)
|
|
@@ -97,10 +174,12 @@ const putBlob = async (fetchImpl, url, bytes, baseHeaders, retryBaseDelayMs) =>
|
|
|
97
174
|
if (attempt < BLOB_RETRIES)
|
|
98
175
|
await delay(retryBaseDelayMs * (attempt + 1));
|
|
99
176
|
}
|
|
100
|
-
|
|
177
|
+
if (lastError instanceof QlipUploadError)
|
|
178
|
+
throw lastError;
|
|
179
|
+
throw new QlipUploadNetworkError('blob', lastError);
|
|
101
180
|
};
|
|
102
181
|
/** Pre-v2 single-multipart upload; fallback when the server lacks v2 routes. */
|
|
103
|
-
const uploadBuildLegacy = async (fetchImpl, baseUrl, buildDir, options, manifest) => {
|
|
182
|
+
const uploadBuildLegacy = async (fetchImpl, baseUrl, buildDir, options, manifest, processingTimeoutMs) => {
|
|
104
183
|
const form = new FormData();
|
|
105
184
|
form.append('manifest', new Blob([JSON.stringify(manifest)], { type: 'application/json' }), 'manifest.json');
|
|
106
185
|
const captured = manifest.entries.filter((e) => e.status === 'captured');
|
|
@@ -131,18 +210,16 @@ const uploadBuildLegacy = async (fetchImpl, baseUrl, buildDir, options, manifest
|
|
|
131
210
|
if (options.triggeredById !== undefined) {
|
|
132
211
|
form.append('triggeredById', String(options.triggeredById));
|
|
133
212
|
}
|
|
134
|
-
const response = await fetchImpl
|
|
135
|
-
method: 'POST',
|
|
136
|
-
body: form,
|
|
137
|
-
headers: authHeaders(options),
|
|
138
|
-
});
|
|
213
|
+
const response = await sendAwaitingProcessing('legacy', fetchImpl, `${baseUrl}/api/builds/upload`, { method: 'POST', body: form, headers: authHeaders(options) }, processingTimeoutMs);
|
|
139
214
|
if (!response.ok)
|
|
140
215
|
throw await errorFromResponse(response, 'legacy');
|
|
141
216
|
return { buildId: manifest.buildId, status: response.status, protocol: 'legacy' };
|
|
142
217
|
};
|
|
143
218
|
export const uploadBuild = async (input) => {
|
|
144
219
|
const { buildDir, options } = input;
|
|
220
|
+
const processingTimeoutMs = resolveProcessingTimeoutMs(options);
|
|
145
221
|
const fetchImpl = input.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
222
|
+
const processingFetch = input.fetchImpl ?? fetchAwaitingProcessing;
|
|
146
223
|
const retryBaseDelayMs = input.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
|
|
147
224
|
const baseUrl = resolveServerUrl(options);
|
|
148
225
|
const headers = authHeaders(options);
|
|
@@ -154,7 +231,8 @@ export const uploadBuild = async (input) => {
|
|
|
154
231
|
entry.sha256 = sha256;
|
|
155
232
|
entry.sizeBytes = sizeBytes;
|
|
156
233
|
});
|
|
157
|
-
const
|
|
234
|
+
const createFetch = withPhase('create', fetchImpl);
|
|
235
|
+
const createResponse = await createFetch(`${baseUrl}/api/builds`, {
|
|
158
236
|
method: 'POST',
|
|
159
237
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
160
238
|
body: JSON.stringify({
|
|
@@ -181,7 +259,7 @@ export const uploadBuild = async (input) => {
|
|
|
181
259
|
});
|
|
182
260
|
// 404 ⇒ server predates the v2 routes; fall back to legacy multipart.
|
|
183
261
|
if (createResponse.status === 404) {
|
|
184
|
-
return uploadBuildLegacy(
|
|
262
|
+
return uploadBuildLegacy(processingFetch, baseUrl, buildDir, options, manifest, processingTimeoutMs);
|
|
185
263
|
}
|
|
186
264
|
if (!createResponse.ok)
|
|
187
265
|
throw await errorFromResponse(createResponse, 'create');
|
|
@@ -201,7 +279,7 @@ export const uploadBuild = async (input) => {
|
|
|
201
279
|
const bytes = await readFile(path.join(buildDir, relPath));
|
|
202
280
|
await putBlob(fetchImpl, `${baseUrl}/api/builds/${buildPath}/blobs/${key}`, bytes, headers, retryBaseDelayMs);
|
|
203
281
|
});
|
|
204
|
-
const finalizeResponse = await
|
|
282
|
+
const finalizeResponse = await sendAwaitingProcessing('finalize', processingFetch, `${baseUrl}/api/builds/${buildPath}/finalize`, { method: 'POST', headers }, processingTimeoutMs);
|
|
205
283
|
if (!finalizeResponse.ok) {
|
|
206
284
|
throw await errorFromResponse(finalizeResponse, 'finalize');
|
|
207
285
|
}
|
|
@@ -58,6 +58,9 @@ Options:
|
|
|
58
58
|
--token <token> Bearer token. Default: \$QLIP_UPLOAD_TOKEN.
|
|
59
59
|
--branch <branch> Override branch detection.
|
|
60
60
|
--commit <sha> Override commit detection.
|
|
61
|
+
--processing-timeout <s> Seconds to wait for the server to process
|
|
62
|
+
the build. Default:
|
|
63
|
+
\$QLIP_PROCESSING_TIMEOUT or 1800.
|
|
61
64
|
--fail-on-upload-error Exit non-zero on upload failure (default:
|
|
62
65
|
log error + exit 0).
|
|
63
66
|
--fail-on-partial-build Exit non-zero when screenshots on disk are
|
|
@@ -101,6 +104,9 @@ const parseArgs = (argv) => {
|
|
|
101
104
|
case '--commit':
|
|
102
105
|
args.commit = next();
|
|
103
106
|
break;
|
|
107
|
+
case '--processing-timeout':
|
|
108
|
+
args.processingTimeout = next();
|
|
109
|
+
break;
|
|
104
110
|
case '--fail-on-upload-error':
|
|
105
111
|
args.failOnUploadError = true;
|
|
106
112
|
break;
|
|
@@ -178,6 +184,16 @@ const runUpload = async (argv, env = process.env, logger = console) => {
|
|
|
178
184
|
logger.error(`qlip-upload: ${err.message}`);
|
|
179
185
|
return { exitCode: 1 };
|
|
180
186
|
}
|
|
187
|
+
const processingTimeout = parsed.processingTimeout ?? env['QLIP_PROCESSING_TIMEOUT'];
|
|
188
|
+
let processingTimeoutMs;
|
|
189
|
+
if (processingTimeout !== undefined && processingTimeout !== '') {
|
|
190
|
+
const seconds = Number(processingTimeout);
|
|
191
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
192
|
+
logger.error(`qlip-upload: --processing-timeout must be a positive number of seconds, got "${processingTimeout}"`);
|
|
193
|
+
return { exitCode: 1 };
|
|
194
|
+
}
|
|
195
|
+
processingTimeoutMs = seconds * 1000;
|
|
196
|
+
}
|
|
181
197
|
const project = parsed.project ?? env['QLIP_PROJECT'] ?? 'default';
|
|
182
198
|
const token = parsed.token ?? env['QLIP_UPLOAD_TOKEN'];
|
|
183
199
|
const upload = {
|
|
@@ -186,6 +202,7 @@ const runUpload = async (argv, env = process.env, logger = console) => {
|
|
|
186
202
|
...(token !== undefined && token !== '' ? { uploadToken: token } : {}),
|
|
187
203
|
...(parsed.branch !== undefined ? { branch: parsed.branch } : {}),
|
|
188
204
|
...(parsed.commit !== undefined ? { commit: parsed.commit } : {}),
|
|
205
|
+
...(processingTimeoutMs !== undefined ? { processingTimeoutMs } : {}),
|
|
189
206
|
failOnUploadError: parsed.failOnUploadError,
|
|
190
207
|
failOnPartialBuild: parsed.failOnPartialBuild,
|
|
191
208
|
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP transport for the upload requests qlip-server answers only once it
|
|
4
|
+
* has processed the whole build: `finalize` and the legacy multipart POST.
|
|
5
|
+
*
|
|
6
|
+
* Node's global `fetch` (undici) fails any request whose response headers
|
|
7
|
+
* take longer than 300 s (`headersTimeout`) with a bare "fetch failed", and
|
|
8
|
+
* the limit can't be raised without depending on the `undici` package. A
|
|
9
|
+
* large build's diff runs longer than that, so these requests go through
|
|
10
|
+
* `node:http`, which imposes no response deadline: the caller's `signal` is
|
|
11
|
+
* the only one. See design/UPLOAD.md "Long-running requests".
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.fetchAwaitingProcessing = void 0;
|
|
18
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
19
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
20
|
+
const NULL_BODY_STATUSES = new Set([204, 205, 304]);
|
|
21
|
+
const encodeBody = async (body) => {
|
|
22
|
+
if (body === undefined || body === null) {
|
|
23
|
+
return { bytes: new Uint8Array(0), contentType: null };
|
|
24
|
+
}
|
|
25
|
+
// Response serializes every BodyInit (FormData gets its multipart boundary).
|
|
26
|
+
const encoded = new Response(body);
|
|
27
|
+
return {
|
|
28
|
+
bytes: new Uint8Array(await encoded.arrayBuffer()),
|
|
29
|
+
contentType: encoded.headers.get('content-type'),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const toResponse = (res, body) => {
|
|
33
|
+
const headers = new Headers();
|
|
34
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
38
|
+
headers.append(name, item);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const status = res.statusCode ?? 0;
|
|
42
|
+
return new Response(NULL_BODY_STATUSES.has(status) ? null : new Uint8Array(body), {
|
|
43
|
+
status,
|
|
44
|
+
statusText: res.statusMessage ?? '',
|
|
45
|
+
headers,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const fetchAwaitingProcessing = async (url, init) => {
|
|
49
|
+
const target = new URL(url);
|
|
50
|
+
const transport = target.protocol === 'https:'
|
|
51
|
+
? node_https_1.default
|
|
52
|
+
: target.protocol === 'http:'
|
|
53
|
+
? node_http_1.default
|
|
54
|
+
: undefined;
|
|
55
|
+
if (transport === undefined) {
|
|
56
|
+
throw new TypeError(`unsupported protocol: ${target.protocol}`);
|
|
57
|
+
}
|
|
58
|
+
const { bytes, contentType } = await encodeBody(init.body);
|
|
59
|
+
const headers = {};
|
|
60
|
+
if (contentType !== null)
|
|
61
|
+
headers['content-type'] = contentType;
|
|
62
|
+
new Headers(init.headers).forEach((value, name) => {
|
|
63
|
+
headers[name] = value;
|
|
64
|
+
});
|
|
65
|
+
headers['content-length'] = String(bytes.byteLength);
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const req = transport.request(target, {
|
|
68
|
+
method: init.method ?? 'GET',
|
|
69
|
+
headers,
|
|
70
|
+
...(init.signal ? { signal: init.signal } : {}),
|
|
71
|
+
}, (res) => {
|
|
72
|
+
const chunks = [];
|
|
73
|
+
res.on('data', (chunk) => {
|
|
74
|
+
chunks.push(chunk);
|
|
75
|
+
});
|
|
76
|
+
res.on('error', reject);
|
|
77
|
+
// A connection dropped mid-body ends the stream without `end`.
|
|
78
|
+
res.on('close', () => {
|
|
79
|
+
if (!res.complete) {
|
|
80
|
+
reject(new Error('connection closed before the response completed'));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
res.on('end', () => {
|
|
84
|
+
try {
|
|
85
|
+
resolve(toResponse(res, Buffer.concat(chunks)));
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
reject(err);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
req.on('error', reject);
|
|
93
|
+
req.end(bytes);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
exports.fetchAwaitingProcessing = fetchAwaitingProcessing;
|
|
@@ -7,16 +7,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
8
|
};
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.uploadBuild = exports.QlipUploadError = exports.resolveServerUrl = exports.DEFAULT_SERVER_URL = void 0;
|
|
10
|
+
exports.uploadBuild = exports.QlipUploadNetworkError = exports.QlipUploadError = exports.resolveServerUrl = exports.DEFAULT_PROCESSING_TIMEOUT_MS = exports.DEFAULT_SERVER_URL = void 0;
|
|
11
11
|
const promises_1 = require("node:fs/promises");
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
13
|
const hash_js_1 = require("./hash.js");
|
|
14
|
+
const processing_request_js_1 = require("./processing-request.js");
|
|
14
15
|
/** Default server when `upload.serverUrl` is omitted: the hosted instance. */
|
|
15
16
|
exports.DEFAULT_SERVER_URL = 'https://qlip.qoretechnologies.com';
|
|
16
17
|
const TRANSFER_CONCURRENCY = 10;
|
|
17
18
|
const BLOB_RETRIES = 2;
|
|
18
19
|
const BLOB_TIMEOUT_MS = 30000;
|
|
19
20
|
const DEFAULT_RETRY_BASE_DELAY_MS = 500;
|
|
21
|
+
/**
|
|
22
|
+
* Default deadline for the finalize / legacy response. qorus-ide builds of
|
|
23
|
+
* ~2.1k snapshots measured 470–713 s of server processing (2026-10, see
|
|
24
|
+
* design/UPLOAD.md "Long-running requests"); 30 min leaves 2.5× headroom.
|
|
25
|
+
*/
|
|
26
|
+
exports.DEFAULT_PROCESSING_TIMEOUT_MS = 30 * 60000;
|
|
20
27
|
/**
|
|
21
28
|
* Resolve the effective server base URL (default applied, trailing slash
|
|
22
29
|
* stripped). Exported so log lines report the URL the upload actually hits.
|
|
@@ -33,6 +40,77 @@ class QlipUploadError extends Error {
|
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
exports.QlipUploadError = QlipUploadError;
|
|
43
|
+
/**
|
|
44
|
+
* `fetch failed` alone hides why: the reason (a timeout, a reset, a DNS
|
|
45
|
+
* failure) lives on the `cause` chain, so the message spells that out.
|
|
46
|
+
*/
|
|
47
|
+
const describeCause = (err) => {
|
|
48
|
+
const parts = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
let current = err;
|
|
51
|
+
while (current !== undefined && current !== null && !seen.has(current)) {
|
|
52
|
+
seen.add(current);
|
|
53
|
+
if (current instanceof Error) {
|
|
54
|
+
const code = current.code;
|
|
55
|
+
parts.push(typeof code === 'string' && code !== ''
|
|
56
|
+
? `${current.message} (${code})`
|
|
57
|
+
: current.message);
|
|
58
|
+
current = current.cause;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
parts.push(String(current));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts.join(': ');
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The request never produced an HTTP response: a network error, or no
|
|
69
|
+
* response before the client's deadline. `status`-less, unlike
|
|
70
|
+
* `QlipUploadError`.
|
|
71
|
+
*/
|
|
72
|
+
class QlipUploadNetworkError extends Error {
|
|
73
|
+
constructor(phase, cause, detail) {
|
|
74
|
+
super(`qlip upload failed [${phase}]: ${detail ?? `network error — ${describeCause(cause)}`}`);
|
|
75
|
+
this.name = 'QlipUploadNetworkError';
|
|
76
|
+
this.phase = phase;
|
|
77
|
+
this.cause = cause;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.QlipUploadNetworkError = QlipUploadNetworkError;
|
|
81
|
+
/** `fetchImpl`, with a transport failure reported as a phase-tagged error. */
|
|
82
|
+
const withPhase = (phase, fetchImpl) => async (url, init) => {
|
|
83
|
+
try {
|
|
84
|
+
return await fetchImpl(url, init);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw new QlipUploadNetworkError(phase, err);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Send a request the server answers only after processing the build. No
|
|
92
|
+
* retry: the server keeps processing after a dropped connection, and a
|
|
93
|
+
* second finalize would race the first.
|
|
94
|
+
*/
|
|
95
|
+
const sendAwaitingProcessing = async (phase, fetchImpl, url, init, timeoutMs) => {
|
|
96
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
97
|
+
try {
|
|
98
|
+
return await fetchImpl(url, { ...init, signal });
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (signal.aborted) {
|
|
102
|
+
throw new QlipUploadNetworkError(phase, err, `no response within ${String(timeoutMs / 1000)} s; the server may still be processing the build — check it in the dashboard, and raise the processing timeout if builds this size take longer`);
|
|
103
|
+
}
|
|
104
|
+
throw new QlipUploadNetworkError(phase, err);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const resolveProcessingTimeoutMs = (options) => {
|
|
108
|
+
const value = options.processingTimeoutMs ?? exports.DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
109
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
110
|
+
throw new RangeError(`upload.processingTimeoutMs must be a positive number of milliseconds, got ${String(value)}`);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
};
|
|
36
114
|
/** Contextual hint appended to 413s, tailored to the phase that hit it. */
|
|
37
115
|
const hintFor = (status, phase) => {
|
|
38
116
|
if (status !== 413)
|
|
@@ -105,10 +183,12 @@ const putBlob = async (fetchImpl, url, bytes, baseHeaders, retryBaseDelayMs) =>
|
|
|
105
183
|
if (attempt < BLOB_RETRIES)
|
|
106
184
|
await delay(retryBaseDelayMs * (attempt + 1));
|
|
107
185
|
}
|
|
108
|
-
|
|
186
|
+
if (lastError instanceof QlipUploadError)
|
|
187
|
+
throw lastError;
|
|
188
|
+
throw new QlipUploadNetworkError('blob', lastError);
|
|
109
189
|
};
|
|
110
190
|
/** Pre-v2 single-multipart upload; fallback when the server lacks v2 routes. */
|
|
111
|
-
const uploadBuildLegacy = async (fetchImpl, baseUrl, buildDir, options, manifest) => {
|
|
191
|
+
const uploadBuildLegacy = async (fetchImpl, baseUrl, buildDir, options, manifest, processingTimeoutMs) => {
|
|
112
192
|
const form = new FormData();
|
|
113
193
|
form.append('manifest', new Blob([JSON.stringify(manifest)], { type: 'application/json' }), 'manifest.json');
|
|
114
194
|
const captured = manifest.entries.filter((e) => e.status === 'captured');
|
|
@@ -139,18 +219,16 @@ const uploadBuildLegacy = async (fetchImpl, baseUrl, buildDir, options, manifest
|
|
|
139
219
|
if (options.triggeredById !== undefined) {
|
|
140
220
|
form.append('triggeredById', String(options.triggeredById));
|
|
141
221
|
}
|
|
142
|
-
const response = await fetchImpl
|
|
143
|
-
method: 'POST',
|
|
144
|
-
body: form,
|
|
145
|
-
headers: authHeaders(options),
|
|
146
|
-
});
|
|
222
|
+
const response = await sendAwaitingProcessing('legacy', fetchImpl, `${baseUrl}/api/builds/upload`, { method: 'POST', body: form, headers: authHeaders(options) }, processingTimeoutMs);
|
|
147
223
|
if (!response.ok)
|
|
148
224
|
throw await errorFromResponse(response, 'legacy');
|
|
149
225
|
return { buildId: manifest.buildId, status: response.status, protocol: 'legacy' };
|
|
150
226
|
};
|
|
151
227
|
const uploadBuild = async (input) => {
|
|
152
228
|
const { buildDir, options } = input;
|
|
229
|
+
const processingTimeoutMs = resolveProcessingTimeoutMs(options);
|
|
153
230
|
const fetchImpl = input.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
231
|
+
const processingFetch = input.fetchImpl ?? processing_request_js_1.fetchAwaitingProcessing;
|
|
154
232
|
const retryBaseDelayMs = input.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
|
|
155
233
|
const baseUrl = (0, exports.resolveServerUrl)(options);
|
|
156
234
|
const headers = authHeaders(options);
|
|
@@ -162,7 +240,8 @@ const uploadBuild = async (input) => {
|
|
|
162
240
|
entry.sha256 = sha256;
|
|
163
241
|
entry.sizeBytes = sizeBytes;
|
|
164
242
|
});
|
|
165
|
-
const
|
|
243
|
+
const createFetch = withPhase('create', fetchImpl);
|
|
244
|
+
const createResponse = await createFetch(`${baseUrl}/api/builds`, {
|
|
166
245
|
method: 'POST',
|
|
167
246
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
168
247
|
body: JSON.stringify({
|
|
@@ -189,7 +268,7 @@ const uploadBuild = async (input) => {
|
|
|
189
268
|
});
|
|
190
269
|
// 404 ⇒ server predates the v2 routes; fall back to legacy multipart.
|
|
191
270
|
if (createResponse.status === 404) {
|
|
192
|
-
return uploadBuildLegacy(
|
|
271
|
+
return uploadBuildLegacy(processingFetch, baseUrl, buildDir, options, manifest, processingTimeoutMs);
|
|
193
272
|
}
|
|
194
273
|
if (!createResponse.ok)
|
|
195
274
|
throw await errorFromResponse(createResponse, 'create');
|
|
@@ -209,7 +288,7 @@ const uploadBuild = async (input) => {
|
|
|
209
288
|
const bytes = await (0, promises_1.readFile)(node_path_1.default.join(buildDir, relPath));
|
|
210
289
|
await putBlob(fetchImpl, `${baseUrl}/api/builds/${buildPath}/blobs/${key}`, bytes, headers, retryBaseDelayMs);
|
|
211
290
|
});
|
|
212
|
-
const finalizeResponse = await
|
|
291
|
+
const finalizeResponse = await sendAwaitingProcessing('finalize', processingFetch, `${baseUrl}/api/builds/${buildPath}/finalize`, { method: 'POST', headers }, processingTimeoutMs);
|
|
213
292
|
if (!finalizeResponse.ok) {
|
|
214
293
|
throw await errorFromResponse(finalizeResponse, 'finalize');
|
|
215
294
|
}
|