Skip to content

Commit

Permalink
Small optimizations (#318)
Browse files Browse the repository at this point in the history
* Small optimizations

* Typed Request and Response

* Go

* Changeset

* More

* 🤦
  • Loading branch information
ardatan authored Feb 7, 2023
1 parent 9ab2682 commit 390510b
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/angry-glasses-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/fetch': minor
---

Type-safe `Response.json`
6 changes: 6 additions & 0 deletions .changeset/few-apricots-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@whatwg-node/node-fetch': patch
'@whatwg-node/server': patch
---

Small optimizations
26 changes: 2 additions & 24 deletions e2e/shared-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,11 @@ export function createTestServerAdapter<TServerContext = {}>(base?: string) {
plugins: [withErrorHandling as any],
});

app.get(
'/greetings/:name',
req =>
new Response(
JSON.stringify({
message: `Hello ${req.params?.name}!`,
}),
{
headers: {
'Content-Type': 'application/json',
},
},
),
);
app.get('/greetings/:name', req => Response.json({ message: `Hello ${req.params?.name}!` }));

app.post('/bye', async req => {
const { name } = await req.json();
return new Response(
JSON.stringify({
message: `Bye ${name}!`,
}),
{
headers: {
'Content-Type': 'application/json',
},
},
);
return Response.json({ message: `Bye ${name}!` });
});

app.get(
Expand Down
11 changes: 10 additions & 1 deletion packages/fetch/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
/// <reference lib="dom" />
/// <reference types="urlpattern-polyfill" />

type TypedResponse<T> = Omit<Response, 'json'> & {
json(): Promise<T>;
}

type TypedResponseConstructor = typeof Response & {
new <T>(...args: ConstructorParameters<typeof Response>): TypedResponse<T>;
json<T>(data: T): TypedResponse<T>;
}

declare const _fetch: typeof fetch;
declare const _Request: typeof Request;
declare const _Response: typeof Response;
declare const _Response: TypedResponseConstructor;
declare const _Headers: typeof Headers;
declare const _FormData: typeof FormData;
declare const _AbortSignal: typeof AbortSignal;
Expand Down
18 changes: 18 additions & 0 deletions packages/fetch/tests/type-api-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Response } from '@whatwg-node/fetch';

let testObj = {
a: 1,
};

let anotherObj = {
b: 2,
};

const response = Response.json(anotherObj);

anotherObj = await response.json();

// @ts-expect-error - should not be assignable
testObj = await response.json();

console.log(testObj);
41 changes: 17 additions & 24 deletions packages/node-fetch/src/Headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,27 @@ export type PonyfillHeadersInit =
| Record<string, string | string[] | undefined>
| Headers;

function isHeadersLike(headers: any): headers is Headers {
return headers && typeof headers.get === 'function';
}

