@useblacksmith/vitest 0.2.0 → 0.3.0
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 +17 -0
- package/dist/git.js +10 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -3
- package/package.json +2 -2
- package/dist/payload.d.ts +0 -56
- package/dist/payload.js +0 -40
- package/dist/reporter.d.ts +0 -29
- package/dist/reporter.js +0 -149
- package/dist/upload.d.ts +0 -17
- package/dist/upload.js +0 -47
package/README.md
CHANGED
|
@@ -24,6 +24,23 @@ With the plugin, vitest only runs tests impacted by changes since the last succe
|
|
|
24
24
|
|
|
25
25
|
Requires Vitest 5.1 or newer.
|
|
26
26
|
|
|
27
|
+
### Vite+
|
|
28
|
+
|
|
29
|
+
Vite+ runs Vitest under `vp test`, so the plugin is loaded from the shared `vite.config.ts`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// vite.config.ts
|
|
33
|
+
import 'vite-plus/test/config'
|
|
34
|
+
import { defineConfig } from 'vite-plus'
|
|
35
|
+
import blacksmith from '@useblacksmith/vitest'
|
|
36
|
+
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
plugins: [blacksmith()],
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`vp test run` then runs the affected selection, including with `--shard`, test file filters, and `--config <path> --dir .` from a workspace package. Requires `vite-plus` 1.5.0 or newer, the first release that ships Vitest 5.1.
|
|
43
|
+
|
|
27
44
|
## Running locally
|
|
28
45
|
|
|
29
46
|
The same selection applies locally: a run reruns what changed on disk since the last successful run, committed or not. To run every test locally and select only in CI:
|
package/dist/git.js
CHANGED
|
@@ -116,6 +116,15 @@ function under(sorted, dir) {
|
|
|
116
116
|
return listing;
|
|
117
117
|
}
|
|
118
118
|
const DIRECTORY_MODE = '040000';
|
|
119
|
+
/** A path nothing could read, e.g. a component over NAME_MAX or a NUL byte, is as absent as a missing one. */
|
|
120
|
+
function lstat(file) {
|
|
121
|
+
try {
|
|
122
|
+
return lstatSync(file, { throwIfNoEntry: false });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
119
128
|
/**
|
|
120
129
|
* What each path looks like now: a file is `<mode> <blob>` as `git ls-tree`
|
|
121
130
|
* prints it, a symlink hashes its target, a directory hashes its listing (every
|
|
@@ -139,7 +148,7 @@ export async function anchorTree(repo, paths) {
|
|
|
139
148
|
return tree;
|
|
140
149
|
const present = new Map();
|
|
141
150
|
for (const file of wanted) {
|
|
142
|
-
const stat =
|
|
151
|
+
const stat = lstat(path.join(repo, file));
|
|
143
152
|
if (stat !== undefined)
|
|
144
153
|
present.set(file, stat);
|
|
145
154
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { Plugin } from 'vitest/config';
|
|
2
|
-
export { BlacksmithReporter } from './reporter.js';
|
|
3
|
-
export { HashShardSequencer, shardFor } from './sequencer.js';
|
|
4
|
-
export { ShadowAnalysis, type ShadowResult, SNAPSHOT_FILE, type Snapshot, } from './affected.js';
|
|
5
|
-
export type { Payload, RunError, TestResultRecord, TestStatus, } from './payload.js';
|
|
6
2
|
export interface BlacksmithOptions {
|
|
7
3
|
/**
|
|
8
4
|
* Run every test instead of only those affected since the last green run.
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
import { compileDependencies, configuredProvider, isTreeSnapshotProvider, isSnapshotRecorder, restoreDependencyCache, ShadowAnalysis, SnapshotRecorder, TreeSnapshotProvider, } from './affected.js';
|
|
3
3
|
import { customSequencer, HashShardSequencer, stableShards, } from './sequencer.js';
|
|
4
|
-
export { BlacksmithReporter } from './reporter.js';
|
|
5
|
-
export { HashShardSequencer, shardFor } from './sequencer.js';
|
|
6
|
-
export { ShadowAnalysis, SNAPSHOT_FILE, } from './affected.js';
|
|
7
4
|
/** Beside this module; `.ts` when the plugin runs from source (its own test fixtures). */
|
|
8
5
|
const recordReads = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './record-reads.ts' : './record-reads.js', import.meta.url));
|
|
9
6
|
/** Whether this Vitest wired up the pieces `--changed` selection relies on. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@useblacksmith/vitest",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Runs only the Vitest tests affected since the last successful run, with Blacksmith-backed caching",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
package/dist/payload.d.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
export type TestStatus = 'passed' | 'failed' | 'skipped';
|
|
2
|
-
export interface TestError {
|
|
3
|
-
message: string;
|
|
4
|
-
stack: string;
|
|
5
|
-
}
|
|
6
|
-
export interface TestResultRecord {
|
|
7
|
-
/** Test file, relative to the repository when the run is inside GITHUB_WORKSPACE */
|
|
8
|
-
file: string;
|
|
9
|
-
/** Vitest project the test ran in; null for the root project */
|
|
10
|
-
project: string | null;
|
|
11
|
-
/** Enclosing describe blocks, outermost first */
|
|
12
|
-
suite: string[];
|
|
13
|
-
name: string;
|
|
14
|
-
status: TestStatus;
|
|
15
|
-
/** null for tests that never ran (skipped, todo) */
|
|
16
|
-
duration_ms: number | null;
|
|
17
|
-
started_at: string | null;
|
|
18
|
-
retries: number;
|
|
19
|
-
flaky: boolean;
|
|
20
|
-
error?: TestError;
|
|
21
|
-
}
|
|
22
|
-
/** A failure outside any test: a module that threw while being imported or collected, or an unhandled error */
|
|
23
|
-
export interface RunError {
|
|
24
|
-
/** null when the error is not attributable to a test file */
|
|
25
|
-
file: string | null;
|
|
26
|
-
message: string;
|
|
27
|
-
stack: string;
|
|
28
|
-
}
|
|
29
|
-
export interface Payload {
|
|
30
|
-
framework: 'vitest';
|
|
31
|
-
framework_version: string;
|
|
32
|
-
reporter_version: string;
|
|
33
|
-
run: {
|
|
34
|
-
started_at: string;
|
|
35
|
-
finished_at: string;
|
|
36
|
-
reason: 'passed' | 'failed' | 'interrupted';
|
|
37
|
-
};
|
|
38
|
-
results: TestResultRecord[];
|
|
39
|
-
errors: RunError[];
|
|
40
|
-
/** true when results or errors beyond the count or byte caps were dropped */
|
|
41
|
-
truncated: boolean;
|
|
42
|
-
}
|
|
43
|
-
export declare const MAX_RESULTS = 50000;
|
|
44
|
-
export declare const MAX_PAYLOAD_BYTES: number;
|
|
45
|
-
export declare const MAX_ERROR_MESSAGE_CHARS = 4096;
|
|
46
|
-
export declare const MAX_ERROR_STACK_CHARS = 16384;
|
|
47
|
-
export declare function truncate(text: string, max: number): string;
|
|
48
|
-
/**
|
|
49
|
-
* Keeps results in order until either cap is hit, then errors in whatever
|
|
50
|
-
* budget is left, so a payload never serializes past MAX_PAYLOAD_BYTES of
|
|
51
|
-
* JSON (the gzip input, which is also the backend's memory ceiling).
|
|
52
|
-
*/
|
|
53
|
-
export declare function buildPayload(results: TestResultRecord[], errors: RunError[], run: Payload['run'], versions: {
|
|
54
|
-
framework: string;
|
|
55
|
-
reporter: string;
|
|
56
|
-
}, maxBytes?: number): Payload;
|
package/dist/payload.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export const MAX_RESULTS = 50_000;
|
|
2
|
-
export const MAX_PAYLOAD_BYTES = 32 * 1024 * 1024;
|
|
3
|
-
export const MAX_ERROR_MESSAGE_CHARS = 4_096;
|
|
4
|
-
export const MAX_ERROR_STACK_CHARS = 16_384;
|
|
5
|
-
export function truncate(text, max) {
|
|
6
|
-
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Keeps results in order until either cap is hit, then errors in whatever
|
|
10
|
-
* budget is left, so a payload never serializes past MAX_PAYLOAD_BYTES of
|
|
11
|
-
* JSON (the gzip input, which is also the backend's memory ceiling).
|
|
12
|
-
*/
|
|
13
|
-
export function buildPayload(results, errors, run, versions, maxBytes = MAX_PAYLOAD_BYTES) {
|
|
14
|
-
let bytes = 0;
|
|
15
|
-
let truncated = false;
|
|
16
|
-
const keep = (items, max) => {
|
|
17
|
-
const kept = [];
|
|
18
|
-
for (const item of items) {
|
|
19
|
-
const size = JSON.stringify(item).length + 1;
|
|
20
|
-
if (kept.length >= max || bytes + size > maxBytes) {
|
|
21
|
-
truncated = true;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
bytes += size;
|
|
25
|
-
kept.push(item);
|
|
26
|
-
}
|
|
27
|
-
return kept;
|
|
28
|
-
};
|
|
29
|
-
const keptResults = keep(results, MAX_RESULTS);
|
|
30
|
-
const keptErrors = keep(errors, MAX_RESULTS);
|
|
31
|
-
return {
|
|
32
|
-
framework: 'vitest',
|
|
33
|
-
framework_version: versions.framework,
|
|
34
|
-
reporter_version: versions.reporter,
|
|
35
|
-
run,
|
|
36
|
-
results: keptResults,
|
|
37
|
-
errors: keptErrors,
|
|
38
|
-
truncated,
|
|
39
|
-
};
|
|
40
|
-
}
|
package/dist/reporter.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Reporter, SerializedError, TestModule, TestRunEndReason, Vitest } from 'vitest/node';
|
|
2
|
-
import { type RunError, type TestResultRecord } from './payload.js';
|
|
3
|
-
export declare const ENV_BACKEND_URL = "BLACKSMITH_BACKEND_URL";
|
|
4
|
-
export declare const ENV_TOKEN = "BLACKSMITH_JOB_HISTORY_TOKEN";
|
|
5
|
-
export declare const RESULTS_PATH = "/api/jobs/test-results";
|
|
6
|
-
/**
|
|
7
|
-
* Marks a reporter as ours across package copies: a workspace whose root and
|
|
8
|
-
* project resolve two installs of this package has two `BlacksmithReporter`
|
|
9
|
-
* classes, and `instanceof` would let both register.
|
|
10
|
-
*/
|
|
11
|
-
export declare const REPORTER_BRAND: unique symbol;
|
|
12
|
-
export declare function isBlacksmithReporter(reporter: unknown): boolean;
|
|
13
|
-
/**
|
|
14
|
-
* Sends every test's outcome and duration to Blacksmith in one request when
|
|
15
|
-
* the run ends. Does nothing unless the runner injected BLACKSMITH_BACKEND_URL
|
|
16
|
-
* and BLACKSMITH_JOB_HISTORY_TOKEN, and never changes the run's outcome.
|
|
17
|
-
*/
|
|
18
|
-
export declare class BlacksmithReporter implements Reporter {
|
|
19
|
-
readonly [REPORTER_BRAND] = true;
|
|
20
|
-
private vitest;
|
|
21
|
-
private startedAt;
|
|
22
|
-
onInit(vitest: Vitest): void;
|
|
23
|
-
onTestRunStart(): void;
|
|
24
|
-
onTestRunEnd(testModules: ReadonlyArray<TestModule>, unhandledErrors: ReadonlyArray<SerializedError>, reason: TestRunEndReason): Promise<void>;
|
|
25
|
-
private report;
|
|
26
|
-
}
|
|
27
|
-
export declare function collectResults(testModules: ReadonlyArray<TestModule>, workspace: string | undefined): TestResultRecord[];
|
|
28
|
-
/** Errors a module raised while being imported or collected, outside any test (a syntax error, a top-level throw). */
|
|
29
|
-
export declare function collectModuleErrors(testModules: ReadonlyArray<TestModule>, workspace: string | undefined): RunError[];
|
package/dist/reporter.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'node:module';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { buildPayload, MAX_ERROR_MESSAGE_CHARS, MAX_ERROR_STACK_CHARS, truncate, } from './payload.js';
|
|
4
|
-
import { upload } from './upload.js';
|
|
5
|
-
export const ENV_BACKEND_URL = 'BLACKSMITH_BACKEND_URL';
|
|
6
|
-
export const ENV_TOKEN = 'BLACKSMITH_JOB_HISTORY_TOKEN';
|
|
7
|
-
export const RESULTS_PATH = '/api/jobs/test-results';
|
|
8
|
-
/**
|
|
9
|
-
* Marks a reporter as ours across package copies: a workspace whose root and
|
|
10
|
-
* project resolve two installs of this package has two `BlacksmithReporter`
|
|
11
|
-
* classes, and `instanceof` would let both register.
|
|
12
|
-
*/
|
|
13
|
-
export const REPORTER_BRAND = Symbol.for('@useblacksmith/vitest:reporter');
|
|
14
|
-
export function isBlacksmithReporter(reporter) {
|
|
15
|
-
return (typeof reporter === 'object' &&
|
|
16
|
-
reporter !== null &&
|
|
17
|
-
REPORTER_BRAND in reporter);
|
|
18
|
-
}
|
|
19
|
-
const { version: reporterVersion } = createRequire(import.meta.url)('../package.json');
|
|
20
|
-
/**
|
|
21
|
-
* Sends every test's outcome and duration to Blacksmith in one request when
|
|
22
|
-
* the run ends. Does nothing unless the runner injected BLACKSMITH_BACKEND_URL
|
|
23
|
-
* and BLACKSMITH_JOB_HISTORY_TOKEN, and never changes the run's outcome.
|
|
24
|
-
*/
|
|
25
|
-
export class BlacksmithReporter {
|
|
26
|
-
[REPORTER_BRAND] = true;
|
|
27
|
-
vitest;
|
|
28
|
-
startedAt = new Date();
|
|
29
|
-
onInit(vitest) {
|
|
30
|
-
this.vitest = vitest;
|
|
31
|
-
}
|
|
32
|
-
onTestRunStart() {
|
|
33
|
-
this.startedAt = new Date();
|
|
34
|
-
}
|
|
35
|
-
async onTestRunEnd(testModules, unhandledErrors, reason) {
|
|
36
|
-
const backendUrl = process.env[ENV_BACKEND_URL];
|
|
37
|
-
const token = process.env[ENV_TOKEN];
|
|
38
|
-
if (!backendUrl || !token)
|
|
39
|
-
return;
|
|
40
|
-
try {
|
|
41
|
-
await this.report(backendUrl, token, testModules, unhandledErrors, reason);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
this.vitest.logger.warn(`[blacksmith] could not report test results: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async report(backendUrl, token, testModules, unhandledErrors, reason) {
|
|
48
|
-
const workspace = process.env.GITHUB_WORKSPACE;
|
|
49
|
-
const results = collectResults(testModules, workspace);
|
|
50
|
-
const errors = [
|
|
51
|
-
...collectModuleErrors(testModules, workspace),
|
|
52
|
-
...unhandledErrors.map((error) => toRunError(null, error)),
|
|
53
|
-
];
|
|
54
|
-
if (results.length === 0 && errors.length === 0)
|
|
55
|
-
return;
|
|
56
|
-
const payload = buildPayload(results, errors, {
|
|
57
|
-
started_at: this.startedAt.toISOString(),
|
|
58
|
-
finished_at: new Date().toISOString(),
|
|
59
|
-
reason,
|
|
60
|
-
}, { framework: this.vitest.version, reporter: reporterVersion });
|
|
61
|
-
const outcome = await upload(`${backendUrl.replace(/\/+$/, '')}${RESULTS_PATH}`, token, `blacksmith-vitest/${reporterVersion}`, JSON.stringify(payload));
|
|
62
|
-
if (!outcome.ok) {
|
|
63
|
-
throw new Error(outcome.error);
|
|
64
|
-
}
|
|
65
|
-
const counts = [
|
|
66
|
-
`${payload.results.length} test results`,
|
|
67
|
-
payload.errors.length > 0 ? `${payload.errors.length} errors` : '',
|
|
68
|
-
payload.truncated ? '(truncated)' : '',
|
|
69
|
-
];
|
|
70
|
-
this.vitest.logger.log(`[blacksmith] reported ${counts.filter(Boolean).join(' ')}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
export function collectResults(testModules, workspace) {
|
|
74
|
-
const records = [];
|
|
75
|
-
for (const testModule of testModules) {
|
|
76
|
-
const file = relativeFile(testModule, workspace);
|
|
77
|
-
const project = testModule.project.name || null;
|
|
78
|
-
for (const testCase of testModule.children.allTests()) {
|
|
79
|
-
const result = testCase.result();
|
|
80
|
-
if (result.state === 'pending')
|
|
81
|
-
continue;
|
|
82
|
-
records.push(toRecord(testCase, file, project, result.state));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return records;
|
|
86
|
-
}
|
|
87
|
-
/** Errors a module raised while being imported or collected, outside any test (a syntax error, a top-level throw). */
|
|
88
|
-
export function collectModuleErrors(testModules, workspace) {
|
|
89
|
-
const errors = [];
|
|
90
|
-
for (const testModule of testModules) {
|
|
91
|
-
const file = relativeFile(testModule, workspace);
|
|
92
|
-
for (const error of testModule.errors()) {
|
|
93
|
-
errors.push(toRunError(file, error));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return errors;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Paths are repo-relative when the test file is inside the checkout (a
|
|
100
|
-
* monorepo package's vitest root is not the repo root), else Vitest's
|
|
101
|
-
* root-relative id.
|
|
102
|
-
*/
|
|
103
|
-
function relativeFile(testModule, workspace) {
|
|
104
|
-
if (workspace) {
|
|
105
|
-
const relative = path.relative(workspace, testModule.moduleId);
|
|
106
|
-
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
107
|
-
return relative.split(path.sep).join('/');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return testModule.relativeModuleId;
|
|
111
|
-
}
|
|
112
|
-
function toRecord(testCase, file, project, status) {
|
|
113
|
-
const diagnostic = testCase.diagnostic();
|
|
114
|
-
const record = {
|
|
115
|
-
file,
|
|
116
|
-
project,
|
|
117
|
-
suite: suiteChain(testCase),
|
|
118
|
-
name: testCase.name,
|
|
119
|
-
status,
|
|
120
|
-
duration_ms: diagnostic?.duration ?? null,
|
|
121
|
-
started_at: diagnostic
|
|
122
|
-
? new Date(diagnostic.startTime).toISOString()
|
|
123
|
-
: null,
|
|
124
|
-
retries: diagnostic?.retryCount ?? 0,
|
|
125
|
-
flaky: diagnostic?.flaky ?? false,
|
|
126
|
-
};
|
|
127
|
-
const error = status === 'failed' ? testCase.result().errors?.[0] : undefined;
|
|
128
|
-
if (error) {
|
|
129
|
-
record.error = {
|
|
130
|
-
message: truncate(error.message ?? '', MAX_ERROR_MESSAGE_CHARS),
|
|
131
|
-
stack: truncate(error.stack ?? '', MAX_ERROR_STACK_CHARS),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
return record;
|
|
135
|
-
}
|
|
136
|
-
function toRunError(file, error) {
|
|
137
|
-
return {
|
|
138
|
-
file,
|
|
139
|
-
message: truncate(error.message ?? '', MAX_ERROR_MESSAGE_CHARS),
|
|
140
|
-
stack: truncate(error.stack ?? '', MAX_ERROR_STACK_CHARS),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
function suiteChain(testCase) {
|
|
144
|
-
const chain = [];
|
|
145
|
-
for (let parent = testCase.parent; parent.type === 'suite'; parent = parent.parent) {
|
|
146
|
-
chain.unshift(parent.name);
|
|
147
|
-
}
|
|
148
|
-
return chain;
|
|
149
|
-
}
|
package/dist/upload.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export declare const REQUEST_TIMEOUT_MS = 10000;
|
|
2
|
-
export declare const ATTEMPTS = 3;
|
|
3
|
-
export declare const BACKOFF_MS: number[];
|
|
4
|
-
export type UploadOutcome = {
|
|
5
|
-
ok: true;
|
|
6
|
-
status: number;
|
|
7
|
-
bytes: number;
|
|
8
|
-
} | {
|
|
9
|
-
ok: false;
|
|
10
|
-
status: number | null;
|
|
11
|
-
error: string;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* One gzipped bearer POST, retried on network errors and 5xx only; a 4xx is
|
|
15
|
-
* final because the same body would be rejected again.
|
|
16
|
-
*/
|
|
17
|
-
export declare function upload(url: string, token: string, userAgent: string, json: string, sleep?: (ms: number) => Promise<void>): Promise<UploadOutcome>;
|
package/dist/upload.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { gzipSync } from 'node:zlib';
|
|
2
|
-
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
3
|
-
export const ATTEMPTS = 3;
|
|
4
|
-
export const BACKOFF_MS = [1_000, 2_000];
|
|
5
|
-
/**
|
|
6
|
-
* One gzipped bearer POST, retried on network errors and 5xx only; a 4xx is
|
|
7
|
-
* final because the same body would be rejected again.
|
|
8
|
-
*/
|
|
9
|
-
export async function upload(url, token, userAgent, json, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
10
|
-
const body = gzipSync(json);
|
|
11
|
-
let last = { ok: false, status: null, error: 'not attempted' };
|
|
12
|
-
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
13
|
-
if (attempt > 0)
|
|
14
|
-
await sleep(BACKOFF_MS[attempt - 1] ?? 0);
|
|
15
|
-
try {
|
|
16
|
-
const response = await fetch(url, {
|
|
17
|
-
method: 'POST',
|
|
18
|
-
headers: {
|
|
19
|
-
authorization: `Bearer ${token}`,
|
|
20
|
-
'content-type': 'application/json',
|
|
21
|
-
'content-encoding': 'gzip',
|
|
22
|
-
'user-agent': userAgent,
|
|
23
|
-
},
|
|
24
|
-
body,
|
|
25
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
26
|
-
});
|
|
27
|
-
if (response.ok) {
|
|
28
|
-
return { ok: true, status: response.status, bytes: body.byteLength };
|
|
29
|
-
}
|
|
30
|
-
last = {
|
|
31
|
-
ok: false,
|
|
32
|
-
status: response.status,
|
|
33
|
-
error: `HTTP ${response.status}`,
|
|
34
|
-
};
|
|
35
|
-
if (response.status < 500)
|
|
36
|
-
return last;
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
last = {
|
|
40
|
-
ok: false,
|
|
41
|
-
status: null,
|
|
42
|
-
error: error instanceof Error ? error.message : String(error),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return last;
|
|
47
|
-
}
|