pnpm 11.26.0 → 11.27.0
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/CHANGELOG.md +60 -0
- package/dist/node_modules/undici/lib/dispatcher/client-h1.js +12 -4
- package/dist/node_modules/undici/lib/handler/retry-handler.js +29 -3
- package/dist/node_modules/undici/lib/web/eventsource/eventsource-stream.js +245 -150
- package/dist/node_modules/undici/lib/web/websocket/connection.js +1 -1
- package/dist/node_modules/undici/lib/web/websocket/permessage-deflate.js +5 -0
- package/dist/node_modules/undici/package.json +1 -1
- package/dist/pnpm.mjs +5543 -5392
- package/dist/worker.js +6355 -3192
- package/package.json +1 -1
- package/dist/node_modules/.package-map.json +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# pnpm
|
|
2
2
|
|
|
3
|
+
## 11.27.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `nodeDownloadMirrors` can now be set in the global config file (`config.yaml`) and through the `PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS` environment variable, so a Node.js download mirror can be configured once for a machine instead of in every workspace [#12124](https://github.com/pnpm/pnpm/issues/12124), [#13611](https://github.com/pnpm/pnpm/issues/13611).
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS='{"release":"https://npmmirror.com/mirrors/node/"}'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Added a new setting `trustPolicyExcludePrune` (default: `false`). When enabled, `pnpm add`, `pnpm update`, and `pnpm remove` prune the entries of `trustPolicyExclude` in `pnpm-workspace.yaml` that the freshly written lockfile no longer resolves: versions that are gone are dropped (an entry is removed once none of its versions remain), and entries for packages that are no longer in the lockfile are removed too. Name patterns (`@scope/*`) are always kept. The cleanup is skipped when the install's lockfile does not cover the whole workspace (`sharedWorkspaceLockfile: false`), since entries another project still needs would look stale.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- pnpm now reads the `packageManager`, `devEngines.packageManager` and runtime pins from the workspace root's `package.json` when `lockfileDir` is set. A project that moved its lockfile lost the pins it declared there [#14633](https://github.com/pnpm/pnpm/issues/14633).
|
|
18
|
+
|
|
19
|
+
- Fixed `pnpm add -g`, `pnpm update -g`, and `pnpm remove -g` mutating global bins or install directories after only partially reading an installed package group. If any declared package manifest is missing, malformed, or unreadable, pnpm now fails before activation or removal and leaves the existing global installation intact [pnpm/pnpm#13796](https://github.com/pnpm/pnpm/issues/13796).
|
|
20
|
+
|
|
21
|
+
- `fetch-timeout` now limits how long a request may make no progress. The timer restarts on every chunk that arrives. A large download over a slow connection is no longer aborted while data is still coming in. A connection that stops delivering data still fails after `fetch-timeout` [#14604](https://github.com/pnpm/pnpm/issues/14604).
|
|
22
|
+
|
|
23
|
+
- `pnpm peers check` no longer reports a peer dependency declared as `workspace:^`, `workspace:~`, or a bare `workspace:` as unmet. pnpm reported these as unmet whatever version the linked workspace project supplied [#14770](https://github.com/pnpm/pnpm/issues/14770).
|
|
24
|
+
|
|
25
|
+
- A `readPackage` hook that edits its argument in place no longer changes what a later install in the same command resolves. A `deprecated` notice read from the lockfile no longer carries over to another install either [#13988](https://github.com/pnpm/pnpm/issues/13988).
|
|
26
|
+
|
|
27
|
+
- `pnpm install` now auto-installs missing transitive peers when workspace projects share a dependency at different depths. This also removes incomplete duplicate peer contexts from the lockfile. Fixes [pnpm/pnpm#14840](https://github.com/pnpm/pnpm/issues/14840).
|
|
28
|
+
|
|
29
|
+
- GitHub Actions updates now stop if an action reference changes while its versions are being resolved. Unrelated workflow edits are preserved.
|
|
30
|
+
|
|
31
|
+
GitHub Actions homepage links no longer expose server credentials. GitHub server URLs now require HTTPS, with HTTP allowed only for loopback hosts.
|
|
32
|
+
|
|
33
|
+
- `pnpm licenses list` now reports the runtime downloaded through `devEngines.runtime` with `onFail: "download"`. The command previously failed with `ERR_PNPM_UNSUPPORTED_PACKAGE_TYPE` [#14172](https://github.com/pnpm/pnpm/issues/14172).
|
|
34
|
+
|
|
35
|
+
- pnpm no longer creates a project `pnpm-lock.yaml` when `devEngines.packageManager.onFail` is `download` and lockfile writing is turned off with `lockfile: false` or `--no-lockfile`. pnpm still switches to the pinned version [#14728](https://github.com/pnpm/pnpm/issues/14728).
|
|
36
|
+
|
|
37
|
+
- A `registry` or `@scope:registry` set in an `.npmrc` now wins over the registry a `pnpm login` credential stored in the global `config.yaml` points at. Previously, after logging in to one registry, installs in a project whose `.npmrc` named a private registry went to the logged-in registry instead. They now go to the registry the `.npmrc` names [#14614](https://github.com/pnpm/pnpm/issues/14614).
|
|
38
|
+
|
|
39
|
+
- A patch that gives a dependency a `preinstall`, `install`, or `postinstall` script, or a `binding.gyp`, now runs that build. pnpm asks for build approval first, so the package is listed under "Ignored build scripts" until it is allowed to build. pnpm 12 ran nothing, and pnpm 11 ran it without asking [#14648](https://github.com/pnpm/pnpm/issues/14648).
|
|
40
|
+
|
|
41
|
+
- Registries that share a host but differ by URL path — one JFrog Artifactory, Nexus, AWS CodeArtifact or GitLab Packages instance serving several repositories — now get a metadata cache directory each. Previously they shared one, so resolving a package from one of them could answer with another's versions, integrity hashes and tarball URLs and fail with `ERR_PNPM_TARBALL_URL_MISMATCH` [#13558](https://github.com/pnpm/pnpm/issues/13558).
|
|
42
|
+
|
|
43
|
+
The URL scheme is part of the cache directory name too, so an `http` registry can no longer hand its metadata — which can be rewritten in transit — to a resolution configured for `https` at the same host.
|
|
44
|
+
|
|
45
|
+
The first install after upgrading refetches registry metadata once. The package store is untouched.
|
|
46
|
+
|
|
47
|
+
`pnpm cache view` now labels each entry with the full registry URL. It printed `registry.npmjs.org` before and prints `https://registry.npmjs.org/` now.
|
|
48
|
+
|
|
49
|
+
`pnpm cache list-registries` and `pnpm cache list` print the new directory names. Scripts that parse either command need updating.
|
|
50
|
+
|
|
51
|
+
- Updated the embedded Node.js release keys to the current canonical `nodejs/release-keys` list.
|
|
52
|
+
|
|
53
|
+
- `pnpm sbom` now omits package author fields when the manifest author name is empty or contains only whitespace [pnpm/pnpm#14685](https://github.com/pnpm/pnpm/issues/14685). In a filtered or split workspace run, only a project with no `author` field inherits the workspace root's author.
|
|
54
|
+
|
|
55
|
+
- `pnpm sbom --sbom-format spdx` now writes `creationInfo.created` with whole seconds, such as `2026-09-08T10:38:21Z`. The timestamp carried fractional seconds, which strict SPDX consumers rejected [#14684](https://github.com/pnpm/pnpm/issues/14684).
|
|
56
|
+
|
|
57
|
+
- Windows filesystem operations now retry permission errors for up to one second. Permanent permission errors previously delayed failure by a minute. Sharing and lock violations retain their one-minute retry budget [pnpm/pnpm#14682](https://github.com/pnpm/pnpm/issues/14682).
|
|
58
|
+
|
|
59
|
+
- pnpm now writes `node_modules/.package-map.json` only when `nodeExperimentalPackageMap` is enabled. Nothing reads the file without that setting. An install that stops writing the map removes the one a previous install left.
|
|
60
|
+
|
|
61
|
+
- pnpm now unpacks a downloaded runtime archive into a randomly named directory inside the store. It previously used a predictable path, where another user of a shared store could plant a symlink and redirect the write outside the store ([GHSA-vwc7-r8mq-g2x9](https://github.com/advisories/GHSA-vwc7-r8mq-g2x9)).
|
|
62
|
+
|
|
3
63
|
## 11.26.0
|
|
4
64
|
|
|
5
65
|
### Minor Changes
|
|
@@ -876,7 +876,7 @@ async function connectH1 (client, socket) {
|
|
|
876
876
|
|
|
877
877
|
function clearIdleSocketValidation (socket) {
|
|
878
878
|
if (socket[kIdleSocketValidationTimeout]) {
|
|
879
|
-
|
|
879
|
+
clearImmediate(socket[kIdleSocketValidationTimeout])
|
|
880
880
|
socket[kIdleSocketValidationTimeout] = null
|
|
881
881
|
}
|
|
882
882
|
|
|
@@ -885,15 +885,23 @@ function clearIdleSocketValidation (socket) {
|
|
|
885
885
|
|
|
886
886
|
function scheduleIdleSocketValidation (client, socket) {
|
|
887
887
|
socket[kIdleSocketValidation] = 1
|
|
888
|
-
|
|
888
|
+
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
|
889
|
+
// already pending on this idle keep-alive socket are processed before the
|
|
890
|
+
// next request is written (GHSA-35p6-xmwp-9g52).
|
|
891
|
+
//
|
|
892
|
+
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
|
893
|
+
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
|
894
|
+
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
|
895
|
+
// A ref'd Immediate both keeps the pending request alive and makes poll
|
|
896
|
+
// return immediately — the hybrid those issues asked for.
|
|
897
|
+
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
|
889
898
|
socket[kIdleSocketValidationTimeout] = null
|
|
890
899
|
socket[kIdleSocketValidation] = 2
|
|
891
900
|
|
|
892
901
|
if (client[kSocket] === socket && !socket.destroyed) {
|
|
893
902
|
client[kResume]()
|
|
894
903
|
}
|
|
895
|
-
}
|
|
896
|
-
socket[kIdleSocketValidationTimeout].unref?.()
|
|
904
|
+
})
|
|
897
905
|
}
|
|
898
906
|
|
|
899
907
|
/**
|
|
@@ -90,6 +90,7 @@ class RetryHandler {
|
|
|
90
90
|
this.end = null
|
|
91
91
|
this.etag = null
|
|
92
92
|
this.resume = null
|
|
93
|
+
this.headersSent = false
|
|
93
94
|
|
|
94
95
|
// Handle possible onConnect duplication
|
|
95
96
|
this.handler.onConnect(reason => {
|
|
@@ -102,6 +103,20 @@ class RetryHandler {
|
|
|
102
103
|
})
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
checkpointResponseEnd (headers, resume) {
|
|
107
|
+
if (this.end == null && this.opts.method !== 'HEAD') {
|
|
108
|
+
const contentLength = headers['content-length']
|
|
109
|
+
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
|
110
|
+
|
|
111
|
+
assert(
|
|
112
|
+
this.end == null || Number.isFinite(this.end),
|
|
113
|
+
'invalid content-length'
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.resume = this.end != null ? resume : null
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
onRequestSent () {
|
|
106
121
|
if (this.handler.onRequestSent) {
|
|
107
122
|
this.handler.onRequestSent()
|
|
@@ -191,6 +206,8 @@ class RetryHandler {
|
|
|
191
206
|
|
|
192
207
|
if (statusCode >= 300) {
|
|
193
208
|
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
|
209
|
+
this.headersSent = true
|
|
210
|
+
this.checkpointResponseEnd(headers, resume)
|
|
194
211
|
return this.handler.onHeaders(
|
|
195
212
|
statusCode,
|
|
196
213
|
rawHeaders,
|
|
@@ -259,8 +276,15 @@ class RetryHandler {
|
|
|
259
276
|
|
|
260
277
|
const { start, size, end = size - 1 } = contentRange
|
|
261
278
|
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
if (this.start !== start || (this.end != null && this.end !== end)) {
|
|
280
|
+
this.abort(
|
|
281
|
+
new RequestRetryError('Content-Range mismatch', statusCode, {
|
|
282
|
+
headers,
|
|
283
|
+
data: { count: this.retryCount }
|
|
284
|
+
})
|
|
285
|
+
)
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
264
288
|
|
|
265
289
|
this.resume = resume
|
|
266
290
|
return true
|
|
@@ -272,6 +296,7 @@ class RetryHandler {
|
|
|
272
296
|
const range = parseRangeHeader(headers['content-range'])
|
|
273
297
|
|
|
274
298
|
if (range == null) {
|
|
299
|
+
this.headersSent = true
|
|
275
300
|
return this.handler.onHeaders(
|
|
276
301
|
statusCode,
|
|
277
302
|
rawHeaders,
|
|
@@ -310,6 +335,7 @@ class RetryHandler {
|
|
|
310
335
|
)
|
|
311
336
|
|
|
312
337
|
this.resume = resume
|
|
338
|
+
this.headersSent = true
|
|
313
339
|
this.etag = headers.etag != null ? headers.etag : null
|
|
314
340
|
|
|
315
341
|
// Weak etags are not useful for comparison nor cache
|
|
@@ -349,7 +375,7 @@ class RetryHandler {
|
|
|
349
375
|
}
|
|
350
376
|
|
|
351
377
|
onError (err) {
|
|
352
|
-
if (this.aborted || isDisturbed(this.opts.body)) {
|
|
378
|
+
if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
|
|
353
379
|
return this.handler.onError(err)
|
|
354
380
|
}
|
|
355
381
|
|
|
@@ -23,6 +23,49 @@ const COLON = 0x3A
|
|
|
23
23
|
*/
|
|
24
24
|
const SPACE = 0x20
|
|
25
25
|
|
|
26
|
+
const DATA = Buffer.from('data')
|
|
27
|
+
const EVENT = Buffer.from('event')
|
|
28
|
+
const ID = Buffer.from('id')
|
|
29
|
+
const RETRY = Buffer.from('retry')
|
|
30
|
+
|
|
31
|
+
function isASCIINumberBytes (buffer, start) {
|
|
32
|
+
if (start >= buffer.length) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (let i = start; i < buffer.length; i++) {
|
|
37
|
+
if (buffer[i] < 0x30 || buffer[i] > 0x39) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isValidLastEventIdBytes (buffer, start) {
|
|
46
|
+
for (let i = start; i < buffer.length; i++) {
|
|
47
|
+
if (buffer[i] === 0x00) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isFieldName (line, length, field) {
|
|
56
|
+
if (length !== field.length) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < length; i++) {
|
|
61
|
+
if (line[i] !== field[i]) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
26
69
|
/**
|
|
27
70
|
* @typedef {object} EventSourceStreamEvent
|
|
28
71
|
* @type {object}
|
|
@@ -63,11 +106,14 @@ class EventSourceStream extends Transform {
|
|
|
63
106
|
eventEndCheck = false
|
|
64
107
|
|
|
65
108
|
/**
|
|
66
|
-
* @type {Buffer}
|
|
109
|
+
* @type {Buffer[]}
|
|
67
110
|
*/
|
|
68
|
-
|
|
111
|
+
chunks = []
|
|
69
112
|
|
|
113
|
+
chunkIndex = 0
|
|
70
114
|
pos = 0
|
|
115
|
+
lineChunkIndex = 0
|
|
116
|
+
linePos = 0
|
|
71
117
|
|
|
72
118
|
event = {
|
|
73
119
|
data: undefined,
|
|
@@ -106,92 +152,20 @@ class EventSourceStream extends Transform {
|
|
|
106
152
|
return
|
|
107
153
|
}
|
|
108
154
|
|
|
109
|
-
|
|
110
|
-
// processing it
|
|
111
|
-
// TODO: Investigate if there is a more performant way to handle
|
|
112
|
-
// incoming chunks
|
|
113
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
114
|
-
if (this.buffer) {
|
|
115
|
-
this.buffer = Buffer.concat([this.buffer, chunk])
|
|
116
|
-
} else {
|
|
117
|
-
this.buffer = chunk
|
|
118
|
-
}
|
|
155
|
+
this.chunks.push(chunk)
|
|
119
156
|
|
|
120
157
|
// Strip leading byte-order-mark if we opened the stream and started
|
|
121
158
|
// the processing of the incoming data
|
|
122
159
|
if (this.checkBOM) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (this.buffer[0] === BOM[0]) {
|
|
127
|
-
// If it is, we need to wait for more data
|
|
128
|
-
callback()
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
132
|
-
// BOM anymore
|
|
133
|
-
this.checkBOM = false
|
|
134
|
-
|
|
135
|
-
// The buffer only contains one byte so we need to wait for more data
|
|
136
|
-
callback()
|
|
137
|
-
return
|
|
138
|
-
case 2:
|
|
139
|
-
// Check if the first two bytes are the same as the first two bytes
|
|
140
|
-
// of the BOM
|
|
141
|
-
if (
|
|
142
|
-
this.buffer[0] === BOM[0] &&
|
|
143
|
-
this.buffer[1] === BOM[1]
|
|
144
|
-
) {
|
|
145
|
-
// If it is, we need to wait for more data, because the third byte
|
|
146
|
-
// is needed to determine if it is the BOM or not
|
|
147
|
-
callback()
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
152
|
-
// BOM anymore
|
|
153
|
-
this.checkBOM = false
|
|
154
|
-
break
|
|
155
|
-
case 3:
|
|
156
|
-
// Check if the first three bytes are the same as the first three
|
|
157
|
-
// bytes of the BOM
|
|
158
|
-
if (
|
|
159
|
-
this.buffer[0] === BOM[0] &&
|
|
160
|
-
this.buffer[1] === BOM[1] &&
|
|
161
|
-
this.buffer[2] === BOM[2]
|
|
162
|
-
) {
|
|
163
|
-
// If it is, we can drop the buffered data, as it is only the BOM
|
|
164
|
-
this.buffer = Buffer.alloc(0)
|
|
165
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
166
|
-
// BOM anymore
|
|
167
|
-
this.checkBOM = false
|
|
168
|
-
|
|
169
|
-
// Await more data
|
|
170
|
-
callback()
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
// If it is not the BOM, we can start processing the data
|
|
174
|
-
this.checkBOM = false
|
|
175
|
-
break
|
|
176
|
-
default:
|
|
177
|
-
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
|
|
178
|
-
// present
|
|
179
|
-
if (
|
|
180
|
-
this.buffer[0] === BOM[0] &&
|
|
181
|
-
this.buffer[1] === BOM[1] &&
|
|
182
|
-
this.buffer[2] === BOM[2]
|
|
183
|
-
) {
|
|
184
|
-
// Remove the BOM from the buffer
|
|
185
|
-
this.buffer = this.buffer.subarray(3)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
189
|
-
this.checkBOM = false
|
|
190
|
-
break
|
|
160
|
+
if (this.handleBOM()) {
|
|
161
|
+
callback()
|
|
162
|
+
return
|
|
191
163
|
}
|
|
192
164
|
}
|
|
193
165
|
|
|
194
|
-
while (this.
|
|
166
|
+
while (this.hasCurrentByte()) {
|
|
167
|
+
const byte = this.currentByte()
|
|
168
|
+
|
|
195
169
|
// If the previous line ended with an end-of-line, we need to check
|
|
196
170
|
// if the next character is also an end-of-line.
|
|
197
171
|
if (this.eventEndCheck) {
|
|
@@ -204,10 +178,9 @@ class EventSourceStream extends Transform {
|
|
|
204
178
|
if (this.crlfCheck) {
|
|
205
179
|
// If the current character is a line feed, we can remove it
|
|
206
180
|
// from the buffer and reset the crlfCheck flag
|
|
207
|
-
if (
|
|
208
|
-
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
209
|
-
this.pos = 0
|
|
181
|
+
if (byte === LF) {
|
|
210
182
|
this.crlfCheck = false
|
|
183
|
+
this.consumeCurrentByte()
|
|
211
184
|
|
|
212
185
|
// It is possible that the line feed is not the end of the
|
|
213
186
|
// event. We need to check if the next character is an
|
|
@@ -223,19 +196,17 @@ class EventSourceStream extends Transform {
|
|
|
223
196
|
this.crlfCheck = false
|
|
224
197
|
}
|
|
225
198
|
|
|
226
|
-
if (
|
|
199
|
+
if (byte === LF || byte === CR) {
|
|
227
200
|
// If the current character is a carriage return, we need to
|
|
228
201
|
// set the crlfCheck flag to true, as we need to check if the
|
|
229
202
|
// next character is a line feed so we can remove it from the
|
|
230
203
|
// buffer
|
|
231
|
-
if (
|
|
204
|
+
if (byte === CR) {
|
|
232
205
|
this.crlfCheck = true
|
|
233
206
|
}
|
|
234
207
|
|
|
235
|
-
this.
|
|
236
|
-
this.
|
|
237
|
-
if (
|
|
238
|
-
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
|
|
208
|
+
this.consumeCurrentByte()
|
|
209
|
+
if (this.hasPendingEvent()) {
|
|
239
210
|
this.processEvent(this.event)
|
|
240
211
|
}
|
|
241
212
|
this.clearEvent()
|
|
@@ -249,22 +220,18 @@ class EventSourceStream extends Transform {
|
|
|
249
220
|
|
|
250
221
|
// If the current character is an end-of-line, we can process the
|
|
251
222
|
// line
|
|
252
|
-
if (
|
|
223
|
+
if (byte === LF || byte === CR) {
|
|
253
224
|
// If the current character is a carriage return, we need to
|
|
254
225
|
// set the crlfCheck flag to true, as we need to check if the
|
|
255
226
|
// next character is a line feed
|
|
256
|
-
if (
|
|
227
|
+
if (byte === CR) {
|
|
257
228
|
this.crlfCheck = true
|
|
258
229
|
}
|
|
259
230
|
|
|
260
231
|
// In any case, we can process the line as we reached an
|
|
261
232
|
// end-of-line character
|
|
262
|
-
this.parseLine(this.
|
|
263
|
-
|
|
264
|
-
// Remove the processed line from the buffer
|
|
265
|
-
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
266
|
-
// Reset the position as we removed the processed line from the buffer
|
|
267
|
-
this.pos = 0
|
|
233
|
+
this.parseLine(this.readLine(), this.event)
|
|
234
|
+
this.consumeCurrentByte()
|
|
268
235
|
// A line was processed and this could be the end of the event. We need
|
|
269
236
|
// to check if the next line is empty to determine if the event is
|
|
270
237
|
// finished.
|
|
@@ -272,7 +239,7 @@ class EventSourceStream extends Transform {
|
|
|
272
239
|
continue
|
|
273
240
|
}
|
|
274
241
|
|
|
275
|
-
this.
|
|
242
|
+
this.advanceCursor()
|
|
276
243
|
}
|
|
277
244
|
|
|
278
245
|
callback()
|
|
@@ -297,64 +264,53 @@ class EventSourceStream extends Transform {
|
|
|
297
264
|
return
|
|
298
265
|
}
|
|
299
266
|
|
|
300
|
-
let
|
|
301
|
-
let
|
|
267
|
+
let fieldLength = line.length
|
|
268
|
+
let valueStart = line.length
|
|
302
269
|
|
|
303
270
|
// If the line contains a U+003A COLON character (:)
|
|
304
271
|
if (colonPosition !== -1) {
|
|
305
|
-
|
|
306
|
-
// character (:), and let field be that string.
|
|
307
|
-
// TODO: Investigate if there is a more performant way to extract the
|
|
308
|
-
// field
|
|
309
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
310
|
-
field = line.subarray(0, colonPosition).toString('utf8')
|
|
272
|
+
fieldLength = colonPosition
|
|
311
273
|
|
|
312
274
|
// Collect the characters on the line after the first U+003A COLON
|
|
313
275
|
// character (:), and let value be that string.
|
|
314
276
|
// If value starts with a U+0020 SPACE character, remove it from value.
|
|
315
|
-
|
|
277
|
+
valueStart = colonPosition + 1
|
|
316
278
|
if (line[valueStart] === SPACE) {
|
|
317
279
|
++valueStart
|
|
318
280
|
}
|
|
319
|
-
// TODO: Investigate if there is a more performant way to extract the
|
|
320
|
-
// value
|
|
321
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
322
|
-
value = line.subarray(valueStart).toString('utf8')
|
|
323
|
-
|
|
324
|
-
// Otherwise, the string is not empty but does not contain a U+003A COLON
|
|
325
|
-
// character (:)
|
|
326
|
-
} else {
|
|
327
|
-
// Process the field using the steps described below, using the whole
|
|
328
|
-
// line as the field name, and the empty string as the field value.
|
|
329
|
-
field = line.toString('utf8')
|
|
330
|
-
value = ''
|
|
331
281
|
}
|
|
332
282
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
283
|
+
if (isFieldName(line, fieldLength, DATA)) {
|
|
284
|
+
const value = line.toString('utf8', valueStart)
|
|
285
|
+
|
|
286
|
+
if (event.data === undefined) {
|
|
287
|
+
event.data = value
|
|
288
|
+
} else {
|
|
289
|
+
event.data += `\n${value}`
|
|
290
|
+
}
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (isFieldName(line, fieldLength, RETRY)) {
|
|
295
|
+
if (isASCIINumberBytes(line, valueStart)) {
|
|
296
|
+
event.retry = line.toString('utf8', valueStart)
|
|
297
|
+
}
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isFieldName(line, fieldLength, ID)) {
|
|
302
|
+
if (isValidLastEventIdBytes(line, valueStart)) {
|
|
303
|
+
event.id = line.toString('utf8', valueStart)
|
|
304
|
+
}
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isFieldName(line, fieldLength, EVENT)) {
|
|
309
|
+
const value = line.toString('utf8', valueStart)
|
|
310
|
+
|
|
311
|
+
if (value.length > 0) {
|
|
312
|
+
event.event = value
|
|
313
|
+
}
|
|
358
314
|
}
|
|
359
315
|
}
|
|
360
316
|
|
|
@@ -384,12 +340,151 @@ class EventSourceStream extends Transform {
|
|
|
384
340
|
}
|
|
385
341
|
|
|
386
342
|
clearEvent () {
|
|
387
|
-
this.event =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
343
|
+
this.event.data = undefined
|
|
344
|
+
this.event.event = undefined
|
|
345
|
+
this.event.id = undefined
|
|
346
|
+
this.event.retry = undefined
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
hasPendingEvent () {
|
|
350
|
+
return this.event.data !== undefined ||
|
|
351
|
+
this.event.event !== undefined ||
|
|
352
|
+
this.event.id !== undefined ||
|
|
353
|
+
this.event.retry !== undefined
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
hasCurrentByte () {
|
|
357
|
+
return this.chunkIndex < this.chunks.length &&
|
|
358
|
+
this.pos < this.chunks[this.chunkIndex].length
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
currentByte () {
|
|
362
|
+
return this.chunks[this.chunkIndex][this.pos]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
consumeCurrentByte () {
|
|
366
|
+
this.advanceCursor()
|
|
367
|
+
this.syncLineStartToCursor()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
advanceCursor () {
|
|
371
|
+
this.pos++
|
|
372
|
+
|
|
373
|
+
while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
|
|
374
|
+
this.chunkIndex++
|
|
375
|
+
this.pos = 0
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
syncLineStartToCursor () {
|
|
380
|
+
this.lineChunkIndex = this.chunkIndex
|
|
381
|
+
this.linePos = this.pos
|
|
382
|
+
this.dropConsumedChunks()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
dropConsumedChunks () {
|
|
386
|
+
while (this.lineChunkIndex > 0) {
|
|
387
|
+
this.chunks.shift()
|
|
388
|
+
this.lineChunkIndex--
|
|
389
|
+
this.chunkIndex--
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.chunkIndex === this.chunks.length) {
|
|
393
|
+
this.chunks.length = 0
|
|
394
|
+
this.chunkIndex = 0
|
|
395
|
+
this.pos = 0
|
|
396
|
+
this.lineChunkIndex = 0
|
|
397
|
+
this.linePos = 0
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
readLine () {
|
|
402
|
+
if (this.lineChunkIndex === this.chunkIndex) {
|
|
403
|
+
return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const chunks = []
|
|
407
|
+
let length = 0
|
|
408
|
+
|
|
409
|
+
for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
|
|
410
|
+
const chunk = this.chunks[i]
|
|
411
|
+
const start = i === this.lineChunkIndex ? this.linePos : 0
|
|
412
|
+
const end = i === this.chunkIndex ? this.pos : chunk.length
|
|
413
|
+
const slice = chunk.subarray(start, end)
|
|
414
|
+
length += slice.length
|
|
415
|
+
chunks.push(slice)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return Buffer.concat(chunks, length)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
peekBufferedByte (offset) {
|
|
422
|
+
let chunkIndex = this.lineChunkIndex
|
|
423
|
+
let pos = this.linePos
|
|
424
|
+
|
|
425
|
+
while (chunkIndex < this.chunks.length) {
|
|
426
|
+
const chunk = this.chunks[chunkIndex]
|
|
427
|
+
const remaining = chunk.length - pos
|
|
428
|
+
|
|
429
|
+
if (offset < remaining) {
|
|
430
|
+
return chunk[pos + offset]
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
offset -= remaining
|
|
434
|
+
chunkIndex++
|
|
435
|
+
pos = 0
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
discardLeadingBytes (count) {
|
|
440
|
+
while (count > 0 && this.lineChunkIndex < this.chunks.length) {
|
|
441
|
+
const chunk = this.chunks[this.lineChunkIndex]
|
|
442
|
+
const remaining = chunk.length - this.linePos
|
|
443
|
+
|
|
444
|
+
if (count < remaining) {
|
|
445
|
+
this.linePos += count
|
|
446
|
+
count = 0
|
|
447
|
+
} else {
|
|
448
|
+
count -= remaining
|
|
449
|
+
this.lineChunkIndex++
|
|
450
|
+
this.linePos = 0
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.chunkIndex = this.lineChunkIndex
|
|
455
|
+
this.pos = this.linePos
|
|
456
|
+
this.dropConsumedChunks()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
handleBOM () {
|
|
460
|
+
const first = this.peekBufferedByte(0)
|
|
461
|
+
const second = this.peekBufferedByte(1)
|
|
462
|
+
const third = this.peekBufferedByte(2)
|
|
463
|
+
|
|
464
|
+
if (second === undefined) {
|
|
465
|
+
if (first === BOM[0]) {
|
|
466
|
+
return true
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.checkBOM = false
|
|
470
|
+
return true
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (third === undefined) {
|
|
474
|
+
if (first === BOM[0] && second === BOM[1]) {
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.checkBOM = false
|
|
479
|
+
return false
|
|
392
480
|
}
|
|
481
|
+
|
|
482
|
+
if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
|
|
483
|
+
this.discardLeadingBytes(3)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.checkBOM = false
|
|
487
|
+
return !this.hasCurrentByte()
|
|
393
488
|
}
|
|
394
489
|
}
|
|
395
490
|
|
|
@@ -192,7 +192,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|
|
192
192
|
// is specified, the server needs to include the same field and one of
|
|
193
193
|
// the selected subprotocol values in its response for the connection to
|
|
194
194
|
// be established.
|
|
195
|
-
if (!requestProtocols.includes(secProtocol)) {
|
|
195
|
+
if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
|
|
196
196
|
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
|
|
197
197
|
return
|
|
198
198
|
}
|