@automattic/vip 4.0.5 → 4.0.6

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.
@@ -4,6 +4,7 @@ exports.__esModule = true;
4
4
  exports.MULTIPART_THRESHOLD = exports.COMPRESS_THRESHOLD = void 0;
5
5
  exports.checkFileAccess = checkFileAccess;
6
6
  exports.detectCompressedMimeType = detectCompressedMimeType;
7
+ exports.fetchWithRetry = fetchWithRetry;
7
8
  exports.getFileHash = void 0;
8
9
  exports.getFileMeta = getFileMeta;
9
10
  exports.getFileSize = getFileSize;
@@ -20,6 +21,7 @@ var _debug = _interopRequireDefault(require("debug"));
20
21
  var _fs = require("fs");
21
22
  var _promises = require("node:fs/promises");
22
23
  var _promises2 = require("node:stream/promises");
24
+ var _nodeTimers = require("node:timers");
23
25
  var _promises3 = require("node:timers/promises");
24
26
  var _os = _interopRequireDefault(require("os"));
25
27
  var _path = _interopRequireDefault(require("path"));
@@ -41,13 +43,34 @@ function parseEtagHeader(etag) {
41
43
  }
42
44
  return normalizedEtag.replace(/^"(.*)"$/u, '$1');
43
45
  }
