Skip to content
19 changes: 7 additions & 12 deletions packages/fresh/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import { trace } from "@opentelemetry/api";

import { DENO_DEPLOYMENT_ID } from "@fresh/build-id";
import * as colors from "@std/fmt/colors";
import {
type MaybeLazyMiddleware,
type Middleware,
runMiddlewares,
} from "./middlewares/mod.ts";
import type { MaybeLazyMiddleware, Middleware } from "./middlewares/mod.ts";
import { Context } from "./context.ts";
import { mergePath, type Method, UrlPatternRouter } from "./router.ts";
import type { FreshConfig, ResolvedFreshConfig } from "./config.ts";
Expand Down Expand Up @@ -387,12 +383,13 @@ export class App<State> {
}
}

const router = new UrlPatternRouter<MaybeLazyMiddleware<State>>();
const router = new UrlPatternRouter<Middleware<State>>();

const { rootMiddlewares } = applyCommands(
const { rootHandler } = applyCommands(
router,
this.#commands,
this.config.basePath,
this.#onError,
);

return async (
Expand All @@ -405,7 +402,7 @@ export class App<State> {

const method = req.method.toUpperCase() as Method;
const matched = router.match(method, url);
let { params, pattern, handlers, methodMatch } = matched;
let { params, pattern, item: handler, methodMatch } = matched;

const span = trace.getActiveSpan();
if (span && pattern) {
Expand All @@ -416,7 +413,7 @@ export class App<State> {
let next: () => Promise<Response>;

if (pattern === null || !methodMatch) {
handlers = rootMiddlewares;
handler = rootHandler;
}

if (matched.pattern !== null && !methodMatch) {
Expand All @@ -442,9 +439,7 @@ export class App<State> {
);

try {
if (handlers.length === 0) return await next();

const result = await runMiddlewares(handlers, ctx, this.#onError);
const result = await (handler !== null ? handler(ctx) : next());
if (!(result instanceof Response)) {
throw new Error(
`Expected a "Response" instance to be returned, but got: ${result}`,
Expand Down
72 changes: 41 additions & 31 deletions packages/fresh/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { setAdditionalStyles } from "./context.ts";
import { HttpError } from "./error.ts";
import { isHandlerByMethod, type PageResponse } from "./handlers.ts";
import type { MaybeLazyMiddleware, Middleware } from "./middlewares/mod.ts";
import {
compileMiddlewares,
type MaybeLazyMiddleware,
type Middleware,
} from "./middlewares/mod.ts";
import { mergePath, type Method, type Router, toRoutePath } from "./router.ts";
import {
getOrCreateSegment,
Expand Down Expand Up @@ -202,22 +206,25 @@ export type Command<State> =
| FsRouteCommand<State>;

export function applyCommands<State>(
router: Router<MaybeLazyMiddleware<State>>,
router: Router<Middleware<State>>,
commands: Command<State>[],
basePath: string,
): { rootMiddlewares: MaybeLazyMiddleware<State>[] } {
onError?: (err: unknown) => void,
): { rootHandler: Middleware<State> } {
const root = newSegment<State>("", null);

applyCommandsInner(root, router, commands, basePath);
applyCommandsInner(root, router, commands, basePath, onError);

return { rootMiddlewares: segmentToMiddlewares(root) };
const rootMiddlewares = segmentToMiddlewares(root);
return { rootHandler: compileMiddlewares(rootMiddlewares, onError) };
}

function applyCommandsInner<State>(
root: Segment<State>,
router: Router<MaybeLazyMiddleware<State>>,
router: Router<Middleware<State>>,
commands: Command<State>[],
basePath: string,
onError?: (err: unknown) => void,
) {
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
Expand Down Expand Up @@ -290,18 +297,19 @@ function applyCommandsInner<State>(
return renderRoute(ctx, def);
});

const compiled = compileMiddlewares(fns, onError);
if (config === undefined || config.methods === "ALL") {
router.add("GET", routePath, fns);
router.add("DELETE", routePath, fns);
router.add("HEAD", routePath, fns);
router.add("OPTIONS", routePath, fns);
router.add("PATCH", routePath, fns);
router.add("POST", routePath, fns);
router.add("PUT", routePath, fns);
router.add("GET", routePath, compiled);
router.add("DELETE", routePath, compiled);
router.add("HEAD", routePath, compiled);
router.add("OPTIONS", routePath, compiled);
router.add("PATCH", routePath, compiled);
router.add("POST", routePath, compiled);
router.add("PUT", routePath, compiled);
} else if (Array.isArray(config.methods)) {
for (let i = 0; i < config.methods.length; i++) {
const method = config.methods[i];
router.add(method, routePath, fns);
router.add(method, routePath, compiled);
}
}
} else {
Expand All @@ -313,17 +321,18 @@ function applyCommandsInner<State>(
false,
));

const compiled = compileMiddlewares(fns, onError);
if (typeof route.handler === "function") {
router.add("GET", routePath, fns);
router.add("DELETE", routePath, fns);
router.add("HEAD", routePath, fns);
router.add("OPTIONS", routePath, fns);
router.add("PATCH", routePath, fns);
router.add("POST", routePath, fns);
router.add("PUT", routePath, fns);
router.add("GET", routePath, compiled);
router.add("DELETE", routePath, compiled);
router.add("HEAD", routePath, compiled);
router.add("OPTIONS", routePath, compiled);
router.add("PATCH", routePath, compiled);
router.add("POST", routePath, compiled);
router.add("PUT", routePath, compiled);
} else if (isHandlerByMethod(route.handler!)) {
for (const method of Object.keys(route.handler)) {
router.add(method as Method, routePath, fns);
router.add(method as Method, routePath, compiled);
}
}
}
Expand All @@ -340,25 +349,26 @@ function applyCommandsInner<State>(

result.push(...fns);

const compiled = compileMiddlewares(result, onError);
const resPath = toRoutePath(mergePath(basePath, pattern, false));
if (method === "ALL") {
router.add("GET", resPath, result);
router.add("DELETE", resPath, result);
router.add("HEAD", resPath, result);
router.add("OPTIONS", resPath, result);
router.add("PATCH", resPath, result);
router.add("POST", resPath, result);
router.add("PUT", resPath, result);
router.add("GET", resPath, compiled);
router.add("DELETE", resPath, compiled);
router.add("HEAD", resPath, compiled);
router.add("OPTIONS", resPath, compiled);
router.add("PATCH", resPath, compiled);
router.add("POST", resPath, compiled);
router.add("PUT", resPath, compiled);
} else {
router.add(method, resPath, result);
router.add(method, resPath, compiled);
}

break;
}
case CommandType.FsRoute: {
const items = cmd.getItems();
const base = mergePath(basePath, cmd.pattern, true);
applyCommandsInner(root, router, items, base);
applyCommandsInner(root, router, items, base, onError);
break;
}
default:
Expand Down
103 changes: 58 additions & 45 deletions packages/fresh/src/middlewares/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,56 +88,69 @@ export type MaybeLazyMiddleware<State> = (
ctx: Context<State>,
) => Response | Promise<Response | Middleware<State>>;

export async function runMiddlewares<State>(
export function compileMiddlewares<State>(
middlewares: MaybeLazyMiddleware<State>[],
ctx: Context<State>,
onError?: (err: unknown) => void,
): Promise<Response> {
return await tracer.startActiveSpan("middlewares", {
attributes: { "fresh.middleware.count": middlewares.length },
}, async (span) => {
try {
let fn = ctx.next;
let i = middlewares.length;
while (i--) {
const local = fn;
let next = middlewares[i];
const idx = i;
fn = async () => {
const internals = getInternals(ctx);
const { app: prevApp, layouts: prevLayouts } = internals;
): Middleware<State> {
if (middlewares.length === 0) return (ctx) => ctx.next();

// Each step is a function that takes (ctx, tail) where tail is the
// original ctx.next captured at invocation time. This avoids the
// infinite recursion bug where the compiled tail reads an already-
// overwritten ctx.next, and is safe under concurrent requests.
type ChainFn = (
ctx: Context<State>,
tail: () => Promise<Response>,
) => Response | Promise<Response>;

let chain: ChainFn = (_ctx, tail) => tail();

ctx.next = local;
try {
const result = await next(ctx);
if (typeof result === "function") {
middlewares[idx] = result;
next = result;
return await result(ctx);
}
for (let i = middlewares.length - 1; i >= 0; i--) {
const nextChain = chain;
let middleware = middlewares[i];
chain = async (ctx, tail) => {
const internals = getInternals(ctx);
const { app: prevApp, layouts: prevLayouts } = internals;

return result;
} catch (err) {
if (ctx.error !== err) {
ctx.error = err;
ctx.next = () => Promise.resolve(nextChain(ctx, tail));
try {
const result = await middleware(ctx);
if (typeof result === "function") {
middleware = result;
return await result(ctx);
}

if (onError !== undefined) {
onError(err);
}
}
throw err;
} finally {
internals.app = prevApp;
internals.layouts = prevLayouts;
return result;
} catch (err) {
if (ctx.error !== err) {
ctx.error = err;

if (onError !== undefined) {
onError(err);
}
};
}
throw err;
} finally {
internals.app = prevApp;
internals.layouts = prevLayouts;
}
};
}

const count = middlewares.length;
return (ctx) => {
const tail = ctx.next;
return tracer.startActiveSpan("middlewares", {
attributes: { "fresh.middleware.count": count },
}, async (span) => {
try {
return await chain(ctx, tail);
} catch (err) {
recordSpanError(span, err);
throw err;
} finally {
span.end();
}
return await fn();
} catch (err) {
recordSpanError(span, err);
throw err;
} finally {
span.end();
}
});
});
};
}
Loading
Loading