From 3fa8b6756114bf2b0ebac0f9498808dc20743ed7 Mon Sep 17 00:00:00 2001 From: Vspcoderz <165260098+vspcoderz@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:17:42 +0530 Subject: [PATCH 1/3] Update user name from 'Ada Lovelace' to 'Ada Wong' --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 32aa861..d1b2752 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ const db = { async getUser(id) { return { id, - name: "Ada Lovelace", + name: "Ada wong", role: "admin", }; }, From 9f1e96d176e9d4ccf6c1699ca368eb7410983687 Mon Sep 17 00:00:00 2001 From: "Nadhi-(Kushi)" Date: Sun, 29 Mar 2026 12:45:15 +0530 Subject: [PATCH 2/3] chore: better dev tools (auto comment routes) --- src/dev/comments.js | 298 +++++++++++++++++++++++++++++++++++ src/index.d.ts | 16 +- src/index.js | 376 +++++++++++++++++++++++++++++++++----------- test/app.ts | 36 ++++- 4 files changed, 630 insertions(+), 96 deletions(-) create mode 100644 src/dev/comments.js diff --git a/src/dev/comments.js b/src/dev/comments.js new file mode 100644 index 0000000..c627cab --- /dev/null +++ b/src/dev/comments.js @@ -0,0 +1,298 @@ +import { readFileSync, writeFileSync } from "node:fs"; + +const COMMENT_PREFIX = "[http-native optimization]"; +const STATUS_DESCRIPTIONS = { + "static-fast-path": + "This route is served by the static fast path and avoids generic bridge dispatch.", + "bridge-dispatch": + "This route currently runs through bridge dispatch because it depends on runtime request data. ", + "runtime-cache-tracking": + "Runtime stability is being tracked to determine whether response caching is safe.", + "runtime-cache-promoted": + "This route has been promoted to runtime response cache after stable output was observed.", +}; + +export function createRouteDevCommentWriter(options = {}) { + if (options?.devComments !== true) { + return null; + } + + const applied = new Set(); + const initializedRoutes = new Set(); + const fileState = new Map(); + const touchedFiles = new Set(); + + return { + markRoute(route, status) { + const sourceLocation = route?.sourceLocation; + if (!sourceLocation?.filePath || !Number.isFinite(sourceLocation?.line)) { + return; + } + + const normalizedStatus = String(status || "optimized").trim(); + if (!normalizedStatus) { + return; + } + + const dedupeKey = `${sourceLocation.filePath}:${sourceLocation.line}:${normalizedStatus}`; + if (applied.has(dedupeKey)) { + return; + } + applied.add(dedupeKey); + touchedFiles.add(sourceLocation.filePath); + + const routeKey = `${sourceLocation.filePath}:${sourceLocation.line}`; + const replaceExisting = initializedRoutes.has(routeKey) === false; + initializedRoutes.add(routeKey); + + annotateFile( + fileState, + sourceLocation.filePath, + sourceLocation.line, + normalizedStatus, + { replaceExisting }, + ); + }, + + cleanup() { + for (const filePath of touchedFiles) { + removeGeneratedOptimizationComments(filePath); + } + }, + }; +} + +function removeGeneratedOptimizationComments(filePath) { + let fileText; + try { + fileText = readFileSync(filePath, "utf8"); + } catch { + return; + } + + const eol = fileText.includes("\r\n") ? "\r\n" : "\n"; + const lines = fileText.split(/\r?\n/); + const output = []; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + + if (trimmed.startsWith("//") && trimmed.includes(COMMENT_PREFIX)) { + continue; + } + + if (trimmed === "/**") { + let endIndex = index; + while (endIndex < lines.length && lines[endIndex].trim() !== "*/") { + endIndex += 1; + } + if (endIndex < lines.length) { + const blockLines = lines.slice(index, endIndex + 1); + const isOptimizationBlock = blockLines.some((entry) => entry.includes(COMMENT_PREFIX)); + if (isOptimizationBlock) { + index = endIndex; + continue; + } + } + } + + output.push(line); + } + + const compacted = compactExtraBlankLines(output); + if (compacted.join(eol) !== lines.join(eol)) { + persist(filePath, compacted, eol); + } +} + +function compactExtraBlankLines(lines) { + const result = []; + let previousBlank = false; + + for (const line of lines) { + const isBlank = line.trim() === ""; + if (isBlank && previousBlank) { + continue; + } + result.push(line); + previousBlank = isBlank; + } + + return result; +} + +function annotateFile(fileState, filePath, originalLine, status, options = {}) { + let fileText; + try { + fileText = readFileSync(filePath, "utf8"); + } catch { + return; + } + + const eol = fileText.includes("\r\n") ? "\r\n" : "\n"; + const lines = fileText.split(/\r?\n/); + const state = getFileState(fileState, filePath); + + const effectiveLine = originalLine + countInsertedBefore(state.insertedAtOriginalLine, originalLine); + const targetIndex = effectiveLine - 1; + if (targetIndex < 0 || targetIndex >= lines.length) { + return; + } + + const routeLine = lines[targetIndex] ?? ""; + const indent = routeLine.match(/^\s*/)?.[0] ?? ""; + const existingBlock = findExistingOptimizationBlock(lines, targetIndex, indent); + const replaceExisting = options.replaceExisting === true; + + if (existingBlock) { + const mergedStatuses = replaceExisting + ? [status] + : mergeStatuses(existingBlock.statuses, status); + const replacement = buildOptimizationBlock(indent, mergedStatuses); + lines.splice( + existingBlock.startIndex, + existingBlock.endIndex - existingBlock.startIndex + 1, + ...replacement, + ); + persist(filePath, lines, eol); + return; + } + + const singleLineIndex = targetIndex - 1; + const singleLineStatuses = parseSingleLineComment(lines[singleLineIndex], indent); + if (singleLineStatuses) { + const mergedStatuses = replaceExisting + ? [status] + : mergeStatuses(singleLineStatuses, status); + const replacement = buildOptimizationBlock(indent, mergedStatuses); + lines.splice(singleLineIndex, 1, ...replacement); + state.insertedAtOriginalLine.push({ line: originalLine, count: replacement.length - 1 }); + state.insertedAtOriginalLine.sort((left, right) => left.line - right.line); + persist(filePath, lines, eol); + return; + } + + const block = buildOptimizationBlock(indent, [status]); + lines.splice(targetIndex, 0, ...block); + state.insertedAtOriginalLine.push({ line: originalLine, count: block.length }); + state.insertedAtOriginalLine.sort((left, right) => left.line - right.line); + persist(filePath, lines, eol); +} + +function findExistingOptimizationBlock(lines, targetIndex, indent) { + let cursor = targetIndex - 1; + + while (cursor >= 0 && lines[cursor].trim() === "") { + cursor -= 1; + } + + if (cursor < 0 || lines[cursor].trim() !== "*/") { + return null; + } + + const endIndex = cursor; + let startIndex = endIndex; + while (startIndex >= 0 && lines[startIndex].trim() !== "/**") { + startIndex -= 1; + } + + if (startIndex < 0) { + return null; + } + + const blockLines = lines.slice(startIndex, endIndex + 1); + const hasPrefix = blockLines.some((line) => line.includes(COMMENT_PREFIX)); + if (!hasPrefix) { + return null; + } + + const statuses = parseStatusesFromLines(blockLines, indent); + return { + startIndex, + endIndex, + statuses, + }; +} + +function parseSingleLineComment(line, indent) { + if (typeof line !== "string") { + return null; + } + + const prefix = `${indent}// ${COMMENT_PREFIX}`; + if (!line.startsWith(prefix)) { + return null; + } + + return line + .slice(prefix.length) + .split("|") + .map((item) => item.trim()) + .filter(Boolean); +} + +function parseStatusesFromLines(lines) { + const firstLineWithPrefix = lines.find((line) => line.includes(COMMENT_PREFIX)); + if (!firstLineWithPrefix) { + return []; + } + + return firstLineWithPrefix + .slice(firstLineWithPrefix.indexOf(COMMENT_PREFIX) + COMMENT_PREFIX.length) + .split("|") + .map((item) => item.trim()) + .filter(Boolean); +} + +function buildOptimizationBlock(indent, statuses) { + const dedupedStatuses = [...new Set(statuses.map((value) => String(value).trim()).filter(Boolean))]; + const description = dedupedStatuses + .map((value) => STATUS_DESCRIPTIONS[value] || `Status '${value}' is active for this route.`) + .join(" "); + + return [ + `${indent}/**`, + `${indent} * ${COMMENT_PREFIX} ${dedupedStatuses.join(" | ")}`, + `${indent} * ${description}`, + `${indent} */`, + ]; +} + +function mergeStatuses(existingStatuses, status) { + const result = [...existingStatuses]; + if (!result.includes(status)) { + result.push(status); + } + return result; +} + +function persist(filePath, lines, eol) { + try { + writeFileSync(filePath, lines.join(eol), "utf8"); + } catch (error) { + if (process.env.NODE_ENV !== "production") { + console.warn("[http-native] failed to write dev route comments:", error?.message || error); + } + } +} + +function getFileState(fileState, filePath) { + if (!fileState.has(filePath)) { + fileState.set(filePath, { + insertedAtOriginalLine: [], + }); + } + + return fileState.get(filePath); +} + +function countInsertedBefore(insertedAtOriginalLine, originalLine) { + let count = 0; + for (const entry of insertedAtOriginalLine) { + if (entry.line <= originalLine) { + count += entry.count; + } + } + return count; +} diff --git a/src/index.d.ts b/src/index.d.ts index 57ca451..813623e 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -106,6 +106,8 @@ export interface RuntimeOptimizationOptions { notifyIntervalMs?: number; /** Enable runtime response cache promotion for deterministic routes */ cache?: boolean; + /** Write dev comments above route declarations with optimization flags (default: true) */ + devComments?: boolean; } export interface ListenOptions { @@ -149,6 +151,18 @@ export interface ServerHandle { close(): void; } +export interface ListenHandle extends Promise { + /** Override port before the server starts */ + port(value: number): ListenHandle; + + /** + * Override runtime optimization options before the server starts. + * + * Supports: await app.listen({ port }).opt({ notify: true }) + */ + opt(options?: RuntimeOptimizationOptions): ListenHandle; +} + export interface OptimizationSnapshot { routes: RouteOptimizationInfo[]; } @@ -201,7 +215,7 @@ export interface Application { all(path: string, handler: RouteHandler): Application; /** Start the server and listen for connections */ - listen(options?: ListenOptions): Promise; + listen(options?: ListenOptions): ListenHandle; } /** Create a new http-native application */ diff --git a/src/index.js b/src/index.js index 7d9cd5d..f3eeadb 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,7 @@ import { Buffer } from "node:buffer"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { analyzeRequestAccess, @@ -14,6 +17,8 @@ import { loadNativeModule } from "./native.js"; import defaultHttpServerConfig, { normalizeHttpServerConfig, } from "./http-server.config.js"; +import { buildRouteEntry } from "./opt/entry.js"; +import { createRouteDevCommentWriter } from "./dev/comments.js"; import { createRuntimeOptimizer } from "./opt/runtime.js"; const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]; @@ -112,6 +117,7 @@ const RESPONSE_POOL_MAX = 512; const responseStatePool = []; const responseObjectPool = []; const DEFAULT_JSON_SERIALIZER = createJsonSerializer("fallback"); +const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url); /** * Acquire a response-state object from the pool, or allocate a fresh one. @@ -409,7 +415,12 @@ function isPromiseLike(value) { // ─── Dispatcher ───────────────────────── -function createDispatcher(compiledRoutes, runtimeOptimizer, errorHandlers = []) { +function createDispatcher( + compiledRoutes, + runtimeOptimizer, + errorHandlers = [], + devRouteCommentWriter = null, +) { const routesById = new Map(compiledRoutes.map((route) => [route.handlerId, route])); const errorRequestFactory = createRequestFactory(ERROR_REQUEST_PLAN, [], null); @@ -497,7 +508,12 @@ function createDispatcher(compiledRoutes, runtimeOptimizer, errorHandlers = []) const responseSnapshot = snapshot(); runtimeOptimizer?.recordDispatch(route, req, responseSnapshot); const encoded = encodeResponseEnvelope(responseSnapshot); - maybePromoteRouteResponseCache(route, responseSnapshot, encoded); + maybePromoteRouteResponseCache( + route, + responseSnapshot, + encoded, + devRouteCommentWriter, + ); releaseRequestObject(req); release(); return encoded; @@ -531,9 +547,60 @@ function normalizeRouteRegistration(method, path, handler, options = {}) { path: normalizeRoutePath(method, path), handler, cache: options.cache || null, + sourceLocation: options.sourceLocation ?? null, }; } +function captureRouteRegistrationLocation() { + const stack = new Error().stack; + if (!stack) { + return null; + } + + const stackLines = stack.split("\n").slice(1); + for (const stackLine of stackLines) { + const line = stackLine.trim(); + if (!line || line.includes("node:internal") || line.includes("internal/")) { + continue; + } + + const match = line.match(/\(?((?:file:\/\/)?[^)\s]+):(\d+):(\d+)\)?/); + if (!match) { + continue; + } + + const [, rawFilePath, rawLine, rawColumn] = match; + let filePath; + try { + filePath = rawFilePath.startsWith("file://") + ? fileURLToPath(rawFilePath) + : rawFilePath; + } catch { + continue; + } + + if (!path.isAbsolute(filePath)) { + filePath = path.resolve(process.cwd(), filePath); + } + + if (!existsSync(filePath)) { + continue; + } + + if (!filePath || filePath === CURRENT_MODULE_PATH) { + continue; + } + + return { + filePath, + line: Number(rawLine), + column: Number(rawColumn), + }; + } + + return null; +} + function compileMiddlewareRegistration(middleware) { const handlerSource = Function.prototype.toString.call(middleware.handler); @@ -591,7 +658,7 @@ function hasNoRequestAccess(plan) { ); } -function maybePromoteRouteResponseCache(route, snapshot, encoded) { +function maybePromoteRouteResponseCache(route, snapshot, encoded, devRouteCommentWriter = null) { const cache = route.runtimeResponseCache; if (!cache || cache.encoded) { return; @@ -607,6 +674,7 @@ function maybePromoteRouteResponseCache(route, snapshot, encoded) { if (cache.stableHits >= ROUTE_CACHE_PROMOTE_HITS) { cache.encoded = encoded; + devRouteCommentWriter?.markRoute(route, "runtime-cache-promoted"); } } @@ -711,16 +779,21 @@ function createMethodRegistrar(app, method) { handler = maybeHandler; } + const sourceLocation = captureRouteRegistrationLocation(); + const routeOptions = sourceLocation + ? { ...options, sourceLocation } + : options; + if (method === "ALL") { for (const concreteMethod of HTTP_METHODS) { app._routes.push( - normalizeRouteRegistration(concreteMethod, path, handler, options), + normalizeRouteRegistration(concreteMethod, path, handler, routeOptions), ); } return app; } - app._routes.push(normalizeRouteRegistration(method, path, handler, options)); + app._routes.push(normalizeRouteRegistration(method, path, handler, routeOptions)); return app; }; } @@ -729,6 +802,14 @@ function normalizeListenOptions(options = {}) { const serverConfig = normalizeHttpServerConfig( options.serverConfig ?? options.httpServerConfig ?? defaultHttpServerConfig, ); + const optionOpt = options.opt ?? null; + const normalizedOpt = { + notify: optionOpt?.notify ?? true, + notifyIntervalMs: optionOpt?.notifyIntervalMs, + cache: optionOpt?.cache, + devComments: + optionOpt?.devComments ?? process.env.HTTP_NATIVE_DEV_COMMENTS !== "0", + }; return { host: options.host ?? serverConfig.defaultHost, @@ -737,11 +818,33 @@ function normalizeListenOptions(options = {}) { options.backlog === undefined || options.backlog === null ? serverConfig.defaultBacklog : Number(options.backlog), - opt: options.opt ?? {}, + opt: normalizedOpt, serverConfig, }; } +function registerDevCommentProcessCleanup(devRouteCommentWriter) { + if (!devRouteCommentWriter?.cleanup) { + return () => undefined; + } + + const cleanup = () => { + devRouteCommentWriter.cleanup(); + }; + + /** + * implmented for now + * but it shouldn't work for now cuz its hard to debug then + */ + process.once("beforeExit", cleanup); + process.once("exit", cleanup); + + return () => { + process.off("beforeExit", cleanup); + process.off("exit", cleanup); + }; +} + // ─── Application Factory ─────────────── /** @@ -796,102 +899,195 @@ export function createApp() { options: undefined, all: undefined, - async listen(options = {}) { - const normalizedOptions = normalizeListenOptions(options); - const compiledMiddlewares = this._middlewares.map(compileMiddlewareRegistration); - const errorHandlerPlans = this._errorHandlers.map((handler) => - analyzeRequestAccess(Function.prototype.toString.call(handler)), - ); + listen(options = {}) { + const startServer = async (listenOptions = options) => { + const normalizedOptions = normalizeListenOptions(listenOptions); + const compiledMiddlewares = this._middlewares.map(compileMiddlewareRegistration); + const errorHandlerPlans = this._errorHandlers.map((handler) => + analyzeRequestAccess(Function.prototype.toString.call(handler)), + ); - const routes = this._routes.map((route) => { - const handlerSource = Function.prototype.toString.call(route.handler); + const routes = this._routes.map((route) => { + const handlerSource = Function.prototype.toString.call(route.handler); + + return { + ...route, + handlerId: nextHandlerId++, + handlerSource, + accessPlan: analyzeRequestAccess(handlerSource), + ...compileRouteShape(route.method, route.path), + }; + }); + const compiledRoutes = routes.map((route) => + compileRouteDispatch( + route, + compiledMiddlewares, + errorHandlerPlans, + normalizedOptions.opt, + ), + ); - return { - ...route, - handlerId: nextHandlerId++, - handlerSource, - accessPlan: analyzeRequestAccess(handlerSource), - ...compileRouteShape(route.method, route.path), + const manifest = { + version: 1, + serverConfig: normalizedOptions.serverConfig, + middlewares: compiledMiddlewares.map((middleware) => ({ + pathPrefix: middleware.pathPrefix, + })), + routes: compiledRoutes.map((route) => ({ + method: route.method, + methodCode: route.methodCode, + path: route.path, + routeKind: route.routeKind, + handlerId: route.handlerId, + handlerSource: route.handlerSource, + paramNames: route.paramNames, + segmentCount: route.segmentCount, + headerKeys: [...route.requestPlan.headerKeys], + fullHeaders: route.requestPlan.fullHeaders, + needsPath: route.requestPlan.path, + needsUrl: route.requestPlan.url, + needsQuery: + route.requestPlan.fullQuery || + route.requestPlan.queryKeys.size > 0, + cache: route.cache + ? { + ttlSecs: route.cache.ttl || 60, + maxEntries: route.cache.maxEntries || 256, + varyBy: (route.cache.varyBy || []).map((key) => { + const dotIndex = key.indexOf("."); + return dotIndex >= 0 + ? { source: key.slice(0, dotIndex), name: key.slice(dotIndex + 1) } + : { source: "query", name: key }; + }), + } + : null, + })), }; - }); - const compiledRoutes = routes.map((route) => - compileRouteDispatch( - route, + + const runtimeOptimizer = createRuntimeOptimizer( + compiledRoutes, compiledMiddlewares, - errorHandlerPlans, normalizedOptions.opt, - ), - ); - - const manifest = { - version: 1, - serverConfig: normalizedOptions.serverConfig, - middlewares: compiledMiddlewares.map((middleware) => ({ - pathPrefix: middleware.pathPrefix, - })), - routes: compiledRoutes.map((route) => ({ - method: route.method, - methodCode: route.methodCode, - path: route.path, - routeKind: route.routeKind, - handlerId: route.handlerId, - handlerSource: route.handlerSource, - paramNames: route.paramNames, - segmentCount: route.segmentCount, - headerKeys: [...route.requestPlan.headerKeys], - fullHeaders: route.requestPlan.fullHeaders, - needsPath: route.requestPlan.path, - needsUrl: route.requestPlan.url, - needsQuery: - route.requestPlan.fullQuery || - route.requestPlan.queryKeys.size > 0, - cache: route.cache - ? { - ttlSecs: route.cache.ttl || 60, - maxEntries: route.cache.maxEntries || 256, - varyBy: (route.cache.varyBy || []).map((key) => { - const dotIndex = key.indexOf("."); - return dotIndex >= 0 - ? { source: key.slice(0, dotIndex), name: key.slice(dotIndex + 1) } - : { source: "query", name: key }; - }), - } - : null, - })), - }; + ); + const devRouteCommentWriter = createRouteDevCommentWriter(normalizedOptions.opt); + const detachDevCommentProcessCleanup = registerDevCommentProcessCleanup( + devRouteCommentWriter, + ); + if (devRouteCommentWriter) { + for (const route of compiledRoutes) { + const routeEntry = buildRouteEntry(route, compiledMiddlewares); + const baseStatus = + routeEntry.staticFastPath === true + ? "static-fast-path" + : "bridge-dispatch"; + devRouteCommentWriter.markRoute(route, baseStatus); + + if (route.runtimeResponseCache) { + devRouteCommentWriter.markRoute(route, "runtime-cache-tracking"); + } + } + } - const runtimeOptimizer = createRuntimeOptimizer( - compiledRoutes, - compiledMiddlewares, - normalizedOptions.opt, - ); - const dispatcher = createDispatcher(compiledRoutes, runtimeOptimizer, this._errorHandlers); - const handle = native.startServer(JSON.stringify(manifest), dispatcher, { - host: normalizedOptions.host, - port: normalizedOptions.port, - backlog: normalizedOptions.backlog, - }); - ACTIVE_NATIVE_SERVERS.add(handle); + const dispatcher = createDispatcher( + compiledRoutes, + runtimeOptimizer, + this._errorHandlers, + devRouteCommentWriter, + ); + const handle = native.startServer(JSON.stringify(manifest), dispatcher, { + host: normalizedOptions.host, + port: normalizedOptions.port, + backlog: normalizedOptions.backlog, + }); + ACTIVE_NATIVE_SERVERS.add(handle); - return { - host: handle.host, - port: handle.port, - url: handle.url, - _handle: handle, - optimizations: { - snapshot() { - return runtimeOptimizer.snapshot(); + return { + host: handle.host, + port: handle.port, + url: handle.url, + _handle: handle, + optimizations: { + snapshot() { + return runtimeOptimizer.snapshot(); + }, + summary() { + return runtimeOptimizer.summary(); + }, }, - summary() { - return runtimeOptimizer.summary(); + close() { + ACTIVE_NATIVE_SERVERS.delete(handle); + runtimeOptimizer?.dispose?.(); + devRouteCommentWriter?.cleanup?.(); + detachDevCommentProcessCleanup(); + return handle.close(); }, + }; + }; + + let selectedPort = options.port; + let selectedOpt = options.opt; + let startPromise = null; + + const resolveOptions = () => { + if (selectedPort === undefined && selectedOpt === undefined) { + return options; + } + + return { + ...options, + ...(selectedPort === undefined ? {} : { port: selectedPort }), + opt: selectedOpt, + }; + }; + + const start = () => { + if (!startPromise) { + startPromise = startServer(resolveOptions()); + } + return startPromise; + }; + + const chainableListen = { + port(value) { + if (startPromise) { + return startPromise; + } + + selectedPort = Number(value); + return chainableListen; }, - close() { - ACTIVE_NATIVE_SERVERS.delete(handle); - runtimeOptimizer?.dispose?.(); - return handle.close(); + opt(optOptions = {}) { + if (startPromise) { + return startPromise; + } + + const safeOptOptions = + optOptions && typeof optOptions === "object" ? optOptions : {}; + + selectedOpt = { + ...(selectedOpt ?? {}), + ...safeOptOptions, + }; + + return chainableListen; + }, + then(onFulfilled, onRejected) { + return start().then(onFulfilled, onRejected); + }, + catch(onRejected) { + return start().catch(onRejected); + }, + finally(onFinally) { + return start().finally(onFinally); }, }; + + // Preserve previous behavior by starting even when callers don't await. + queueMicrotask(() => { + void start(); + }); + + return chainableListen; }, }; diff --git a/test/app.ts b/test/app.ts index b32a97e..490b994 100644 --- a/test/app.ts +++ b/test/app.ts @@ -2,21 +2,47 @@ import { createApp } from "../src/index.js"; let app = createApp(); +const db: any = { + getUser: async (id: number) => { + await Promise.resolve(); + return { + id, + name: "Alice" + } + } +} + app.error(async (error, req, res) => { await Promise.resolve(); console.error("Error", error, req, res); }); + +/** + * [http-native optimization] static-fast-path + * This route is served by the static fast path and avoids generic bridge dispatch. + */ app.get("/", (req, res) => { - res.status(200).json({ + res.json({ ok: true, - data: req.query, }); }); -const server = await app.listen({ - port: 8190, - opt: { notify: true} + +/** + * [http-native optimization] bridge-dispatch + * This route currently runs through bridge dispatch because it depends on runtime request data. + */ +app.get("/url", async (req, res) => { + const data = await db.getUser(Math.floor(Math.random() * 1000) + 1); + res.status(200).json({ + ok: true, + data: data + }); }); +const server = await app.listen().port(8190).opt({ devComments: true}); + + + console.log(`http-native listening on ${server.url}`); From 2dcaa9a3e32a3d2705940e2f3025d0fc7ce8ca1d Mon Sep 17 00:00:00 2001 From: "Nadhi-(Kushi)" Date: Sun, 29 Mar 2026 13:07:29 +0530 Subject: [PATCH 3/3] chore: added groups --- src/index.d.ts | 3 +++ src/index.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 813623e..5190130 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -193,6 +193,9 @@ export interface Application { /** Register a global error handler */ onError(handler: ErrorHandler): Application; + /** Group routes under a shared path prefix */ + group(pathPrefix: string, registerGroup: (group: Application) => void): Application; + /** Register a GET route handler */ get(path: string, handler: RouteHandler): Application; diff --git a/src/index.js b/src/index.js index f3eeadb..5c886a4 100644 --- a/src/index.js +++ b/src/index.js @@ -91,6 +91,30 @@ function pathPrefixMatches(pathPrefix, requestPath) { return requestPath === pathPrefix || requestPath.startsWith(`${pathPrefix}/`); } +function combinePathPrefixes(basePrefix, nextPrefix) { + if (basePrefix === "/") { + return nextPrefix; + } + + if (nextPrefix === "/") { + return basePrefix; + } + + return `${basePrefix}${nextPrefix}`; +} + +function applyGroupPrefixToRoutePath(routePath, groupPrefix) { + if (groupPrefix === "/") { + return routePath; + } + + if (routePath === "/") { + return groupPrefix; + } + + return `${groupPrefix}${routePath}`; +} + function normalizeContentType(type) { if (type.includes("/")) { return type; @@ -771,6 +795,11 @@ function createMethodRegistrar(app, method) { return (path, optionsOrHandler, maybeHandler) => { let options = {}; let handler; + const groupPrefix = app._groupPrefix ?? "/"; + const scopedPath = + typeof path === "string" + ? applyGroupPrefixToRoutePath(path, groupPrefix) + : path; if (typeof optionsOrHandler === "function") { handler = optionsOrHandler; @@ -787,13 +816,13 @@ function createMethodRegistrar(app, method) { if (method === "ALL") { for (const concreteMethod of HTTP_METHODS) { app._routes.push( - normalizeRouteRegistration(concreteMethod, path, handler, routeOptions), + normalizeRouteRegistration(concreteMethod, scopedPath, handler, routeOptions), ); } return app; } - app._routes.push(normalizeRouteRegistration(method, path, handler, routeOptions)); + app._routes.push(normalizeRouteRegistration(method, scopedPath, handler, routeOptions)); return app; }; } @@ -861,14 +890,19 @@ export function createApp() { _routes: [], _middlewares: [], _errorHandlers: [], + _groupPrefix: "/", use(pathOrMiddleware, maybeMiddleware) { let pathPrefix = "/"; let handler = pathOrMiddleware; + const groupPrefix = this._groupPrefix ?? "/"; if (typeof pathOrMiddleware === "string") { pathPrefix = normalizePathPrefix(pathOrMiddleware); + pathPrefix = combinePathPrefixes(groupPrefix, pathPrefix); handler = maybeMiddleware; + } else if (groupPrefix !== "/") { + pathPrefix = groupPrefix; } if (typeof handler !== "function") { @@ -891,6 +925,24 @@ export function createApp() { return this.onError(handler); }, + group(pathPrefix, registerGroup) { + if (typeof registerGroup !== "function") { + throw new TypeError("group(path, callback) requires a callback function"); + } + + const normalizedPrefix = normalizePathPrefix(pathPrefix); + const previousPrefix = this._groupPrefix ?? "/"; + this._groupPrefix = combinePathPrefixes(previousPrefix, normalizedPrefix); + + try { + registerGroup(this); + } finally { + this._groupPrefix = previousPrefix; + } + + return this; + }, + get: undefined, post: undefined, put: undefined,