BREAKING CHANGE(typedserver): migrate route handlers to use IRequestContext and lazy body parsers

This commit is contained in:
2025-12-20 08:11:04 +00:00
parent d5800f58b4
commit 64f8f400c2
12 changed files with 193 additions and 127 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@api.global/typedserver',
version: '7.11.1',
version: '8.0.0',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

View File

@@ -148,7 +148,7 @@ export interface IServerOptions {
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';
export interface IRouteHandler {
(request: Request): Promise<Response | null>;
(ctx: plugins.smartserve.IRequestContext): Promise<Response | null>;
}
export class TypedServer {
@@ -202,19 +202,11 @@ export class TypedServer {
* Supports Express-style path patterns like '/path/:param' and '/path/*splat'
* @param path - The route path pattern
* @param method - HTTP method (GET, POST, PUT, DELETE, PATCH, ALL)
* @param handler - Async function that receives Request and returns Response or null
* @param handler - Async function that receives IRequestContext and returns Response or null
*/
public addRoute(path: string, method: THttpMethod, handler: IRouteHandler): void {
// Delegate to smartserve's ControllerRegistry
plugins.smartserve.ControllerRegistry.addRoute(path, method, async (ctx: plugins.smartserve.IRequestContext) => {
// Convert context to Request for backwards compatibility
const request = new Request(ctx.url.toString(), {
method: ctx.method,
headers: ctx.headers,
});
(request as any).params = ctx.params;
return handler(request);
});
plugins.smartserve.ControllerRegistry.addRoute(path, method, handler);
}
/**
@@ -274,6 +266,9 @@ export class TypedServer {
});
// Register controllers with SmartServe's ControllerRegistry
// Note: @Route decorators auto-register classes at import time.
// Controllers with constructor args (like DevToolsController) use default no-op
// constructors to handle auto-instantiation gracefully.
if (this.options.injectReload) {
plugins.smartserve.ControllerRegistry.registerInstance(this.devToolsController);
}
@@ -389,10 +384,10 @@ export class TypedServer {
/**
* Create an IRequestContext from a Request
*/
private async createContext(
private createContext(
request: Request,
params: Record<string, string>
): Promise<plugins.smartserve.IRequestContext> {
): plugins.smartserve.IRequestContext {
const url = new URL(request.url);
const method = request.method.toUpperCase() as THttpMethod;
@@ -402,20 +397,14 @@ export class TypedServer {
query[key] = value;
});
// Parse body
let body: unknown = undefined;
const contentType = request.headers.get('content-type');
if (contentType?.includes('application/json')) {
try {
body = await request.clone().json();
} catch {
body = {};
}
}
// Cached body parsers (lazy evaluation)
let jsonCache: unknown;
let textCache: string;
let arrayBufferCache: ArrayBuffer;
let formDataCache: FormData;
return {
request,
body,
params,
query,
headers: request.headers,
@@ -424,6 +413,30 @@ export class TypedServer {
url,
runtime: 'node' as const,
state: {},
async json<T = unknown>(): Promise<T> {
if (jsonCache === undefined) {
jsonCache = await request.clone().json();
}
return jsonCache as T;
},
async text(): Promise<string> {
if (textCache === undefined) {
textCache = await request.clone().text();
}
return textCache;
},
async arrayBuffer(): Promise<ArrayBuffer> {
if (arrayBufferCache === undefined) {
arrayBufferCache = await request.clone().arrayBuffer();
}
return arrayBufferCache;
},
async formData(): Promise<FormData> {
if (formDataCache === undefined) {
formDataCache = await request.clone().formData();
}
return formDataCache;
},
};
}
@@ -577,7 +590,7 @@ export class TypedServer {
}
// Process the request and wrap response with all configured headers
const response = await this.handleRequestInternal(request, url, path, method);
const response = await this.handleRequestInternal(request, path, method);
return this.applyResponseHeaders(response);
}
@@ -586,7 +599,6 @@ export class TypedServer {
*/
private async handleRequestInternal(
request: Request,
url: URL,
path: string,
method: THttpMethod
): Promise<Response> {
@@ -594,7 +606,7 @@ export class TypedServer {
const match = plugins.smartserve.ControllerRegistry.matchRoute(path, method);
if (match) {
try {
const context = await this.createContext(request, match.params);
const context = this.createContext(request, match.params);
const result = await match.route.handler(context);
// Handle Response or convert to Response

View File

@@ -10,9 +10,10 @@ export class DevToolsController {
private getLastReload: () => number;
private getEnded: () => boolean;
constructor(options: { getLastReload: () => number; getEnded: () => boolean }) {
this.getLastReload = options.getLastReload;
this.getEnded = options.getEnded;
constructor(options?: { getLastReload: () => number; getEnded: () => boolean }) {
// Default no-op functions for when controller is auto-instantiated without options
this.getLastReload = options?.getLastReload ?? (() => 0);
this.getEnded = options?.getEnded ?? (() => false);
}
@plugins.smartserve.Get('/devtools')

View File

@@ -14,7 +14,8 @@ export class TypedRequestController {
@plugins.smartserve.Post('')
async handleTypedRequest(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
try {
const response = await this.typedRouter.routeAndAddResponse(ctx.body as plugins.typedrequestInterfaces.ITypedRequest);
const body = await ctx.json() as plugins.typedrequestInterfaces.ITypedRequest;
const response = await this.typedRouter.routeAndAddResponse(body);
return new Response(plugins.smartjson.stringify(response), {
status: 200,

View File

@@ -116,8 +116,8 @@ export class UtilityWebsiteServer {
this.typedserver.addRoute(
'/assetbroker/manifest/:manifestAsset',
'GET',
async (request: Request) => {
let manifestAssetName = (request as any).params?.manifestAsset;
async (ctx) => {
let manifestAssetName = ctx.params?.manifestAsset;
if (manifestAssetName === 'favicon.png') {
manifestAssetName = `favicon_${this.options.domain
.replace('.', '')