44
- async function fetchWithRetry(input, init, retries = 3) {
45
- for (let attempt = 0; attempt <= retries; attempt++) {
46
+ /**
47
+ * Wraps `fetch` with exponential-backoff retries.
48
+ *
49
+ * A request body that is a Node stream can only be consumed once. If the body
50
+ * is a stream and we retried with the same `init`, the second attempt would
51
+ * pass an already-consumed (disturbed/locked) stream to `fetch`, which throws
52
+ * the misleading `Response body object should not be disturbed or locked` error
53
+ * and masks the original network failure.
54
+ *
55
+ * To retry safely, callers that send a stream body should pass `createBody`,
56
+ * which is invoked to produce a fresh body for every attempt. If a stream body
57
+ * is supplied without a `createBody` factory, the request is attempted only
58
+ * once so the underlying error surfaces instead of the misleading undici one.
59
+ */
60
+ async function fetchWithRetry(input, init = {}, retries = 3, createBody) {
61
+ const bodyIsStream = typeof init.body?.pipe === 'function';
62
+ // Only retry when we can hand `fetch` a fresh, replayable body each attempt.
63
+ const maxAttempts = createBody || !bodyIsStream ? retries : 0;
64
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
65
+ const requestInit = createBody ? {
66
+ ...init,
67
+ body: createBody()
68
+ } : init;
46
69
  try {
47
70
  // eslint-disable-next-line no-await-in-loop
48
- return await (0, _undici.fetch)(input, init);
71
+ return await (0, _undici.fetch)(input, requestInit);
49
72
  } catch (err) {
50
- if (attempt === retries) {
73
+ if (attempt === maxAttempts) {
51
74
  throw err;
52
75
  }
53
76
  // eslint-disable-next-line no-await-in-loop
@@ -220,20 +243,28 @@ async function uploadUsingPutObject({
220
243
  ...fetchOptions.headers,
221
244
  'Content-Length': `${fileSize}` // This has to be a string
222
245
  };
223
- let readBytes = 0;
224
- const progressPassThrough = new _stream.PassThrough();
225
- progressPassThrough.on('data', data => {
226
- readBytes += data.length;
227
- const percentage = `${Math.floor(100 * readBytes / fileSize)}%`;
228
- debug(percentage);
229
- if (typeof progressCallback === 'function') {
230
- progressCallback(percentage);
246
+
247
+ // Build a fresh request body for every attempt. The upload streams the file
248
+ // from disk, and a stream can only be consumed once; recreating it per
249
+ // attempt lets `fetchWithRetry` retry without reusing a consumed stream
250
+ // (which would otherwise throw `... should not be disturbed or locked`).
251
+ const createBody = () => {
252
+ if (fileContent) {
253
+ return fileContent;
231
254
  }
232
- });
233
- const response = await fetchWithRetry(presignedRequest.url, {
234
- ...fetchOptions,
235
- body: fileContent ?? (0, _fs.createReadStream)(fileName).pipe(progressPassThrough)
236
- });
255
+ let readBytes = 0;
256
+ const progressPassThrough = new _stream.PassThrough();
257
+ progressPassThrough.on('data', data => {
258
+ readBytes += data.length;
259
+ const percentage = `${Math.floor(100 * readBytes / fileSize)}%`;
260
+ debug(percentage);
261
+ if (typeof progressCallback === 'function') {
262
+ progressCallback(percentage);
263
+ }
264
+ });
265
+ return (0, _fs.createReadStream)(fileName).pipe(progressPassThrough);
266
+ };
267
+ const response = await fetchWithRetry(presignedRequest.url, fetchOptions, 3, createBody);
237
268
  if (response.status === 200) {
238
269
  return 'ok';
239
270
  }
@@ -428,18 +459,19 @@ async function uploadParts({
428
459
  progressCallback
429
460
  }) {
430
461
  let uploadsInProgress = 0;
431
- let totalBytesRead = 0;
432
462
  const partPercentages = new Array(parts.length).fill(0);
463
+ const partBytesRead = new Array(parts.length).fill(0);
433
464
  const readyForPartUpload = () => new Promise(resolve => {
434
- const canDoInterval = setInterval(() => {
465
+ const canDoInterval = (0, _nodeTimers.setInterval)(() => {
435
466
  if (uploadsInProgress < MAX_CONCURRENT_PART_UPLOADS) {
436
467
  uploadsInProgress++;
437
- clearInterval(canDoInterval);
468
+ (0, _nodeTimers.clearInterval)(canDoInterval);
438
469
  resolve();
439
470
  }
440
471
  }, 300);
441
472
  });
442
473
  const updateProgress = () => {
474
+ const totalBytesRead = partBytesRead.reduce((sum, bytes) => sum + bytes, 0);
443
475
  const percentage = `${Math.floor(100 * totalBytesRead / fileMeta.fileSize)}%`;
444
476
  if (typeof progressCallback === 'function') {
445
477
  progressCallback(percentage);
@@ -451,32 +483,46 @@ async function uploadParts({
451
483
  return `Part # ${index}: ${partPercentage}% of ${(partSize / _fileSize.MB_IN_BYTES).toFixed(2)}MB`;
452
484
  }).join('\n') + `\n\nOverall Progress: ${percentage}% of ${(fileMeta.fileSize / _fileSize.MB_IN_BYTES).toFixed(2)}MB`);
453
485
  };
454
- const updateProgressInterval = setInterval(updateProgress, 500);
486
+ const updateProgressInterval = (0, _nodeTimers.setInterval)(updateProgress, 500);
455
487
  const allDone = await Promise.all(parts.map(async part => {
456
488
  const {
457
489
  index,
458
- partSize
490
+ partSize,
491
+ start,
492
+ end
459
493
  } = part;
460
- const progressPassThrough = new _stream.PassThrough();
461
- let partBytesRead = 0;
462
- progressPassThrough.on('data', data => {
463
- totalBytesRead += data.length;
464
- partBytesRead += data.length;
465
- partPercentages[index] = Math.floor(100 * partBytesRead / partSize);
466
- });
494
+
495
+ // Build a fresh request body for every attempt. A stream can only be
496
+ // consumed once, so recreating the ranged read stream (and its progress
497
+ // PassThrough) per attempt lets `fetchWithRetry` retry without reusing a
498
+ // disturbed/locked stream. Reset this part's progress counters so a retry
499
+ // re-streams the part without double-counting bytes.
500
+ const createBody = () => {
501
+ partBytesRead[index] = 0;
502
+ partPercentages[index] = 0;
503
+ const progressPassThrough = new _stream.PassThrough();
504
+ progressPassThrough.on('data', data => {
505
+ partBytesRead[index] += data.length;
506
+ partPercentages[index] = Math.floor(100 * partBytesRead[index] / partSize);
507
+ });
508
+ return (0, _fs.createReadStream)(fileMeta.fileName, {
509
+ start,
510
+ end
511
+ }).pipe(progressPassThrough);
512
+ };
467
513
  await readyForPartUpload();
468
514
  const uploadResult = await uploadPart({
469
515
  app,
470
516
  env,
471
517
  fileMeta,
472
518
  part,
473
- progressPassThrough,
519
+ createBody,
474
520
  uploadId
475
521
  });
476
522
  uploadsInProgress--;
477
523
  return uploadResult;
478
524
  }));
479
- clearInterval(updateProgressInterval);
525
+ (0, _nodeTimers.clearInterval)(updateProgressInterval);
480
526
  updateProgress();
481
527
  return allDone;
482
528
  }
@@ -484,18 +530,15 @@ async function uploadPart({
484
530
  app,
485
531
  env,
486
532
  fileMeta: {
487
- basename,
488
- fileName
533
+ basename
489
534
  },
490
535
  part,
491
- progressPassThrough,
536
+ createBody,
492
537
  uploadId
493
538
  }) {
494
539
  const {
495
- end,
496
540
  index,
497
- partSize,
498
- start
541
+ partSize
499
542
  } = part;
500
543
  const s3PartNumber = index + 1; // S3 multipart is indexed from 1
501
544
 
@@ -521,11 +564,7 @@ async function uploadPart({
521
564
  * ...so it may not be feasible to include with presigned requests.
522
565
  */
523
566
  };
524
- fetchOptions.body = (0, _fs.createReadStream)(fileName, {
525
- start,
526
- end
527
- }).pipe(progressPassThrough);
528
- const fetchResponse = await fetchWithRetry(partUploadRequestData.url, fetchOptions);
567
+ const fetchResponse = await fetchWithRetry(partUploadRequestData.url, fetchOptions, 3, createBody);
529
568
  if (fetchResponse.status === 200) {
530
569
  const etag = fetchResponse.headers.get('etag');
531
570
  if (!etag) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "4.0.5",
3
+ "version": "4.0.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@automattic/vip",
9
- "version": "4.0.5",
9
+ "version": "4.0.6",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -28,13 +28,13 @@
28
28
  "graphql": "17.0.1",
29
29
  "graphql-tag": "2.12.7",
30
30
  "https-proxy-agent": "^9.0.0",
31
- "js-yaml": "^4.1.0",
31
+ "js-yaml": "^5.0.0",
32
32
  "jwt-decode": "4.0.0",
33
33
  "lando": "github:automattic/lando-cli#78d382fcf40357c4a4cebe4a3cfaef032eab743a",
34
34
  "node-stream-zip": "1.15.0",
35
35
  "open": "^11.0.0",
36
36
  "proxy-from-env": "^2.0.0",
37
- "semver": "7.8.4",
37
+ "semver": "7.8.5",
38
38
  "shelljs": "^0.10.0",
39
39
  "socket.io-client": "^4.5.3",
40
40
  "socket.io-stream": "npm:@wwa/socket.io-stream@^0.10.0",
@@ -124,7 +124,7 @@
124
124
  "@types/ejs": "^3.1.5",
125
125
  "@types/jest": "^30.0.0",
126
126
  "@types/js-yaml": "^4.0.9",
127
- "@types/node": "^25.9.1",
127
+ "@types/node": "^26.0.0",
128
128
  "@types/proxy-from-env": "^1.0.4",
129
129
  "@types/semver": "^7.5.5",
130
130
  "@types/shelljs": "^0.10.0",
@@ -2865,6 +2865,30 @@
2865
2865
  "url": "https://github.com/sponsors/sindresorhus"
2866
2866
  }
2867
2867
  },
2868
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
2869
+ "version": "4.2.0",
2870
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
2871
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
2872
+ "dev": true,
2873
+ "funding": [
2874
+ {
2875
+ "type": "github",
2876
+ "url": "https://github.com/sponsors/puzrin"
2877
+ },
2878
+ {
2879
+ "type": "github",
2880
+ "url": "https://github.com/sponsors/nodeca"
2881
+ }
2882
+ ],
2883
+ "license": "MIT",
2884
+ "peer": true,
2885
+ "dependencies": {
2886
+ "argparse": "^2.0.1"
2887
+ },
2888
+ "bin": {
2889
+ "js-yaml": "bin/js-yaml.js"
2890
+ }
2891
+ },
2868
2892
  "node_modules/@eslint/eslintrc/node_modules/minimatch": {
2869
2893
  "version": "3.1.5",
2870
2894
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -4570,12 +4594,12 @@
4570
4594
  "license": "MIT"
4571
4595
  },
4572
4596
  "node_modules/@types/node": {
4573
- "version": "25.9.3",
4574
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
4575
- "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
4597
+ "version": "26.0.0",
4598
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
4599
+ "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
4576
4600
  "license": "MIT",
4577
4601
  "dependencies": {
4578
- "undici-types": ">=7.24.0 <7.24.7"
4602
+ "undici-types": "~8.3.0"
4579
4603
  }
4580
4604
  },
4581
4605
  "node_modules/@types/proxy-from-env": {
@@ -11394,9 +11418,9 @@
11394
11418
  "license": "MIT"
11395
11419
  },
11396
11420
  "node_modules/js-yaml": {
11397
- "version": "4.2.0",
11398
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
11399
- "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
11421
+ "version": "5.0.0",
11422
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.0.0.tgz",
11423
+ "integrity": "sha512-GSvaPUbk1U+FMZ7rJzF+F8e5YVtu7KnD40et/5rBXXRBv2jCO9L3qCewvIDDdudC0QycTFlf6EAA+h3kxBsuUw==",
11400
11424
  "funding": [
11401
11425
  {
11402
11426
  "type": "github",
@@ -11412,7 +11436,7 @@
11412
11436
  "argparse": "^2.0.1"
11413
11437
  },
11414
11438
  "bin": {
11415
- "js-yaml": "bin/js-yaml.js"
11439
+ "js-yaml": "bin/js-yaml.mjs"
11416
11440
  }
11417
11441
  },
11418
11442
  "node_modules/jsdoc-type-pratt-parser": {
@@ -11668,6 +11692,28 @@
11668
11692
  "url": "https://github.com/sponsors/isaacs"
11669
11693
  }
11670
11694
  },
11695
+ "node_modules/lando/node_modules/js-yaml": {
11696
+ "version": "4.2.0",
11697
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
11698
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
11699
+ "funding": [
11700
+ {
11701
+ "type": "github",
11702
+ "url": "https://github.com/sponsors/puzrin"
11703
+ },
11704
+ {
11705
+ "type": "github",
11706
+ "url": "https://github.com/sponsors/nodeca"
11707
+ }
11708
+ ],
11709
+ "license": "MIT",
11710
+ "dependencies": {
11711
+ "argparse": "^2.0.1"
11712
+ },
11713
+ "bin": {
11714
+ "js-yaml": "bin/js-yaml.js"
11715
+ }
11716
+ },
11671
11717
  "node_modules/lando/node_modules/lru-cache": {
11672
11718
  "version": "11.2.7",
11673
11719
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
@@ -13418,9 +13464,9 @@
13418
13464
  }
13419
13465
  },
13420
13466
  "node_modules/semver": {
13421
- "version": "7.8.4",
13422
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
13423
- "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
13467
+ "version": "7.8.5",
13468
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
13469
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
13424
13470
  "license": "ISC",
13425
13471
  "bin": {
13426
13472
  "semver": "bin/semver.js"
@@ -14587,9 +14633,9 @@
14587
14633
  }
14588
14634
  },
14589
14635
  "node_modules/undici-types": {
14590
- "version": "7.24.6",
14591
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
14592
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
14636
+ "version": "8.3.0",
14637
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
14638
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
14593
14639
  "license": "MIT"
14594
14640
  },
14595
14641
  "node_modules/unicode-canonical-property-names-ecmascript": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "4.0.5",
3
+ "version": "4.0.6",
4
4
  "description": "The VIP Javascript library & CLI",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -128,7 +128,7 @@
128
128
  "@types/ejs": "^3.1.5",
129
129
  "@types/jest": "^30.0.0",
130
130
  "@types/js-yaml": "^4.0.9",
131
- "@types/node": "^25.9.1",
131
+ "@types/node": "^26.0.0",
132
132
  "@types/proxy-from-env": "^1.0.4",
133
133
  "@types/semver": "^7.5.5",
134
134
  "@types/shelljs": "^0.10.0",
@@ -159,13 +159,13 @@
159
159
  "graphql": "17.0.1",
160
160
  "graphql-tag": "2.12.7",
161
161
  "https-proxy-agent": "^9.0.0",
162
- "js-yaml": "^4.1.0",
162
+ "js-yaml": "^5.0.0",
163
163
  "jwt-decode": "4.0.0",
164
164
  "lando": "github:automattic/lando-cli#78d382fcf40357c4a4cebe4a3cfaef032eab743a",
165
165
  "node-stream-zip": "1.15.0",
166
166
  "open": "^11.0.0",
167
167
  "proxy-from-env": "^2.0.0",
168
- "semver": "7.8.4",
168
+ "semver": "7.8.5",
169
169
  "shelljs": "^0.10.0",
170
170
  "socket.io-client": "^4.5.3",
171
171
  "socket.io-stream": "npm:@wwa/socket.io-stream@^0.10.0",