hillclimb 0.9.12 → 0.9.13
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/dist/main.js +143 -35
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -118,19 +118,28 @@ import https from "https";
|
|
|
118
118
|
import net from "net";
|
|
119
119
|
var DEFAULT_SOCKET_IDLE_TIMEOUT_MS = 12e4;
|
|
120
120
|
var DEFAULT_DEADLINE_MS = 60 * 6e4;
|
|
121
|
+
var DEADLINE_FLOOR_BYTES_PER_SECOND = 128 * 1024;
|
|
122
|
+
var BUFFER_BODY_SLICE_BYTES = 64 * 1024;
|
|
123
|
+
function defaultPutDeadlineMs(bodyBytes) {
|
|
124
|
+
return Math.max(
|
|
125
|
+
DEFAULT_DEADLINE_MS,
|
|
126
|
+
Math.ceil(bodyBytes / DEADLINE_FLOOR_BYTES_PER_SECOND) * 1e3
|
|
127
|
+
);
|
|
128
|
+
}
|
|
121
129
|
var MAX_RESPONSE_BODY_BYTES = 64 * 1024;
|
|
122
130
|
async function putToPresignedUrl(url, headers, body, options) {
|
|
123
131
|
const socketIdleTimeoutMs = options?.socketIdleTimeoutMs ?? DEFAULT_SOCKET_IDLE_TIMEOUT_MS;
|
|
124
|
-
const deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
125
132
|
const parsed = new URL(url);
|
|
126
133
|
const transport = parsed.protocol === "http:" ? http : https;
|
|
127
134
|
const contentLength = body.kind === "buffer" ? body.buffer.byteLength : body.sizeBytes;
|
|
135
|
+
const deadlineMs = options?.deadlineMs ?? defaultPutDeadlineMs(contentLength);
|
|
128
136
|
const start = Date.now();
|
|
129
137
|
return new Promise((resolve, reject) => {
|
|
130
138
|
let inputStream;
|
|
131
139
|
let progressTimer;
|
|
132
140
|
let phase = "connecting";
|
|
133
141
|
let requestFinished = false;
|
|
142
|
+
let bodyBytesWritten = 0;
|
|
134
143
|
const seen = {};
|
|
135
144
|
const snapshot = () => ({
|
|
136
145
|
elapsedMs: Date.now() - start,
|
|
@@ -138,6 +147,7 @@ async function putToPresignedUrl(url, headers, body, options) {
|
|
|
138
147
|
requestFinished,
|
|
139
148
|
totalBodyBytes: contentLength,
|
|
140
149
|
...inputStream ? { bodyBytesRead: inputStream.bytesRead } : {},
|
|
150
|
+
...body.kind === "buffer" ? { bodyBytesWritten } : {},
|
|
141
151
|
...seen,
|
|
142
152
|
// Bun fills the peer address in only after 'connect'.
|
|
143
153
|
remoteAddress: seen.remoteAddress ?? req.socket?.remoteAddress
|
|
@@ -150,6 +160,7 @@ async function putToPresignedUrl(url, headers, body, options) {
|
|
|
150
160
|
clearTimeout(deadlineTimer);
|
|
151
161
|
clearInterval(progressTimer);
|
|
152
162
|
inputStream?.destroy();
|
|
163
|
+
if (!requestFinished) req.destroy();
|
|
153
164
|
resolve({ ...result, attempt });
|
|
154
165
|
};
|
|
155
166
|
const settleReject = (err) => {
|
|
@@ -174,6 +185,7 @@ async function putToPresignedUrl(url, headers, body, options) {
|
|
|
174
185
|
},
|
|
175
186
|
(res) => {
|
|
176
187
|
phase = "receiving-response";
|
|
188
|
+
seen.responseMs = Date.now() - start;
|
|
177
189
|
seen.status = res.statusCode;
|
|
178
190
|
const header2 = (name) => {
|
|
179
191
|
const value = res.headers[name];
|
|
@@ -240,6 +252,7 @@ async function putToPresignedUrl(url, headers, body, options) {
|
|
|
240
252
|
req.on("finish", () => {
|
|
241
253
|
if (req.socket?.errored) return;
|
|
242
254
|
requestFinished = true;
|
|
255
|
+
seen.finishMs = Date.now() - start;
|
|
243
256
|
if (phase !== "receiving-response") phase = "awaiting-response";
|
|
244
257
|
});
|
|
245
258
|
if (options?.onProgress) {
|
|
@@ -248,6 +261,7 @@ async function putToPresignedUrl(url, headers, body, options) {
|
|
|
248
261
|
elapsedMs: Date.now() - start,
|
|
249
262
|
phase,
|
|
250
263
|
socketBytesWritten: req.socket?.bytesWritten ?? 0,
|
|
264
|
+
...body.kind === "buffer" ? { bodyBytesWritten } : {},
|
|
251
265
|
totalBodyBytes: contentLength,
|
|
252
266
|
requestFinished
|
|
253
267
|
});
|
|
@@ -262,8 +276,33 @@ async function putToPresignedUrl(url, headers, body, options) {
|
|
|
262
276
|
req.destroy(err);
|
|
263
277
|
});
|
|
264
278
|
req.on("error", settleReject);
|
|
265
|
-
if (body.kind === "buffer") {
|
|
266
|
-
|
|
279
|
+
if (body.kind === "buffer" && process.versions.bun) {
|
|
280
|
+
const size = body.buffer.byteLength;
|
|
281
|
+
req.end(body.buffer, () => {
|
|
282
|
+
bodyBytesWritten = size;
|
|
283
|
+
});
|
|
284
|
+
} else if (body.kind === "buffer") {
|
|
285
|
+
const buffer = body.buffer;
|
|
286
|
+
let offset = 0;
|
|
287
|
+
const writeSlices = () => {
|
|
288
|
+
while (offset < buffer.byteLength) {
|
|
289
|
+
if (settled || req.destroyed) return;
|
|
290
|
+
const slice = buffer.subarray(
|
|
291
|
+
offset,
|
|
292
|
+
offset + BUFFER_BODY_SLICE_BYTES
|
|
293
|
+
);
|
|
294
|
+
offset += slice.byteLength;
|
|
295
|
+
const drained = req.write(slice, (err) => {
|
|
296
|
+
if (!err) bodyBytesWritten += slice.byteLength;
|
|
297
|
+
});
|
|
298
|
+
if (!drained) {
|
|
299
|
+
req.once("drain", writeSlices);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
req.end();
|
|
304
|
+
};
|
|
305
|
+
writeSlices();
|
|
267
306
|
} else {
|
|
268
307
|
inputStream = fs2.createReadStream(body.path);
|
|
269
308
|
inputStream.on("error", (err) => {
|
|
@@ -388,6 +427,19 @@ function putRetryDelayMs(input) {
|
|
|
388
427
|
|
|
389
428
|
// src/platform/client.ts
|
|
390
429
|
var API_REQUEST_TIMEOUT_MS = 3e4;
|
|
430
|
+
async function digestPutBody(body) {
|
|
431
|
+
const sha256 = createHash("sha256");
|
|
432
|
+
const md5 = createHash("md5");
|
|
433
|
+
let size = 0;
|
|
434
|
+
const update = (chunk) => {
|
|
435
|
+
sha256.update(chunk);
|
|
436
|
+
md5.update(chunk);
|
|
437
|
+
size += chunk.length;
|
|
438
|
+
};
|
|
439
|
+
if (body.kind === "buffer") update(body.buffer);
|
|
440
|
+
else for await (const chunk of fs3.createReadStream(body.path)) update(chunk);
|
|
441
|
+
return { sha256: sha256.digest("hex"), md5: md5.digest("hex"), size };
|
|
442
|
+
}
|
|
391
443
|
var PlatformError = class extends Error {
|
|
392
444
|
constructor(message, status, code) {
|
|
393
445
|
super(message);
|
|
@@ -639,30 +691,20 @@ var PlatformClient = class {
|
|
|
639
691
|
async uploadReplayableArtifact(contributionId, input, body) {
|
|
640
692
|
const sizeBytes = body.kind === "buffer" ? body.buffer.length : body.sizeBytes;
|
|
641
693
|
const prefix = `/api/v1/contributions/${encodeURIComponent(contributionId)}`;
|
|
642
|
-
let
|
|
694
|
+
let digests;
|
|
643
695
|
const verify = async (uploadId, options = {}) => {
|
|
644
|
-
if (
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
if (body.kind === "buffer") {
|
|
648
|
-
hash.update(body.buffer);
|
|
649
|
-
localSize = body.buffer.length;
|
|
650
|
-
} else {
|
|
651
|
-
for await (const chunk of fs3.createReadStream(body.path)) {
|
|
652
|
-
hash.update(chunk);
|
|
653
|
-
localSize += chunk.length;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (localSize !== sizeBytes)
|
|
696
|
+
if (digests === void 0) {
|
|
697
|
+
const local = await digestPutBody(body);
|
|
698
|
+
if (local.size !== sizeBytes)
|
|
657
699
|
throw new PlatformError(
|
|
658
700
|
"Prepared artifact size does not match its manifest"
|
|
659
701
|
);
|
|
660
|
-
|
|
702
|
+
digests = local;
|
|
661
703
|
}
|
|
662
|
-
return this.
|
|
704
|
+
return this.verifyStoredArtifact(
|
|
663
705
|
`${prefix}/uploads/${encodeURIComponent(uploadId)}/download`,
|
|
664
706
|
sizeBytes,
|
|
665
|
-
|
|
707
|
+
digests,
|
|
666
708
|
options
|
|
667
709
|
);
|
|
668
710
|
};
|
|
@@ -739,7 +781,12 @@ var PlatformClient = class {
|
|
|
739
781
|
}
|
|
740
782
|
await put(target);
|
|
741
783
|
}
|
|
742
|
-
|
|
784
|
+
/**
|
|
785
|
+
* Does storage hold exactly these bytes for an upload? Absent answers
|
|
786
|
+
* `exists: false`; different bytes throw UPLOAD_REPLAY_MISMATCH.
|
|
787
|
+
*/
|
|
788
|
+
async verifyStoredArtifact(relative, sizeBytes, digests, options = {}) {
|
|
789
|
+
const logPrefix = options.logPrefix ?? "upload-replay";
|
|
743
790
|
const redirect = await fetch(this.url(relative), {
|
|
744
791
|
headers: this.headers(),
|
|
745
792
|
redirect: "manual",
|
|
@@ -773,24 +820,42 @@ var PlatformClient = class {
|
|
|
773
820
|
"Upload verification requires a secure storage target"
|
|
774
821
|
);
|
|
775
822
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
823
|
+
const get = async (range) => {
|
|
824
|
+
try {
|
|
825
|
+
return await fetch(target, {
|
|
826
|
+
redirect: "error",
|
|
827
|
+
...range ? { headers: { range: "bytes=0-0" } } : {},
|
|
828
|
+
signal: AbortSignal.timeout(60 * 60 * 1e3)
|
|
829
|
+
});
|
|
830
|
+
} catch {
|
|
831
|
+
throw new PlatformError("Stored artifact verification request failed");
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
let response = await get(sizeBytes > 0);
|
|
785
835
|
if (response.status === 404 || response.status === 403 && options.forbiddenIsAbsent) {
|
|
786
836
|
await response.body?.cancel();
|
|
787
837
|
if (response.status === 403)
|
|
788
838
|
appendLog(
|
|
789
839
|
"info",
|
|
790
|
-
|
|
840
|
+
`${logPrefix}: storage refused the pending artifact (403); treating it as never written and letting the guarded PUT decide`
|
|
791
841
|
);
|
|
792
842
|
return { exists: false, url: location };
|
|
793
843
|
}
|
|
844
|
+
if (response.status === 206) {
|
|
845
|
+
await response.body?.cancel();
|
|
846
|
+
const total = /\/(\d+)$/.exec(
|
|
847
|
+
response.headers.get("content-range") ?? ""
|
|
848
|
+
)?.[1];
|
|
849
|
+
const etag = response.headers.get("etag")?.replace(/"/g, "");
|
|
850
|
+
if (Number(total) === sizeBytes && etag === digests.md5) {
|
|
851
|
+
appendLog(
|
|
852
|
+
"info",
|
|
853
|
+
`${logPrefix}: verified immutable stored artifact by ETag (bytes=${sizeBytes})`
|
|
854
|
+
);
|
|
855
|
+
return { exists: true, url: location };
|
|
856
|
+
}
|
|
857
|
+
response = await get(false);
|
|
858
|
+
}
|
|
794
859
|
if (!response.ok) {
|
|
795
860
|
await response.body?.cancel();
|
|
796
861
|
throw new PlatformError(
|
|
@@ -822,7 +887,7 @@ var PlatformClient = class {
|
|
|
822
887
|
await reader?.cancel().catch(() => {
|
|
823
888
|
});
|
|
824
889
|
}
|
|
825
|
-
if (bytes !== sizeBytes || hash.digest("hex") !==
|
|
890
|
+
if (bytes !== sizeBytes || hash.digest("hex") !== digests.sha256) {
|
|
826
891
|
throw new PlatformError(
|
|
827
892
|
"Stored artifact does not match the pending bytes",
|
|
828
893
|
409,
|
|
@@ -831,10 +896,39 @@ var PlatformClient = class {
|
|
|
831
896
|
}
|
|
832
897
|
appendLog(
|
|
833
898
|
"info",
|
|
834
|
-
|
|
899
|
+
`${logPrefix}: verified immutable stored artifact (bytes=${bytes})`
|
|
835
900
|
);
|
|
836
901
|
return { exists: true, url: location };
|
|
837
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* The whole body left this machine but no response came back. The object
|
|
905
|
+
* may still have landed: on Windows the OS finishes sending in the
|
|
906
|
+
* background after we close, and a response can be lost on the way back.
|
|
907
|
+
* Storage is the source of truth, so ask it before failing the upload.
|
|
908
|
+
*/
|
|
909
|
+
async storedAfterLostResponse(refresh, body, sizeBytes, logPrefix, label) {
|
|
910
|
+
try {
|
|
911
|
+
const digests = await digestPutBody(body);
|
|
912
|
+
if (digests.size !== sizeBytes) return false;
|
|
913
|
+
const { exists } = await this.verifyStoredArtifact(
|
|
914
|
+
`/api/v1/contributions/${encodeURIComponent(refresh.contributionId)}/uploads/${encodeURIComponent(refresh.uploadId)}/download`,
|
|
915
|
+
sizeBytes,
|
|
916
|
+
digests,
|
|
917
|
+
{ forbiddenIsAbsent: true, logPrefix: `${logPrefix} delivery check` }
|
|
918
|
+
);
|
|
919
|
+
appendLog(
|
|
920
|
+
exists ? "warn" : "info",
|
|
921
|
+
exists ? `${logPrefix} no response after the full body was sent, but storage holds exactly these bytes; treating the upload as complete | ${label}` : `${logPrefix} no response after the full body was sent, and storage does not hold the object | ${label}`
|
|
922
|
+
);
|
|
923
|
+
return exists;
|
|
924
|
+
} catch (error) {
|
|
925
|
+
appendLog(
|
|
926
|
+
"warn",
|
|
927
|
+
`${logPrefix} delivery check failed after a lost response: ${formatError(error)} | ${label}`
|
|
928
|
+
);
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
838
932
|
async uploadToPresignedUrl(presignedUrl, headers, body, refresh) {
|
|
839
933
|
await this.putToPresignedUrlLogged(
|
|
840
934
|
presignedUrl,
|
|
@@ -877,7 +971,7 @@ var PlatformClient = class {
|
|
|
877
971
|
res = await putToPresignedUrl(presignedUrl, headers, body, {
|
|
878
972
|
onProgress: (progress) => appendLog(
|
|
879
973
|
"info",
|
|
880
|
-
`[PUT ${host}] progress (attempt=${attempt}, phase=${progress.phase}, elapsedMs=${progress.elapsedMs}, requestFinished=${progress.requestFinished}, socketBytesWritten=${progress.socketBytesWritten}, totalBodyBytes=${progress.totalBodyBytes}) | ${label}`
|
|
974
|
+
`[PUT ${host}] progress (attempt=${attempt}, phase=${progress.phase}, elapsedMs=${progress.elapsedMs}, requestFinished=${progress.requestFinished}, socketBytesWritten=${progress.socketBytesWritten}${progress.bodyBytesWritten !== void 0 ? `, bodyBytesWritten=${progress.bodyBytesWritten}` : ""}, totalBodyBytes=${progress.totalBodyBytes}) | ${label}`
|
|
881
975
|
)
|
|
882
976
|
});
|
|
883
977
|
} catch (err) {
|
|
@@ -900,6 +994,14 @@ var PlatformClient = class {
|
|
|
900
994
|
"error",
|
|
901
995
|
`[PUT ${host}] FAILED ${Date.now() - start}ms ${formatError(err)} (attempt ${attempt}/${maxAttempts}) ${diagnostics2}`
|
|
902
996
|
);
|
|
997
|
+
if (refresh && err.putAttempt?.requestFinished && await this.storedAfterLostResponse(
|
|
998
|
+
refresh,
|
|
999
|
+
body,
|
|
1000
|
+
sizeBytes,
|
|
1001
|
+
`[PUT ${host}]`,
|
|
1002
|
+
label
|
|
1003
|
+
))
|
|
1004
|
+
return;
|
|
903
1005
|
throw err;
|
|
904
1006
|
}
|
|
905
1007
|
const elapsedMs = Date.now() - start;
|
|
@@ -1002,6 +1104,12 @@ function formatPutAttempt(attempt) {
|
|
|
1002
1104
|
);
|
|
1003
1105
|
if (attempt.bodyBytesRead !== void 0)
|
|
1004
1106
|
fields.push(`bodyBytesRead=${attempt.bodyBytesRead}`);
|
|
1107
|
+
if (attempt.bodyBytesWritten !== void 0)
|
|
1108
|
+
fields.push(`bodyBytesWritten=${attempt.bodyBytesWritten}`);
|
|
1109
|
+
if (attempt.finishMs !== void 0)
|
|
1110
|
+
fields.push(`finishMs=${attempt.finishMs}`);
|
|
1111
|
+
if (attempt.responseMs !== void 0)
|
|
1112
|
+
fields.push(`responseMs=${attempt.responseMs}`);
|
|
1005
1113
|
if (attempt.connectMs !== void 0)
|
|
1006
1114
|
fields.push(`connectMs=${attempt.connectMs}`);
|
|
1007
1115
|
if (attempt.tlsMs !== void 0) fields.push(`tlsMs=${attempt.tlsMs}`);
|
|
@@ -18764,7 +18872,7 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
|
|
|
18764
18872
|
}
|
|
18765
18873
|
|
|
18766
18874
|
// package.json
|
|
18767
|
-
var version = "0.9.
|
|
18875
|
+
var version = "0.9.13";
|
|
18768
18876
|
|
|
18769
18877
|
// src/version.ts
|
|
18770
18878
|
var CLI_VERSION = version;
|