@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.
- package/dist/lib/client-file-uploader.js +81 -42
- package/npm-shrinkwrap.json +65 -19
- package/package.json +4 -4
|
@@ -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
|
-
|
|
45
|
-
|
|
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,
|
|
71
|
+
return await (0, _undici.fetch)(input, requestInit);
|
|
49
72
|
} catch (err) {
|
|
50
|
-
if (attempt ===
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/vip",
|
|
3
|
-
"version": "4.0.
|
|
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.
|
|
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": "^
|
|
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.
|
|
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": "^
|
|
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": "
|
|
4574
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-
|
|
4575
|
-
"integrity": "sha512-
|
|
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": "
|
|
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": "
|
|
11398
|
-
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-
|
|
11399
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
13422
|
-
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.
|
|
13423
|
-
"integrity": "sha512-
|
|
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": "
|
|
14591
|
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-
|
|
14592
|
-
"integrity": "sha512-
|
|
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.
|
|
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": "^
|
|
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": "^
|
|
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.
|
|
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",
|