export class PonyfillHeaders implements Headers {
private map = new Map<string, string>();
constructor(headersInit?: PonyfillHeadersInit) {
if (headersInit != null) {
if (Array.isArray(headersInit)) {
for (const [key, value] of headersInit) {
if (Array.isArray(value)) {
for (const v of value) {
this.append(key, v);
}
} else {
this.map.set(key, value);
}
}
} else if ('get' in headersInit) {
(headersInit as Headers).forEach((value, key) => {
this.append(key, value);
this.map = new Map(headersInit);
} else if (isHeadersLike(headersInit)) {
headersInit.forEach((value, key) => {
this.map.set(key, value);
});
} else {
for (const key in headersInit) {
const value = headersInit[key];
if (Array.isArray(value)) {
for (const v of value) {
this.append(key, v);
}
} else if (value != null) {
this.set(key, value);
for (const initKey in headersInit) {
const initValue = headersInit[initKey];
if (initValue != null) {
const normalizedValue = Array.isArray(initValue) ? initValue.join(', ') : initValue;
const normalizedKey = initKey.toLowerCase();
this.map.set(normalizedKey, normalizedValue);
}
}
}
Expand All @@ -38,10 +32,9 @@ export class PonyfillHeaders implements Headers {

append(name: string, value: string): void {
const key = name.toLowerCase();
if (this.map.has(key)) {
value = this.map.get(key) + ', ' + value;
}
this.map.set(key, value);
const existingValue = this.map.get(key);
const finalValue = existingValue ? `${existingValue}, ${value}` : value;
this.map.set(key, finalValue);
}

get(name: string): string | null {
Expand Down
28 changes: 5 additions & 23 deletions packages/router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ properties:
Let's create a basic REST API that manages users.

```ts
import { createRouter } from '@whatwg-node/router'
import { createRouter, Response } from '@whatwg-node/router'

const router = createRouter()

Expand All @@ -51,13 +51,7 @@ const users = [
{ id: '2', name: 'Jane' }
]

router.get('/users', request => {
return new Response(JSON.stringify(users), {
headers: {
'Content-Type': 'application/json'
}
})
})
router.get('/users', request => Response.json(users))

// Parameters are given in the `request.params` object
router.get('/users/:id', request => {
Expand All @@ -69,11 +63,7 @@ router.get('/users/:id', request => {
})
}

return new Response(JSON.stringify(user), {
headers: {
'Content-Type': 'application/json'
}
})
return Response.json(user)
})

router.delete('/users/:id', request => {
Expand Down Expand Up @@ -103,11 +93,7 @@ router.put('/users', async request => {

users.push(user)

return new Response(JSON.stringify(user), {
headers: {
'Content-Type': 'application/json'
}
})
return Response.json(user)
})

// Handle both parameters and JSON body
Expand All @@ -124,11 +110,7 @@ router.post('/users/:id', async request => {

user.name = body.name

return new Response(JSON.stringify(user), {
headers: {
'Content-Type': 'application/json'
}
})
return Response.json(user)
})
```

Expand Down
56 changes: 45 additions & 11 deletions packages/server/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2';
import type { Http2ServerRequest, Http2ServerResponse, OutgoingHttpHeaders } from 'node:http2';
import type { Socket } from 'node:net';
import type { Readable } from 'node:stream';
import { FetchEvent } from './types';
Expand Down Expand Up @@ -175,44 +175,78 @@ function configureSocket(rawRequest: NodeRequest) {
rawRequest?.socket?.setKeepAlive?.(true);
}

function endResponse(serverResponse: NodeResponse) {
// @ts-expect-error Avoid arguments adaptor trampoline https://v8.dev/blog/adaptor-frame
serverResponse.end(null, null, null);
}

function getHeadersObj(headers: Headers): OutgoingHttpHeaders {
return new Proxy(
{},
{
get(_target, prop: string) {
return headers.get(prop);
},
set(_target, prop: string, value: string) {
headers.set(prop, value);
return true;
},
has(_target, prop: string) {
return headers.has(prop);
},
deleteProperty(_target, prop: string) {
headers.delete(prop);
return true;
},
ownKeys() {
const keys: string[] = [];
headers.forEach((_, key) => keys.push(key));
return keys;
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
},
);
}

export async function sendNodeResponse(
fetchResponse: Response,
serverResponse: NodeResponse,
nodeRequest: NodeRequest,
) {
fetchResponse.headers.forEach((value, name) => {
serverResponse.setHeader(name, value);
});
serverResponse.statusCode = fetchResponse.status;
serverResponse.statusMessage = fetchResponse.statusText;
const headersObj = getHeadersObj(fetchResponse.headers);
serverResponse.writeHead(fetchResponse.status, fetchResponse.statusText, headersObj);
// eslint-disable-next-line no-async-promise-executor
return new Promise<void>(async resolve => {
serverResponse.once('close', resolve);
// Our Node-fetch enhancements

if (
'bodyType' in fetchResponse &&
fetchResponse.bodyType != null &&
(fetchResponse.bodyType === 'String' || fetchResponse.bodyType === 'Uint8Array')
) {
// @ts-expect-error http and http2 writes are actually compatible
serverResponse.write(fetchResponse.bodyInit);
serverResponse.end();
endResponse(serverResponse);
return;
}

// Other fetch implementations
const fetchBody = fetchResponse.body;
if (fetchBody == null) {
serverResponse.end();
endResponse(serverResponse);
return;
}

if (fetchBody[Symbol.toStringTag] === 'Uint8Array') {
serverResponse
// @ts-expect-error http and http2 writes are actually compatible
.write(fetchBody);
serverResponse.end();
endResponse(serverResponse);
return;
}

Expand All @@ -236,7 +270,7 @@ export async function sendNodeResponse(
break;
}
}
serverResponse.end();
endResponse(serverResponse);
}
});
}
Expand Down

0 comments on commit 390510b

Please sign in to comment.