/** * Mandu notFound() helper for SSR loaders and handlers. * * Symmetric with `redirect()` — both short-circuit the SSR pipeline by * returning (or throwing) a marked `Response`. Where `redirect()` tells * the runtime "navigate somewhere else", `notFound()` tells the runtime * "this resource does not exist; render the 404 surface". * * ## Usage * * ```ts * import { Mandu, notFound } from "@mandujs/core"; * * export const filling = Mandu.filling().loader(async (ctx) => { * const post = await db.post.find(ctx.params.slug); * if (!post) return notFound(); // or: throw notFound(); * return { post }; * }); * ``` * * The SSR pipeline (server.ts `loadPageData`) checks each loader result * with `isNotFoundResponse()`. On a hit it: * * 1. Prefers `app/not-found.tsx` (if registered) — rendered as a normal * page with status 404 and any pending cookies preserved. * 2. Falls back to the framework's built-in 404 JSON error. * * ## Why a branded Response * * A bare `new Response(null, { status: 404 })` is NOT treated as a * notFound sentinel. That's intentional: a loader that accidentally * returns a generic 404 Response (e.g. proxying an upstream fetch) must * NOT hijack the page to show our 404 page. Only values minted through * `notFound()` carry the internal brand. * * The brand is a non-enumerable WeakSet membership stamped on the * Response object. Never serialised, never visible to clients. */ /** Internal brand — identifies Response objects minted by `notFound()`. */ export const NOT_FOUND_BRAND: unique symbol = Symbol.for("@mandujs/core/not-found"); /** WeakSet of Response instances tagged as notFound. Avoids property writes. */ const brandedNotFoundResponses = new WeakSet(); /** Options for tuning a notFound response. */ export interface NotFoundOptions { /** Optional human-readable message. Surfaced to the 404 page via body. */ message?: string; } /** * Create a `Response` that signals "not found" to the Mandu SSR pipeline. * * Returns a real `Response` (status 404) for three reasons: * * 1. Loaders that `return` or `throw` it are treated identically — no * extra plumbing for the "deep call stack wants to bail out" case. * 2. Consumers outside an SSR loader (route handlers, middleware) can * use the same helper without a separate API. * 3. A hostile or buggy loader returning a bare `new Response(null, {status:404})` * does NOT trigger the framework's 404 page path — it falls through * the existing error channel like any other unexpected Response. * * @param options - Optional `{ message }`. Message is serialised into the * response body as `text/plain; charset=utf-8`. If omitted, the body * defaults to `"Not Found"`. */ export function notFound(options: NotFoundOptions = {}): Response { const message = typeof options.message === "string" && options.message.length > 0 ? options.message : "Not Found"; const response = new Response(message, { status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" }, }); brandedNotFoundResponses.add(response); return response; } /** * True when `value` is a Response produced by `notFound()`. * * Deliberately strict — only branded responses match. A bare * `new Response(null, { status: 404 })` is NOT recognised, nor is a * redirect Response (even one with a 404-like status, which would be * malformed but shouldn't confuse us). */ export function isNotFoundResponse(value: unknown): value is Response { if (!(value instanceof Response)) return false; return brandedNotFoundResponses.has(value); }