@automattic/vip 4.0.5 → 4.0.7

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,41 @@ 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
+ const isStreamBody = body => typeof body?.pipe === 'function';
47
+
48
+ /**
49
+ * Wraps `fetch` with exponential-backoff retries.
50
+ *
51
+ * A request body that is a Node stream can only be consumed once. If the body
52
+ * is a stream and we retried with the same `init`, the second attempt would
53
+ * pass an already-consumed (disturbed/locked) stream to `fetch`, which throws
54
+ * the misleading `Response body object should not be disturbed or locked` error
55
+ * and masks the original network failure.
56
+ *
57
+ * To retry safely, callers that send a stream body should pass `createBody`,
58
+ * which is invoked to produce a fresh body for every attempt. If a stream body
59
+ * is supplied without a `createBody` factory, the request is attempted only
60
+ * once so the underlying error surfaces instead of the misleading undici one.
61
+ */
62
+ async function fetchWithRetry(input, init = {}, retries = 3, createBody) {
63
+ const bodyIsStream = isStreamBody(init.body);
64
+ // Only retry when we can hand `fetch` a fresh, replayable body each attempt.
65
+ const maxAttempts = createBody || !bodyIsStream ? retries : 0;
66
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
67
+ const requestInit = createBody ? {
68
+ ...init,
69
+ body: createBody()
70
+ } : {
71
+ ...init
72
+ };
73
+ if (isStreamBody(requestInit.body) && !requestInit.duplex) {
74
+ requestInit.duplex = 'half';
75
+ }
46
76
  try {
47
77
  // eslint-disable-next-line no-await-in-loop
48
- return await (0, _undici.fetch)(input, init);
78
+ return await (0, _undici.fetch)(input, requestInit);
49
79
  } catch (err) {
50
- if (attempt === retries) {
80
+ if (attempt === maxAttempts) {
51
81
  throw err;
52
82
  }
53
83
  // eslint-disable-next-line no-await-in-loop
@@ -220,20 +250,28 @@ async function uploadUsingPutObject({
220
250
  ...fetchOptions.headers,
221
251
  'Content-Length': `${fileSize}` // This has to be a string
222
252
  };
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);
253
+
254
+ // Build a fresh request body for every attempt. The upload streams the file
255
+ // from disk, and a stream can only be consumed once; recreating it per
256
+ // attempt lets `fetchWithRetry` retry without reusing a consumed stream
257
+ // (which would otherwise throw `... should not be disturbed or locked`).
258
+ const createBody = () => {
259
+ if (fileContent) {
260
+ return fileContent;
231
261
  }
232
- });
233
- const response = await fetchWithRetry(presignedRequest.url, {
234
- ...fetchOptions,
235
- body: fileContent ?? (0, _fs.createReadStream)(fileName).pipe(progressPassThrough)
236
- });
262
+ let readBytes = 0;
263
+ const progressPassThrough = new _stream.PassThrough();
264
+ progressPassThrough.on('data', data => {
265
+ readBytes += data.length;
266
+ const percentage = `${Math.floor(100 * readBytes / fileSize)}%`;
267
+ debug(percentage);
268
+ if (typeof progressCallback === 'function') {
269
+ progressCallback(percentage);
270
+ }
271
+ });
272
+ return (0, _fs.createReadStream)(fileName).pipe(progressPassThrough);
273
+ };
274
+ const response = await fetchWithRetry(presignedRequest.url, fetchOptions, 3, createBody);
237
275
  if (response.status === 200) {
238
276
  return 'ok';
239
277
  }
@@ -428,18 +466,19 @@ async function uploadParts({
428
466
  progressCallback
429
467
  }) {
430
468
  let uploadsInProgress = 0;
431
- let totalBytesRead = 0;
432
469
  const partPercentages = new Array(parts.length).fill(0);
470
+ const partBytesRead = new Array(parts.length).fill(0);
433
471
  const readyForPartUpload = () => new Promise(resolve => {
434
- const canDoInterval = setInterval(() => {
472
+ const canDoInterval = (0, _nodeTimers.setInterval)(() => {
435
473
  if (uploadsInProgress < MAX_CONCURRENT_PART_UPLOADS) {
436
474
  uploadsInProgress++;
437
- clearInterval(canDoInterval);
475
+ (0, _nodeTimers.clearInterval)(canDoInterval);
438
476
  resolve();
439
477
  }
440
478
  }, 300);
441
479
  });
442
480
  const updateProgress = () => {
481
+ const totalBytesRead = partBytesRead.reduce((sum, bytes) => sum + bytes, 0);
443
482
  const percentage = `${Math.floor(100 * totalBytesRead / fileMeta.fileSize)}%`;
444
483
  if (typeof progressCallback === 'function') {
445
484
  progressCallback(percentage);
@@ -451,32 +490,46 @@ async function uploadParts({
451
490
  return `Part # ${index}: ${partPercentage}% of ${(partSize / _fileSize.MB_IN_BYTES).toFixed(2)}MB`;
452
491
  }).join('\n') + `\n\nOverall Progress: ${percentage}% of ${(fileMeta.fileSize / _fileSize.MB_IN_BYTES).toFixed(2)}MB`);
453
492
  };
454
- const updateProgressInterval = setInterval(updateProgress, 500);
493
+ const updateProgressInterval = (0, _nodeTimers.setInterval)(updateProgress, 500);
455
494
  const allDone = await Promise.all(parts.map(async part => {
456
495
  const {
457
496
  index,
458
- partSize
497
+ partSize,
498
+ start,
499
+ end
459
500
  } = 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
- });
501
+
502
+ // Build a fresh request body for every attempt. A stream can only be
503
+ // consumed once, so recreating the ranged read stream (and its progress
504
+ // PassThrough) per attempt lets `fetchWithRetry` retry without reusing a
505
+ // disturbed/locked stream. Reset this part's progress counters so a retry
506
+ // re-streams the part without double-counting bytes.
507
+ const createBody = () => {
508
+ partBytesRead[index] = 0;
509
+ partPercentages[index] = 0;
510
+ const progressPassThrough = new _stream.PassThrough();
511
+ progressPassThrough.on('data', data => {
512
+ partBytesRead[index] += data.length;
513
+ partPercentages[index] = Math.floor(100 * partBytesRead[index] / partSize);
514
+ });
515
+ return (0, _fs.createReadStream)(fileMeta.fileName, {
516
+ start,
517
+ end
518
+ }).pipe(progressPassThrough);
519
+ };
467
520
  await readyForPartUpload();
468
521
  const uploadResult = await uploadPart({
469
522
  app,
470
523
  env,
471
524
  fileMeta,
472
525
  part,
473
- progressPassThrough,
526
+ createBody,
474
527
  uploadId
475
528
  });
476
529
  uploadsInProgress--;
477
530
  return uploadResult;
478
531
  }));
479
- clearInterval(updateProgressInterval);
532
+ (0, _nodeTimers.clearInterval)(updateProgressInterval);
480
533
  updateProgress();
481
534
  return allDone;
482
535
  }
@@ -484,18 +537,15 @@ async function uploadPart({
484
537
  app,
485
538
  env,
486
539
  fileMeta: {
487
- basename,
488
- fileName
540
+ basename
489
541
  },
490
542
  part,
491
- progressPassThrough,
543
+ createBody,
492
544
  uploadId
493
545
  }) {
494
546
  const {
495
- end,
496
547
  index,
497
- partSize,
498
- start
548
+ partSize
499
549
  } = part;
500
550
  const s3PartNumber = index + 1; // S3 multipart is indexed from 1
501
551
 
@@ -521,11 +571,7 @@ async function uploadPart({
521
571
  * ...so it may not be feasible to include with presigned requests.
522
572
  */
523
573
  };
524
- fetchOptions.body = (0, _fs.createReadStream)(fileName, {
525
- start,
526
- end
527
- }).pipe(progressPassThrough);
528
- const fetchResponse = await fetchWithRetry(partUploadRequestData.url, fetchOptions);
574
+ const fetchResponse = await fetchWithRetry(partUploadRequestData.url, fetchOptions, 3, createBody);
529
575
  if (fetchResponse.status === 200) {
530
576
  const etag = fetchResponse.headers.get('etag');
531
577
  if (!etag) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
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.7",
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.7",
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",