Skip to content

Try accelerator #2379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fets-2379-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fets": patch
---
dependencies updates:
- Added dependency [`json-accelerator@^0.0.1` ↗︎](https://www.npmjs.com/package/json-accelerator/v/0.0.1) (to `dependencies`)
1 change: 1 addition & 0 deletions packages/fets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@whatwg-node/fetch": "^0.10.0",
"@whatwg-node/server": "^0.9.55",
"hotscript": "^1.0.11",
"json-accelerator": "^0.0.1",
"json-schema-to-ts": "^3.0.0",
"qs": "^6.13.1",
"ts-toolbelt": "^9.6.0",
Expand Down
78 changes: 77 additions & 1 deletion packages/fets/src/Response.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
import { Response as OriginalResponse } from '@whatwg-node/fetch';
import { fakePromise } from '@whatwg-node/server';
import { StatusCode, TypedResponse, TypedResponseCtor } from './typed-fetch.js';
import { JSONSerializer } from './types.js';

export const LAZY_SERIALIZED_RESPONSE = Symbol('LAZY_SERIALIZED_RESPONSE');
export const defaultSerializer: JSONSerializer = obj => JSON.stringify(obj);
export interface LazySerializedResponse {
[LAZY_SERIALIZED_RESPONSE]: true;
resolveWithSerializer(serializer: JSONSerializer): void;
init?: ResponseInit;
actualResponse: Response;
jsonObj: any;
json: () => Promise<any>;
status: StatusCode;
headers: Headers;
}
export function isLazySerializedResponse(response: any): response is LazySerializedResponse {
return response[LAZY_SERIALIZED_RESPONSE];
}
function isHeadersLike(headers: any): headers is Headers {
return headers?.get && headers?.forEach;
}
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
function getHeadersFromHeadersInit(init?: HeadersInit): Headers {
let headers: Headers;
if (isHeadersLike(init)) {
headers = init;
} else {
headers = new Headers(init);
}
if (!headers.has('content-type')) {
headers.set('content-type', JSON_CONTENT_TYPE);
}
return headers;
}
export function createLazySerializedResponse(
jsonObj: any,
init: ResponseInit = {},
): LazySerializedResponse {
let actualResponse: Response;
let headers: Headers;
function getHeaders() {
if (headers == null) {
headers = getHeadersFromHeadersInit(init.headers);
}
return headers;
}
return {
jsonObj,
get actualResponse() {
return actualResponse;
},
[LAZY_SERIALIZED_RESPONSE]: true,
init,
resolveWithSerializer(serializer: JSONSerializer) {
const serialized = serializer(jsonObj);
init.headers = getHeaders();
actualResponse = new OriginalResponse(serialized, init) as Response;
},
json() {
return fakePromise(jsonObj);
},
get status() {
return (init?.status || 200) as StatusCode;
},
get headers() {
return getHeaders();
},
};
}

// This allows us to hook into serialization of the response body
/**
Expand All @@ -12,7 +81,14 @@ import { StatusCode, TypedResponse, TypedResponseCtor } from './typed-fetch.js';
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Response
*/
export const Response = OriginalResponse as TypedResponseCtor;
export const Response = new Proxy(OriginalResponse, {
get(OriginalResponse, prop, receiver) {
if (prop === 'json') {
return createLazySerializedResponse;
}
return Reflect.get(OriginalResponse, prop, receiver);
},
}) as TypedResponseCtor;

export type Response<
TJSON = any,
Expand Down
63 changes: 59 additions & 4 deletions packages/fets/src/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ import { useDefineRoutes } from './plugins/define-routes.js';
import { useOpenAPI } from './plugins/openapi.js';
import { useTypeBox } from './plugins/typebox.js';
import { EMPTY_OBJECT } from './plugins/utils.js';
import { isLazySerializedResponse } from './Response.js';
import { HTTPMethod, TypedRequest, TypedResponse } from './typed-fetch.js';
import type {
OnRouteHandleHook,
OnRouteHook,
OnRouterInitHook,
OnSerializeResponseHook,
OpenAPIDocument,
OpenAPIInfo,
Router,
RouterBaseObject,
RouterComponentsBase,
RouterOptions,
RouterPlugin,
RouterRequest,
RouterSDK,
RouteSchemas,
RouteWithSchemasOpts,
Expand All @@ -44,6 +47,7 @@ export function createRouterBase(
const __onRouterInitHooks: OnRouterInitHook<any>[] = [];
const onRouteHooks: OnRouteHook<any>[] = [];
const onRouteHandleHooks: OnRouteHandleHook<any, RouterComponentsBase>[] = [];
const onSerializeResponseHooks: OnSerializeResponseHook<any>[] = [];
for (const plugin of plugins) {
if (plugin.onRouterInit) {
__onRouterInitHooks.push(plugin.onRouterInit);
Expand All @@ -54,6 +58,9 @@ export function createRouterBase(
if (plugin.onRouteHandle) {
onRouteHandleHooks.push(plugin.onRouteHandle);
}
if (plugin.onSerializeResponse) {
onSerializeResponseHooks.push(plugin.onSerializeResponse);
}
}
const routeByPatternByMethod = new Map<
HTTPMethod,
Expand Down Expand Up @@ -104,6 +111,36 @@ export function createRouterBase(
return undefined as any;
}

interface ProcessHandlerResultOpts {
routerRequest: RouterRequest;
context: any;
path: string;
}

function processHandlerResult(
handlerResult: TypedResponse | Response | undefined,
opts: ProcessHandlerResultOpts,
) {
if (handlerResult) {
if (isLazySerializedResponse(handlerResult)) {
const onSerializeResponseHookPayload = {
request: opts.routerRequest,
path: opts.path,
lazyResponse: handlerResult,
serverContext: opts.context,
};
for (const onSerializeResponseHook of onSerializeResponseHooks) {
onSerializeResponseHook(onSerializeResponseHookPayload);
}
return (
handlerResult.actualResponse ||
fetchAPI.Response.json(handlerResult.jsonObj, handlerResult.init)
);
}
return handlerResult;
}
}

return {
openAPIDocument,
handle(request: Request, context: any) {
Expand Down Expand Up @@ -151,18 +188,27 @@ export function createRouterBase(
if (isPromise(handlerResult$)) {
return handlerResult$.then(handlerResult => {
if (handlerResult) {
return handlerResult as Response;
return processHandlerResult(handlerResult, {
routerRequest: request as any,
context,
path: route.path,
});
}
return handleUnhandledRoute(request.url);
});
}
if (handlerResult$) {
return handlerResult$ as Response;
return processHandlerResult(handlerResult$, {
routerRequest: request as any,
context,
path: route.path,
});
}
}
}
const methodPatternMaps = routeByPatternByMethod.get(request.method as HTTPMethod);
if (methodPatternMaps) {
let path: string;
const patternHandlerResult$ = asyncIterationUntilReturn(
methodPatternMaps.entries(),
([pattern, route]) => {
Expand Down Expand Up @@ -196,6 +242,7 @@ export function createRouterBase(
request,
});
}
path = route.path;
// @ts-expect-error - We know it's a TypedRequest
return route.handler(request, context);
}
Expand All @@ -204,13 +251,21 @@ export function createRouterBase(
if (isPromise(patternHandlerResult$)) {
return patternHandlerResult$.then(patternHandlerResult => {
if (patternHandlerResult) {
return patternHandlerResult as Response;
return processHandlerResult(patternHandlerResult, {
routerRequest: request as any,
context,
path,
});
}
return handleUnhandledRoute(request.url);
});
}
if (patternHandlerResult$) {
return patternHandlerResult$ as Response;
return processHandlerResult(patternHandlerResult$, {
routerRequest: request as any,
context,
path: path!,
});
}
}
return handleUnhandledRoute(request.url);
Expand Down
31 changes: 30 additions & 1 deletion packages/fets/src/plugins/typebox.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createAccelerator } from 'json-accelerator';
import { TypeGuard, type TSchema } from '@sinclair/typebox';
import { TypeCompiler, ValueError, ValueErrorIterator } from '@sinclair/typebox/compiler';
import { Value } from '@sinclair/typebox/value';
import { HTTPError } from '@whatwg-node/server';
import { RouterComponentsBase, RouterPlugin } from '../types.js';
import { StatusCode } from '../typed-fetch.js';
import { JSONSerializer, RouterComponentsBase, RouterPlugin, RouteSchemas } from '../types.js';
import { getHeadersObj } from './utils.js';

type ValidateFn = <T>(data: T) => ValueErrorIterator;
Expand Down Expand Up @@ -45,6 +47,8 @@ export function useTypeBox<TServerContext, TComponents extends RouterComponentsB
}
return validateFn;
}
const serializersByRequest = new WeakMap<Request, Map<number, JSONSerializer>>();
const serializerByStatusCode = new Map<RouteSchemas, Map<number, JSONSerializer>>();
return {
onRouteHandle({ route: { schemas }, request }) {
if (schemas?.request?.headers && TypeGuard.IsSchema(schemas.request.headers)) {
Expand Down Expand Up @@ -175,6 +179,31 @@ export function useTypeBox<TServerContext, TComponents extends RouterComponentsB
}),
});
}
if (schemas?.responses) {
let serializers = serializerByStatusCode.get(schemas);
if (!serializers) {
serializers = new Map();
serializerByStatusCode.set(schemas, serializers);
for (const statusCodeStr in schemas.responses) {
const statusCodeNum = Number(statusCodeStr) as StatusCode;
const schema = schemas.responses[statusCodeNum];
if (TypeGuard.IsSchema(schema)) {
const accelerator = createAccelerator(schema);
serializers.set(statusCodeNum, accelerator);
}
}
}
serializersByRequest.set(request as any, serializers);
}
},
onSerializeResponse({ request, lazyResponse }) {
const serializers = serializersByRequest.get(request as any);
if (serializers) {
const serializer = serializers.get(lazyResponse.status);
if (serializer) {
lazyResponse.resolveWithSerializer(serializer);
}
}
},
};
}
Expand Down
12 changes: 12 additions & 0 deletions packages/fets/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
import type { ClientTypedResponsePromise } from './client/clientResponse.js';
import type { ClientRequestInit } from './client/types.js';
import type { SwaggerUIOpts } from './plugins/openapi.js';
import { LazySerializedResponse } from './Response.js';
import type {
HTTPMethod,
StatusCode,
Expand Down Expand Up @@ -364,13 +365,24 @@ export type OnRouteHookPayload<TServerContext> = {

export type OnRouterInitHook<TServerContext> = (router: Router<TServerContext, any, any>) => void;

export type OnSerializeResponsePayload<TServerContext> = {
request: TypedRequest;
path: string;
serverContext: TServerContext;
lazyResponse: LazySerializedResponse;
};
export type OnSerializeResponseHook<TServerContext> = (
payload: OnSerializeResponsePayload<TServerContext>,
) => void;

export type RouterPlugin<
TServerContext,
TComponents extends RouterComponentsBase,
> = ServerAdapterPlugin<TServerContext> & {
onRouterInit?: OnRouterInitHook<TServerContext>;
onRoute?: OnRouteHook<TServerContext>;
onRouteHandle?: OnRouteHandleHook<TServerContext, TComponents>;
onSerializeResponse?: OnSerializeResponseHook<TServerContext>;
};

type ObjectSchemaWithPrimitiveProperties = JSONSchema & {
Expand Down
6 changes: 5 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9304,6 +9304,11 @@ jsesc@^3.0.2, jsesc@~3.0.2:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==

json-accelerator@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/json-accelerator/-/json-accelerator-0.0.1.tgz#12132221048e6439d0f79ace1bf4a0086382b830"
integrity sha512-Z7FXIx2bnyD+z1+gkt9s8cZGfcApC0y6E4O/n0lOpd8pe99w9b9iNZrv+AlBwSE/WfwlGSHBFNFYnoZrNqzd5g==

json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
Expand Down Expand Up @@ -14097,7 +14102,6 @@ typescript@5.7.3, typescript@^5.5.2:

"uWebSockets.js@uNetworking/uWebSockets.js#semver:^20":
version "20.51.0"
uid "6609a88ffa9a16ac5158046761356ce03250a0df"
resolved "https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/6609a88ffa9a16ac5158046761356ce03250a0df"

ufo@^1.5.4:
Expand Down