Skip to content

Commit 9655941

Browse files
authored
enhance(server): refactor and simplify useContentEncoding plugin (#2229)
1 parent 6bf6aa0 commit 9655941

File tree

2 files changed

+46
-110
lines changed

2 files changed

+46
-110
lines changed

.changeset/mean-ties-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@whatwg-node/server': patch
3+
---
4+
5+
Simplify `useContentEncoding` plugin
Lines changed: 41 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
import type { Readable } from 'node:stream';
2-
import {
3-
decompressedResponseMap,
4-
getSupportedEncodings,
5-
isAsyncIterable,
6-
isReadable,
7-
} from '../utils.js';
1+
import { decompressedResponseMap, getSupportedEncodings } from '../utils.js';
82
import type { ServerAdapterPlugin } from './types.js';
93

4+
const emptyEncodings = ['none', 'identity'];
5+
106
export function useContentEncoding<TServerContext>(): ServerAdapterPlugin<TServerContext> {
11-
const encodingMap = new WeakMap<Request, string[]>();
127
return {
138
onRequest({ request, setRequest, fetchAPI, endResponse }) {
14-
if (request.body) {
15-
const contentEncodingHeader = request.headers.get('content-encoding');
16-
if (contentEncodingHeader && contentEncodingHeader !== 'none') {
17-
const contentEncodings = contentEncodingHeader?.split(',');
9+
const contentEncodingHeader = request.headers.get('content-encoding');
10+
if (
11+
contentEncodingHeader &&
12+
contentEncodingHeader !== 'none' &&
13+
contentEncodingHeader !== 'identity' &&
14+
request.body
15+
) {
16+
const contentEncodings = contentEncodingHeader
17+
.split(',')
18+
.filter(encoding => !emptyEncodings.includes(encoding)) as CompressionFormat[];
19+
if (contentEncodings.length) {
1820
if (
19-
!contentEncodings.every(encoding =>
20-
getSupportedEncodings(fetchAPI).includes(encoding as CompressionFormat),
21-
)
21+
!contentEncodings.every(encoding => getSupportedEncodings(fetchAPI).includes(encoding))
2222
) {
2323
endResponse(
2424
new fetchAPI.Response(`Unsupported 'Content-Encoding': ${contentEncodingHeader}`, {
@@ -30,83 +30,46 @@ export function useContentEncoding<TServerContext>(): ServerAdapterPlugin<TServe
3030
}
3131
let newBody = request.body;
3232
for (const contentEncoding of contentEncodings) {
33-
newBody = newBody.pipeThrough(
34-
new fetchAPI.DecompressionStream(contentEncoding as CompressionFormat),
35-
);
33+
newBody = request.body.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding));
3634
}
37-
request = new fetchAPI.Request(request.url, {
38-
body: newBody,
39-
cache: request.cache,
40-
credentials: request.credentials,
41-
headers: request.headers,
42-
integrity: request.integrity,
43-
keepalive: request.keepalive,
44-
method: request.method,
45-
mode: request.mode,
46-
redirect: request.redirect,
47-
referrer: request.referrer,
48-
referrerPolicy: request.referrerPolicy,
49-
signal: request.signal,
50-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
51-
// @ts-ignore - not in the TS types yet
52-
duplex: 'half',
53-
});
54-
setRequest(request);
35+
setRequest(
36+
new fetchAPI.Request(request.url, {
37+
body: newBody,
38+
cache: request.cache,
39+
credentials: request.credentials,
40+
headers: request.headers,
41+
integrity: request.integrity,
42+
keepalive: request.keepalive,
43+
method: request.method,
44+
mode: request.mode,
45+
redirect: request.redirect,
46+
referrer: request.referrer,
47+
referrerPolicy: request.referrerPolicy,
48+
signal: request.signal,
49+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
50+
// @ts-ignore - not in the TS types yet
51+
duplex: 'half',
52+
}),
53+
);
5554
}
5655
}
56+
},
57+
onResponse({ request, response, setResponse, fetchAPI }) {
5758
const acceptEncoding = request.headers.get('accept-encoding');
5859
if (acceptEncoding) {
59-
encodingMap.set(request, acceptEncoding.split(','));
60-
}
61-
},
62-
onResponse({ request, response, setResponse, fetchAPI, serverContext }) {
63-
// Hack for avoiding to create whatwg-node to create a readable stream until it's needed
64-
if ((response as any)['bodyInit'] || response.body) {
65-
const encodings = encodingMap.get(request);
66-
if (encodings) {
60+
const encodings = acceptEncoding.split(',') as CompressionFormat[];
61+
if (encodings.length && response.body) {
6762
const supportedEncoding = encodings.find(encoding =>
68-
getSupportedEncodings(fetchAPI).includes(encoding as CompressionFormat),
63+
getSupportedEncodings(fetchAPI).includes(encoding),
6964
);
7065
if (supportedEncoding) {
7166
const compressionStream = new fetchAPI.CompressionStream(
7267
supportedEncoding as CompressionFormat,
7368
);
74-
// To calculate final content-length
75-
const contentLength = response.headers.get('content-length');
76-
if (contentLength) {
77-
const bufOfRes = (response as any)._buffer;
78-
if (bufOfRes) {
79-
const writer = compressionStream.writable.getWriter();
80-
const write$ = writer.write(bufOfRes);
81-
serverContext.waitUntil?.(write$);
82-
const close$ = writer.close();
83-
serverContext.waitUntil?.(close$);
84-
const uint8Arrays$ = isReadable((compressionStream.readable as any)['readable'])
85-
? collectReadableValues((compressionStream.readable as any)['readable'])
86-
: isAsyncIterable(compressionStream.readable)
87-
? collectAsyncIterableValues(compressionStream.readable)
88-
: collectReadableStreamValues(compressionStream.readable);
89-
return uint8Arrays$.then(uint8Arrays => {
90-
const chunks = uint8Arrays.flatMap(uint8Array => [...uint8Array]);
91-
const uint8Array = new Uint8Array(chunks);
92-
const newHeaders = new fetchAPI.Headers(response.headers);
93-
newHeaders.set('content-encoding', supportedEncoding);
94-
newHeaders.set('content-length', uint8Array.byteLength.toString());
95-
const compressedResponse = new fetchAPI.Response(uint8Array, {
96-
...response,
97-
headers: newHeaders,
98-
});
99-
decompressedResponseMap.set(compressedResponse, response);
100-
setResponse(compressedResponse);
101-
const close$ = compressionStream.writable.close();
102-
serverContext.waitUntil?.(close$);
103-
});
104-
}
105-
}
10669
const newHeaders = new fetchAPI.Headers(response.headers);
10770
newHeaders.set('content-encoding', supportedEncoding);
10871
newHeaders.delete('content-length');
109-
const compressedBody = response.body!.pipeThrough(compressionStream);
72+
const compressedBody = response.body.pipeThrough(compressionStream);
11073
const compressedResponse = new fetchAPI.Response(compressedBody, {
11174
status: response.status,
11275
statusText: response.statusText,
@@ -120,35 +83,3 @@ export function useContentEncoding<TServerContext>(): ServerAdapterPlugin<TServe
12083
},
12184
};
12285
}
123-
124-
function collectReadableValues<T>(readable: Readable): Promise<T[]> {
125-
const values: T[] = [];
126-
readable.on('data', value => values.push(value));
127-
return new Promise((resolve, reject) => {
128-
readable.once('end', () => resolve(values));
129-
readable.once('error', reject);
130-
});
131-
}
132-
133-
async function collectAsyncIterableValues<T>(asyncIterable: AsyncIterable<T>): Promise<T[]> {
134-
const values: T[] = [];
135-
for await (const value of asyncIterable) {
136-
values.push(value);
137-
}
138-
return values;
139-
}
140-
141-
async function collectReadableStreamValues<T>(readableStream: ReadableStream<T>): Promise<T[]> {
142-
const reader = readableStream.getReader();
143-
const values: T[] = [];
144-
while (true) {
145-
const { done, value } = await reader.read();
146-
if (done) {
147-
reader.releaseLock();
148-
break;
149-
} else if (value) {
150-
values.push(value);
151-
}
152-
}
153-
return values;
154-
}

0 commit comments

Comments
 (0)