BREAKING CHANGE(typedserver): migrate route handlers to use IRequestContext and lazy body parsers
This commit is contained in:
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('.', '')
|
||||
|
||||
Reference in New Issue
Block a user