@logbrew/sdk 0.1.38 → 0.1.39
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/automatic-delivery.cjs +271 -0
- package/batch-sender.cjs +164 -0
- package/bounded-number.cjs +34 -0
- package/browser-breadcrumbs.cjs +346 -136
- package/capture-support.cjs +100 -0
- package/client-options.cjs +292 -0
- package/client-report.cjs +232 -0
- package/client.cjs +447 -0
- package/console-arguments.cjs +151 -0
- package/console-capture.cjs +194 -0
- package/core.cjs +30 -2810
- package/delivery-health.cjs +133 -0
- package/error-causes.cjs +130 -0
- package/error-details.cjs +124 -0
- package/error-issue.cjs +187 -0
- package/event-clone.cjs +55 -0
- package/event-metadata.cjs +40 -0
- package/event-pipeline.cjs +325 -0
- package/event-queue.cjs +306 -0
- package/event-store.cjs +237 -0
- package/event-validation.cjs +159 -0
- package/examples/agent-timeline.cjs +57 -46
- package/examples/agent-timeline.mjs +41 -35
- package/examples/index.mjs +122 -94
- package/examples/readme-example.cjs +36 -27
- package/examples/readme-example.mjs +21 -17
- package/examples/real-user-smoke.cjs +41 -32
- package/examples/real-user-smoke.mjs +26 -22
- package/exception-chain.cjs +205 -0
- package/index.d.ts +144 -1076
- package/index.js +1 -1
- package/issue-exception.cjs +13 -8
- package/metric-validation.cjs +95 -0
- package/network-milestone.cjs +155 -0
- package/null-value.cjs +4 -0
- package/opentelemetry-bridge.cjs +330 -0
- package/opentelemetry-context.cjs +245 -0
- package/opentelemetry-exceptions.cjs +69 -0
- package/opentelemetry-keys.cjs +183 -0
- package/opentelemetry-span.cjs +286 -0
- package/opentelemetry-summary.cjs +213 -0
- package/opentelemetry-time.cjs +131 -0
- package/opentelemetry.cjs +99 -1068
- package/package.json +55 -1
- package/pino-destination.cjs +108 -0
- package/pino-record.cjs +237 -0
- package/product-action.cjs +163 -0
- package/provided-breadcrumbs.cjs +117 -0
- package/random-hex.cjs +29 -0
- package/react-native.js +1 -1
- package/recording-transport.cjs +68 -0
- package/release-artifacts-build.cjs +263 -84
- package/release-artifacts-common.js +347 -66
- package/release-artifacts-debug-id.js +178 -0
- package/release-artifacts-inspect.js +213 -0
- package/release-artifacts-issue-event.js +99 -0
- package/release-artifacts-manifest-artifact.js +107 -0
- package/release-artifacts-manifest.js +77 -0
- package/release-artifacts-mappings.js +265 -0
- package/release-artifacts-prepare.js +297 -0
- package/release-artifacts-source-context.js +210 -0
- package/release-artifacts-symbolication.js +272 -421
- package/release-artifacts-upload-files.js +122 -0
- package/release-artifacts-upload-request.js +246 -0
- package/release-artifacts-upload.js +155 -300
- package/release-artifacts-urls.js +49 -0
- package/release-artifacts.js +189 -662
- package/retry-delay.cjs +32 -0
- package/span-validation.cjs +191 -0
- package/stored-events.cjs +243 -0
- package/types/client.d.ts +67 -0
- package/types/delivery.d.ts +125 -0
- package/types/issues.d.ts +249 -0
- package/types/logging.d.ts +175 -0
- package/types/opentelemetry.d.ts +205 -0
- package/types/product.d.ts +128 -0
- package/types/recording-transport.d.ts +15 -0
- package/types/sdk-error.d.ts +9 -0
- package/types/telemetry.d.ts +171 -0
- package/types/trace-context.d.ts +110 -0
- package/types/transport-error.d.ts +10 -0
- package/utf8.cjs +37 -0
- package/validation.cjs +49 -1
- package/vite-release-artifacts-config.cjs +120 -0
- package/vite-release-artifacts.cjs +199 -99
- package/vite-release-artifacts.d.cts +65 -40
- package/vite-release-artifacts.d.ts +65 -40
- package/vite-release-artifacts.js +2 -2
- package/winston-info.cjs +205 -0
- package/winston.cjs +189 -289
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
byteSize,
|
|
3
|
+
isFile,
|
|
4
|
+
isJsonObject,
|
|
5
|
+
mapInOrder,
|
|
6
|
+
normalizeProjectId,
|
|
7
|
+
pathExists,
|
|
8
|
+
safeResolve,
|
|
9
|
+
sha256File
|
|
10
|
+
} from "./release-artifacts-common.js";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @import { JsonObject } from "./release-artifacts-common.js"
|
|
15
|
+
* @import { UploadFile } from "./release-artifacts-upload-request.js"
|
|
16
|
+
* @typedef {{ readonly artifacts: readonly unknown[], readonly manifest: JsonObject }} ReadyManifest
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const REQUIRED_FILE_FIELDS = ["path", "artifactSha256", "byteSize"];
|
|
20
|
+
const NO_ARTIFACTS = 0;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {JsonObject} manifest
|
|
24
|
+
* @param {boolean} requireProjectId
|
|
25
|
+
* @returns {ReadyManifest}
|
|
26
|
+
*/
|
|
27
|
+
function requireReadyJavaScriptManifest(manifest, requireProjectId) {
|
|
28
|
+
const { artifacts, validation } = manifest;
|
|
29
|
+
if (manifest["artifactType"] !== "javascript_source_map_manifest") {
|
|
30
|
+
throw new Error("only javascript_source_map_manifest uploads are supported by this verifier");
|
|
31
|
+
}
|
|
32
|
+
if ((isJsonObject(validation) ? validation["status"] : undefined) !== "ready") {
|
|
33
|
+
throw new Error("manifest validation status must be ready before upload");
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(artifacts) || artifacts.length === NO_ARTIFACTS) {
|
|
36
|
+
throw new Error("manifest must contain at least one JavaScript release artifact");
|
|
37
|
+
}
|
|
38
|
+
normalizeProjectId(
|
|
39
|
+
manifest["projectId"],
|
|
40
|
+
requireProjectId
|
|
41
|
+
? "hosted release artifact uploads require manifest projectId as a UUID"
|
|
42
|
+
: "manifest projectId must be a UUID when provided",
|
|
43
|
+
requireProjectId
|
|
44
|
+
);
|
|
45
|
+
return { artifacts, manifest };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {JsonObject} artifact
|
|
50
|
+
* @param {string} section
|
|
51
|
+
* @returns {JsonObject}
|
|
52
|
+
*/
|
|
53
|
+
function requireFileMetadata(artifact, section) {
|
|
54
|
+
const payload = artifact[section];
|
|
55
|
+
if (!isJsonObject(payload)) {
|
|
56
|
+
throw new Error(`artifact is missing ${section}`);
|
|
57
|
+
}
|
|
58
|
+
const missingField = REQUIRED_FILE_FIELDS.find((field) => payload[field] === undefined || payload[field] === "");
|
|
59
|
+
if (missingField !== undefined) {
|
|
60
|
+
throw new Error(`${section} is missing ${missingField}`);
|
|
61
|
+
}
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string | undefined} filePath
|
|
67
|
+
* @param {JsonObject} payload
|
|
68
|
+
* @param {string} section
|
|
69
|
+
* @returns {Promise<string>}
|
|
70
|
+
*/
|
|
71
|
+
async function requireUnchangedFile(filePath, payload, section) {
|
|
72
|
+
const recordedPath = String(payload["path"]);
|
|
73
|
+
if (filePath === undefined || !(await pathExists(filePath)) || !(await isFile(filePath))) {
|
|
74
|
+
throw new Error(`${section} file is missing: ${recordedPath}`);
|
|
75
|
+
}
|
|
76
|
+
const size = await byteSize(filePath);
|
|
77
|
+
if (size !== Number(payload["byteSize"])) {
|
|
78
|
+
throw new Error(`${section} byte size changed after manifest creation: ${recordedPath}`);
|
|
79
|
+
}
|
|
80
|
+
const digest = await sha256File(filePath);
|
|
81
|
+
if (digest !== String(payload["artifactSha256"])) {
|
|
82
|
+
throw new Error(`${section} sha256 changed after manifest creation: ${recordedPath}`);
|
|
83
|
+
}
|
|
84
|
+
return filePath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {JsonObject} artifact
|
|
89
|
+
* @param {string} buildDir
|
|
90
|
+
* @param {string} section
|
|
91
|
+
* @returns {Promise<string>}
|
|
92
|
+
*/
|
|
93
|
+
async function requireArtifactFile(artifact, buildDir, section) {
|
|
94
|
+
const payload = requireFileMetadata(artifact, section);
|
|
95
|
+
const filePath = await safeResolve(path.join(buildDir, String(payload["path"])), buildDir);
|
|
96
|
+
return requireUnchangedFile(filePath, payload, section);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {JsonObject} manifest
|
|
101
|
+
* @param {string} buildDir
|
|
102
|
+
* @param {boolean} requireProjectId
|
|
103
|
+
* @returns {Promise<{ readonly artifactCount: number, readonly files: readonly UploadFile[] }>}
|
|
104
|
+
*/
|
|
105
|
+
async function collectUploadFiles(manifest, buildDir, requireProjectId) {
|
|
106
|
+
const { artifacts } = requireReadyJavaScriptManifest(manifest, requireProjectId);
|
|
107
|
+
const files = await mapInOrder([...artifacts.keys()], async (index) => {
|
|
108
|
+
const artifact = artifacts[index];
|
|
109
|
+
if (!isJsonObject(artifact)) {
|
|
110
|
+
throw new Error("artifact entries must be JSON objects");
|
|
111
|
+
}
|
|
112
|
+
/** @type {UploadFile[]} */
|
|
113
|
+
const pair = [
|
|
114
|
+
[`minified_source_${String(index)}`, await requireArtifactFile(artifact, buildDir, "minifiedSource")],
|
|
115
|
+
[`source_map_${String(index)}`, await requireArtifactFile(artifact, buildDir, "sourceMap")]
|
|
116
|
+
];
|
|
117
|
+
return pair;
|
|
118
|
+
});
|
|
119
|
+
return { artifactCount: artifacts.length, files: files.flat() };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { collectUploadFiles };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { errorMessage, mapInOrder, stableJson } from "./release-artifacts-common.js";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @import { JsonObject } from "./release-artifacts-common.js"
|
|
11
|
+
* @typedef {"auth_failed" | "retryable_error" | "upload_failed" | "uploaded" | "validation_failed"} UploadResult
|
|
12
|
+
* @typedef {{ readonly attempt: number, readonly httpStatus?: number, readonly result: UploadResult }} UploadAttempt
|
|
13
|
+
* @typedef {readonly [name: string, filePath: string]} UploadFile
|
|
14
|
+
* @typedef {{
|
|
15
|
+
* readonly body: Buffer,
|
|
16
|
+
* readonly boundary: string,
|
|
17
|
+
* readonly endpoint: string,
|
|
18
|
+
* readonly maxRetries: number,
|
|
19
|
+
* readonly retryDelaySeconds: number,
|
|
20
|
+
* readonly timeoutSeconds: number,
|
|
21
|
+
* readonly token: string
|
|
22
|
+
* }} UploadRequest
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const HOSTED_UPLOAD_ENDPOINT = "https://api.logbrew.co/api/release-artifacts";
|
|
26
|
+
const HTTP_BAD_REQUEST = 400;
|
|
27
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
28
|
+
const HTTP_FORBIDDEN = 403;
|
|
29
|
+
const HTTP_REQUEST_TIMEOUT = 408;
|
|
30
|
+
const HTTP_PAYLOAD_TOO_LARGE = 413;
|
|
31
|
+
const HTTP_TOO_MANY_REQUESTS = 429;
|
|
32
|
+
const AUTH_FAILED_STATUSES = new Set([HTTP_UNAUTHORIZED, HTTP_FORBIDDEN]);
|
|
33
|
+
const NON_RETRYABLE_UPLOAD_STATUSES = new Set([HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_PAYLOAD_TOO_LARGE]);
|
|
34
|
+
const RETRYABLE_UPLOAD_STATUSES = new Set([HTTP_REQUEST_TIMEOUT, HTTP_TOO_MANY_REQUESTS]);
|
|
35
|
+
const SUCCESS_STATUS_MIN = 200;
|
|
36
|
+
const SUCCESS_STATUS_LIMIT = 300;
|
|
37
|
+
const SERVER_ERROR_STATUS_MIN = 500;
|
|
38
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
39
|
+
const FIRST_ATTEMPT = 1;
|
|
40
|
+
const NO_RETRY_DELAY = 0;
|
|
41
|
+
const NOT_AN_IP = 0;
|
|
42
|
+
const SCRIPT_VERSION = "0.1.0";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} endpoint
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function endpointWithoutQuery(endpoint) {
|
|
49
|
+
const { host, pathname, protocol } = new URL(endpoint);
|
|
50
|
+
return `${protocol}//${host}${pathname === "" ? "/" : pathname}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} endpoint
|
|
55
|
+
* @returns {URL}
|
|
56
|
+
*/
|
|
57
|
+
function parseEndpoint(endpoint) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = new URL(endpoint);
|
|
60
|
+
if (["http:", "https:"].includes(parsed.protocol)) {
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`upload endpoint is not a valid URL: ${errorMessage(error)}`, { cause: error });
|
|
65
|
+
}
|
|
66
|
+
throw new Error("release artifact upload proof endpoint must use http or https");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} endpointHostname
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function isLoopbackEndpoint(endpointHostname) {
|
|
74
|
+
const hostname = endpointHostname.toLowerCase();
|
|
75
|
+
return (
|
|
76
|
+
hostname === "localhost" ||
|
|
77
|
+
hostname === "[::1]" ||
|
|
78
|
+
hostname === "::1" ||
|
|
79
|
+
(net.isIP(hostname) !== NOT_AN_IP && (hostname.startsWith("127.") || hostname === "::1"))
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} endpoint
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function isLoopbackUploadEndpoint(endpoint) {
|
|
88
|
+
return isLoopbackEndpoint(parseEndpoint(endpoint).hostname);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} endpoint
|
|
93
|
+
* @param {boolean} allowHosted
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
function requireUploadEndpoint(endpoint, allowHosted) {
|
|
97
|
+
if (isLoopbackUploadEndpoint(endpoint)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (!allowHosted) {
|
|
101
|
+
throw new Error("release artifact hosted upload requires explicit --allow-hosted; use loopback endpoints for local proof");
|
|
102
|
+
}
|
|
103
|
+
if (endpoint !== HOSTED_UPLOAD_ENDPOINT) {
|
|
104
|
+
throw new Error(`hosted release artifact uploads must use ${HOSTED_UPLOAD_ENDPOINT}`);
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} value
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
function quoteMultipartValue(value) {
|
|
114
|
+
return value.replaceAll("\\", String.raw`\\`).replaceAll("\"", String.raw`\"`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} boundary
|
|
119
|
+
* @param {{ readonly contentType: string, readonly filename: string, readonly name: string }} part
|
|
120
|
+
* @param {Buffer} bytes
|
|
121
|
+
* @returns {Buffer[]}
|
|
122
|
+
*/
|
|
123
|
+
function multipartPart(boundary, { contentType, filename, name }, bytes) {
|
|
124
|
+
return [
|
|
125
|
+
Buffer.from(`--${boundary}\r\n`, "ascii"),
|
|
126
|
+
Buffer.from(
|
|
127
|
+
`Content-Disposition: form-data; name="${quoteMultipartValue(name)}"; filename="${quoteMultipartValue(filename)}"\r\n`,
|
|
128
|
+
"utf8"
|
|
129
|
+
),
|
|
130
|
+
Buffer.from(`Content-Type: ${contentType}\r\n\r\n`, "ascii"),
|
|
131
|
+
bytes,
|
|
132
|
+
Buffer.from("\r\n", "ascii")
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {JsonObject} manifest
|
|
138
|
+
* @param {readonly UploadFile[]} files
|
|
139
|
+
* @returns {Promise<{ readonly body: Buffer, readonly boundary: string }>}
|
|
140
|
+
*/
|
|
141
|
+
async function encodeMultipart(manifest, files) {
|
|
142
|
+
const boundary = `logbrew-${randomUUID().replaceAll("-", "")}`;
|
|
143
|
+
const manifestPart = multipartPart(
|
|
144
|
+
boundary,
|
|
145
|
+
{ contentType: "application/json", filename: "manifest.json", name: "manifest" },
|
|
146
|
+
Buffer.from(stableJson(manifest), "utf8")
|
|
147
|
+
);
|
|
148
|
+
const fileParts = await mapInOrder(files, async ([name, filePath]) =>
|
|
149
|
+
multipartPart(boundary, { contentType: "application/octet-stream", filename: path.basename(filePath), name }, await readFile(filePath)));
|
|
150
|
+
return {
|
|
151
|
+
body: Buffer.concat([...manifestPart, ...fileParts.flat(), Buffer.from(`--${boundary}--\r\n`, "ascii")]),
|
|
152
|
+
boundary
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {number} status
|
|
158
|
+
* @returns {UploadResult}
|
|
159
|
+
*/
|
|
160
|
+
function classifyUploadStatus(status) {
|
|
161
|
+
if (status >= SUCCESS_STATUS_MIN && status < SUCCESS_STATUS_LIMIT) {
|
|
162
|
+
return "uploaded";
|
|
163
|
+
}
|
|
164
|
+
if (AUTH_FAILED_STATUSES.has(status)) {
|
|
165
|
+
return "auth_failed";
|
|
166
|
+
}
|
|
167
|
+
if (NON_RETRYABLE_UPLOAD_STATUSES.has(status)) {
|
|
168
|
+
return "validation_failed";
|
|
169
|
+
}
|
|
170
|
+
if (RETRYABLE_UPLOAD_STATUSES.has(status) || status >= SERVER_ERROR_STATUS_MIN) {
|
|
171
|
+
return "retryable_error";
|
|
172
|
+
}
|
|
173
|
+
return "upload_failed";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {UploadRequest} request
|
|
178
|
+
* @returns {Promise<number>}
|
|
179
|
+
*/
|
|
180
|
+
async function postMultipart({ body, boundary, endpoint, timeoutSeconds, token }) {
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeout = setTimeout(() => {
|
|
183
|
+
controller.abort();
|
|
184
|
+
}, timeoutSeconds * MILLISECONDS_PER_SECOND);
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(endpoint, {
|
|
187
|
+
body,
|
|
188
|
+
headers: {
|
|
189
|
+
Authorization: `Bearer ${token}`,
|
|
190
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
191
|
+
"User-Agent": `logbrew-release-artifact-verifier/${SCRIPT_VERSION}`
|
|
192
|
+
},
|
|
193
|
+
method: "POST",
|
|
194
|
+
signal: controller.signal
|
|
195
|
+
});
|
|
196
|
+
await response.arrayBuffer();
|
|
197
|
+
return response.status;
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {UploadRequest} request
|
|
205
|
+
* @param {number} attempt
|
|
206
|
+
* @returns {Promise<UploadAttempt>}
|
|
207
|
+
*/
|
|
208
|
+
async function attemptUpload(request, attempt) {
|
|
209
|
+
try {
|
|
210
|
+
const httpStatus = await postMultipart(request);
|
|
211
|
+
return { attempt, httpStatus, result: classifyUploadStatus(httpStatus) };
|
|
212
|
+
} catch {
|
|
213
|
+
return { attempt, result: "retryable_error" };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {UploadRequest} request
|
|
219
|
+
* @returns {Promise<{ readonly attempts: readonly UploadAttempt[], readonly retryCount: number, readonly status: UploadResult }>}
|
|
220
|
+
*/
|
|
221
|
+
async function uploadWithRetries(request) {
|
|
222
|
+
/** @type {UploadAttempt[]} */
|
|
223
|
+
const attempts = [];
|
|
224
|
+
/**
|
|
225
|
+
* @param {number} attempt
|
|
226
|
+
* @returns {Promise<void>}
|
|
227
|
+
*/
|
|
228
|
+
async function attemptFrom(attempt) {
|
|
229
|
+
const outcome = await attemptUpload(request, attempt);
|
|
230
|
+
attempts.push(outcome);
|
|
231
|
+
if (outcome.result === "retryable_error" && attempt <= request.maxRetries) {
|
|
232
|
+
if (request.retryDelaySeconds > NO_RETRY_DELAY) {
|
|
233
|
+
await delay(request.retryDelaySeconds * MILLISECONDS_PER_SECOND);
|
|
234
|
+
}
|
|
235
|
+
await attemptFrom(attempt + FIRST_ATTEMPT);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
await attemptFrom(FIRST_ATTEMPT);
|
|
239
|
+
return {
|
|
240
|
+
attempts,
|
|
241
|
+
retryCount: Math.max(NO_RETRY_DELAY, attempts.length - FIRST_ATTEMPT),
|
|
242
|
+
status: attempts.at(-FIRST_ATTEMPT)?.result ?? "upload_failed"
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { encodeMultipart, endpointWithoutQuery, isLoopbackUploadEndpoint, requireUploadEndpoint, uploadWithRetries };
|