@real-router/ssr-data-plugin 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/{deferRegistry-B2z5CFjD.js → deferRegistry-BQgydXL6.js} +2 -2
- package/dist/cjs/deferRegistry-BQgydXL6.js.map +1 -0
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/server.js +1 -1
- package/dist/esm/{deferRegistry-Bz1ihUeo.mjs → deferRegistry-C4u1L5qm.mjs} +2 -2
- package/dist/esm/deferRegistry-C4u1L5qm.mjs.map +1 -0
- package/dist/esm/errors.mjs +1 -1
- package/dist/esm/errors.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/server.mjs +1 -1
- package/package.json +3 -4
- package/dist/cjs/deferRegistry-B2z5CFjD.js.map +0 -1
- package/dist/esm/deferRegistry-Bz1ihUeo.mjs.map +0 -1
- package/src/constants.ts +0 -3
- package/src/errors.ts +0 -6
- package/src/factory.ts +0 -22
- package/src/getSsrDataMode.ts +0 -29
- package/src/index.ts +0 -27
- package/src/invalidate.ts +0 -38
- package/src/server.ts +0 -319
- package/src/types.ts +0 -42
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=`__rrDeferRegistry__`,t=`__rrDefer__`,n=`__rrDeferError__`;function r(){return globalThis}function i(){let t=r(),n=t[e];return n===void 0&&(n=new Map,t[e]=n),n}function a(e){let t=i(),n=t.get(e);if(n===void 0){let r,i;n={promise:new Promise((e,t)=>{r=e,i=t}),resolve:r,reject:i},t.set(e,n)}return n.promise}function o(){return`(function(g){var R=g.${e};if(!R)R=g.${e}=new Map();function E(k){var e=R.get(k);if(!e){var rs,rj;var p=new Promise(function(r,j){rs=r;rj=j});e={promise:p,resolve:rs,reject:rj};R.set(k,e)}return e}g.${t}=function(k,j){E(k).resolve(JSON.parse(j))};g.${n}=function(k,j){var d=JSON.parse(j);var er=new Error(d&&d.message?d.message:"deferred error");if(d&&d.name)er.name=d.name;E(k).reject(er)}})(typeof globalThis!=='undefined'?globalThis:(typeof window!=='undefined'?window:self));`}const s=[[
|
|
2
|
-
//# sourceMappingURL=deferRegistry-
|
|
1
|
+
const e=`__rrDeferRegistry__`,t=`__rrDefer__`,n=`__rrDeferError__`;function r(){return globalThis}function i(){let t=r(),n=t[e];return n===void 0&&(n=new Map,t[e]=n),n}function a(e){let t=i(),n=t.get(e);if(n===void 0){let r,i;n={promise:new Promise((e,t)=>{r=e,i=t}),resolve:r,reject:i},t.set(e,n)}return n.promise}function o(){return`(function(g){var R=g.${e};if(!R)R=g.${e}=new Map();function E(k){var e=R.get(k);if(!e){var rs,rj;var p=new Promise(function(r,j){rs=r;rj=j});e={promise:p,resolve:rs,reject:rj};R.set(k,e)}return e}g.${t}=function(k,j){E(k).resolve(JSON.parse(j))};g.${n}=function(k,j){var d=JSON.parse(j);var er=new Error(d&&d.message?d.message:"deferred error");if(d&&d.name)er.name=d.name;E(k).reject(er)}})(typeof globalThis!=='undefined'?globalThis:(typeof window!=='undefined'?window:self));`}const s=[[`<`,String.raw`\u003c`],[`>`,String.raw`\u003e`],[`&`,String.raw`\u0026`],[String.fromCodePoint(8232),String.raw`\u2028`],[String.fromCodePoint(8233),String.raw`\u2029`]],c=Object.fromEntries(s),l=RegExp(`[${s.map(([e])=>e).join(``)}]`,`g`);function u(e){let t;try{t=JSON.stringify(e)}catch{t=void 0}return typeof t==`string`?t.replace(l,e=>c[e]??e):`null`}function d(e,r,i){return`<script>${i?n:t}(${u(e)},${u(r)})<\/script>`}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return a}});
|
|
2
|
+
//# sourceMappingURL=deferRegistry-BQgydXL6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferRegistry-BQgydXL6.js","names":[],"sources":["../../../../shared/ssr/deferRegistry.ts"],"sourcesContent":["/**\n * Client-side registry for deferred values streamed from the server.\n *\n * The contract spans three actors:\n *\n * 1. **Server stream injects `<script>__rrDefer__(\"key\", \"json\")</script>`\n * tags** as each loader-returned promise resolves. The bootstrap script\n * (also server-emitted) installs `__rrDefer__` and the registry on\n * `globalThis` before any settle script runs.\n *\n * 2. **Plugin start interceptor** (post-hydration scratchpad path) reads the\n * `<deferredKeysNamespace>` list from the hydrated state, then calls\n * `ensureRegistryPromise(key)` once per key to obtain the promise that\n * `useDeferred()` will return. This ensures a stable Promise reference\n * across the initial render and any inline-script settlements.\n *\n * 3. **Adapter `useDeferred(key)`** reads from `state.context.<deferredNamespace>`\n * which the plugin populated above. The returned Promise integrates with\n * React `use()`, Solid `<Await/>`, Svelte `{#await}`, etc.\n */\n\ninterface RegistryEntry {\n promise: Promise<unknown>;\n resolve: (value: unknown) => void;\n reject: (error: unknown) => void;\n}\n\nconst REGISTRY_GLOBAL_KEY = \"__rrDeferRegistry__\";\nconst SETTLE_FN_NAME = \"__rrDefer__\";\nconst REJECT_FN_NAME = \"__rrDeferError__\";\n\ninterface DeferGlobal {\n [REGISTRY_GLOBAL_KEY]?: Map<string, RegistryEntry>;\n [SETTLE_FN_NAME]?: (key: string, json: string) => void;\n [REJECT_FN_NAME]?: (key: string, json: string) => void;\n}\n\nfunction getGlobal(): DeferGlobal {\n return globalThis as unknown as DeferGlobal;\n}\n\nfunction getOrCreateRegistry(): Map<string, RegistryEntry> {\n const g = getGlobal();\n let registry = g[REGISTRY_GLOBAL_KEY];\n\n if (registry === undefined) {\n registry = new Map<string, RegistryEntry>();\n g[REGISTRY_GLOBAL_KEY] = registry;\n }\n\n return registry;\n}\n\n/**\n * Returns the registered Promise for `key`, creating a fresh pending entry on\n * first access. Stable across calls — `useDeferred` relies on Promise\n * reference identity for React `use()` to track resolution.\n */\nexport function ensureRegistryPromise(key: string): Promise<unknown> {\n const registry = getOrCreateRegistry();\n let entry = registry.get(key);\n\n if (entry === undefined) {\n let resolve!: (value: unknown) => void;\n let reject!: (error: unknown) => void;\n\n const promise = new Promise<unknown>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n\n entry = { promise, resolve, reject };\n registry.set(key, entry);\n }\n\n return entry.promise;\n}\n\n/**\n * Returns the inline bootstrap script (no `<script>` wrapper). Embed in a\n * `<script>` tag emitted **once before any `__rrDefer__()` call lands** in\n * the response stream. Idempotent — re-installing is a no-op.\n *\n * The script source is kept terse (ES5-ish, no template literals, no\n * arrow functions) so it works without transpilation in legacy browsers and\n * stays under ~600 bytes uncompressed.\n */\nexport function getDeferBootstrapScript(): string {\n // The script idempotently installs __rrDefer__/__rrDeferError__ on `g`. If\n // the registry already exists (e.g. from a prior call to\n // ensureRegistryPromise on the client adapter), reuse it — only the settle\n // functions are (re)assigned. This handles the realistic ordering:\n // adapter creates the registry during hydration; the first settle script\n // arriving in the response stream installs the global functions.\n return (\n \"(function(g){\" +\n `var R=g.${REGISTRY_GLOBAL_KEY};` +\n `if(!R)R=g.${REGISTRY_GLOBAL_KEY}=new Map();` +\n \"function E(k){\" +\n \"var e=R.get(k);\" +\n \"if(!e){\" +\n \"var rs,rj;\" +\n \"var p=new Promise(function(r,j){rs=r;rj=j});\" +\n \"e={promise:p,resolve:rs,reject:rj};\" +\n \"R.set(k,e)\" +\n \"}\" +\n \"return e\" +\n \"}\" +\n `g.${SETTLE_FN_NAME}=function(k,j){E(k).resolve(JSON.parse(j))};` +\n `g.${REJECT_FN_NAME}=function(k,j){` +\n \"var d=JSON.parse(j);\" +\n 'var er=new Error(d&&d.message?d.message:\"deferred error\");' +\n \"if(d&&d.name)er.name=d.name;\" +\n \"E(k).reject(er)\" +\n \"}\" +\n \"})(typeof globalThis!=='undefined'?globalThis:\" +\n \"(typeof window!=='undefined'?window:self));\"\n );\n}\n\n// Single-pass replacement table for the chars escapeForScript must encode\n// as `\\uXXXX` to keep them out of the raw HTML parser. Five consecutive\n// `replace` / `split`+`join` passes used to walk the string for each\n// codepoint; the regex + lookup form does it in one pass — ~1.6× faster\n// on large payloads, indistinguishable on short keys (the common case).\n//\n// Roundtrip + HTML-safety properties are pinned by the\n// `escapeForScript: pure-function security invariants` PBT block in\n// `tests/property/ssr-data.properties.ts` (numRuns: 1000).\n//\n// Built at module init via `String.fromCodePoint(...)` so the source file\n// itself never contains raw U+2028 / U+2029 codepoints (which would\n// terminate string literals / regex literals at parse time on legacy\n// JS engines and even in modern TS parsers under some configs).\nconst ESCAPE_FOR_SCRIPT_PAIRS: readonly (readonly [string, string])[] = [\n [\"<\", String.raw`\\u003c`],\n [\">\", String.raw`\\u003e`],\n [\"&\", String.raw`\\u0026`],\n [String.fromCodePoint(0x20_28), String.raw`\\u2028`],\n [String.fromCodePoint(0x20_29), String.raw`\\u2029`],\n] as const;\nconst ESCAPE_FOR_SCRIPT_TABLE: Record<string, string> = Object.fromEntries(\n ESCAPE_FOR_SCRIPT_PAIRS,\n);\nconst ESCAPE_FOR_SCRIPT_REGEX = new RegExp(\n `[${ESCAPE_FOR_SCRIPT_PAIRS.map(([c]) => c).join(\"\")}]`,\n \"g\",\n);\n\n/**\n * Encode an arbitrary string as a **JS string literal** that is also safe to\n * embed inside a `<script>...</script>` body. Returns the literal **with**\n * surrounding quotes — drop it directly into a script template.\n *\n * Encoding via Unicode escapes (`\\uXXXX`) means:\n * - The raw HTML parser sees no `<`, `>`, U+2028, or U+2029 — so it cannot\n * terminate the script tag prematurely (`</script>`, `<!--`) or trigger\n * legacy JS line-terminator interpretation.\n * - The JS parser interprets `<`/`>`/`U+2028`/`U+2029` back to\n * their original chars, so the runtime string value is bit-identical to\n * the input.\n * - Crucially, the same encoding works for two consumer paths:\n * 1. **Plain JS literal** (e.g. the deferred KEY): the JS parser hands\n * back the original string directly.\n * 2. **JS literal containing JSON** (e.g. the deferred VALUE): the JS\n * parser hands back a string with `<` text inside (the leading\n * `\\\\` of `\\\\u003c` escaped to `\\`, then `u003c` is plain text), and\n * `JSON.parse` then unescapes `<` → `<`. Net round-trip is\n * identity.\n * Both decode paths land on the original string — so the same\n * `escapeForScript` works for both keys (parsed as JS literal) and values\n * (parsed as JS literal containing JSON).\n *\n * The `&` → `&` substitution defends against `<![CDATA[` / template\n * engine post-processing that might re-interpret HTML entities; it is not\n * strictly necessary for `<script>` body parsing but cheap and conservative.\n */\nexport function escapeForScript(value: string): string {\n // The TS contract is `value: string`, but a cast at a callsite or a\n // misbehaving custom serializer can still smuggle a non-string through.\n // Three failure modes JSON.stringify can have on non-strings:\n // - returns `undefined` (`stringify(undefined)`, `stringify(symbol)`,\n // `stringify(function)`),\n // - throws (`stringify(bigint)` → `TypeError`,\n // `stringify(circular)` → `TypeError`),\n // - returns `\"null\"` (already safe for our pipeline).\n // Catch both and emit the JSON `null` literal — the safest single-token\n // representation that JSON.parse will accept downstream.\n let json: string | undefined;\n\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n\n if (typeof json !== \"string\") {\n return \"null\";\n }\n\n return json.replace(\n ESCAPE_FOR_SCRIPT_REGEX,\n // The `?? c` arm is structurally unreachable — the regex char class is\n // built from the table's own keys, so every match has a table entry; the\n // fallback only satisfies the `string | undefined` index signature.\n /* v8 ignore next -- @preserve: TS index-access fallback, regex ⊆ table keys */\n (c) => ESCAPE_FOR_SCRIPT_TABLE[c] ?? c,\n );\n}\n\n/**\n * Format a single settle script for one resolved promise.\n * Output: `<script>__rrDefer__(\"key\",\"jsonString\")</script>`. Both `key`\n * and `serializedValue` are user-controlled in the general case (route\n * params can flow into deferred-map keys; loader returns flow into values),\n * so both go through {@link escapeForScript}.\n */\nexport function formatSettleScript(\n key: string,\n serializedValue: string,\n isError: boolean,\n): string {\n const fn = isError ? REJECT_FN_NAME : SETTLE_FN_NAME;\n const safeKey = escapeForScript(key);\n const safeValue = escapeForScript(serializedValue);\n\n return `<script>${fn}(${safeKey},${safeValue})</script>`;\n}\n\n/** Test-only — clears the global registry. Not exported from index.ts. */\nexport function __resetRegistryForTests(): void {\n const g = getGlobal();\n delete g[REGISTRY_GLOBAL_KEY];\n delete g[SETTLE_FN_NAME];\n delete g[REJECT_FN_NAME];\n}\n"],"mappings":"AA2BA,MAAM,EAAsB,sBACtB,EAAiB,cACjB,EAAiB,mBAQvB,SAAS,GAAyB,CAChC,OAAO,UACT,CAEA,SAAS,GAAkD,CACzD,IAAM,EAAI,EAAU,EAChB,EAAW,EAAE,GAOjB,OALI,IAAa,IAAA,KACf,EAAW,IAAI,IACf,EAAE,GAAuB,GAGpB,CACT,CAOA,SAAgB,EAAsB,EAA+B,CACnE,IAAM,EAAW,EAAoB,EACjC,EAAQ,EAAS,IAAI,CAAG,EAE5B,GAAI,IAAU,IAAA,GAAW,CACvB,IAAI,EACA,EAOJ,EAAQ,CAAE,QAAA,IALU,SAAkB,EAAK,IAAQ,CACjD,EAAU,EACV,EAAS,CACX,CAEgB,EAAG,UAAS,QAAO,EACnC,EAAS,IAAI,EAAK,CAAK,CACzB,CAEA,OAAO,EAAM,OACf,CAWA,SAAgB,GAAkC,CAOhD,MACE,wBACW,EAAoB,aAClB,EAAoB,gKAW5B,EAAe,gDACf,EAAe,mOASxB,CAgBA,MAAM,EAAkE,CACtE,CAAC,IAAK,OAAO,GAAG,QAAQ,EACxB,CAAC,IAAK,OAAO,GAAG,QAAQ,EACxB,CAAC,IAAK,OAAO,GAAG,QAAQ,EACxB,CAAC,OAAO,cAAc,IAAO,EAAG,OAAO,GAAG,QAAQ,EAClD,CAAC,OAAO,cAAc,IAAO,EAAG,OAAO,GAAG,QAAQ,CACpD,EACM,EAAkD,OAAO,YAC7D,CACF,EACM,EAA8B,OAClC,IAAI,EAAwB,KAAK,CAAC,KAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,GACrD,GACF,EA8BA,SAAgB,EAAgB,EAAuB,CAWrD,IAAI,EAEJ,GAAI,CACF,EAAO,KAAK,UAAU,CAAK,CAC7B,MAAQ,CACN,EAAO,IAAA,EACT,CAMA,OAJI,OAAO,GAAS,SAIb,EAAK,QACV,EAKC,GAAM,EAAwB,IAAM,CACvC,EAVS,MAWX,CASA,SAAgB,EACd,EACA,EACA,EACQ,CAKR,MAAO,WAJI,EAAU,EAAiB,EAIjB,GAHL,EAAgB,CAGF,EAAE,GAFd,EAAgB,CAES,EAAE,YAC/C"}
|
package/dist/cjs/errors.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=class extends Error{target;status;code=`LOADER_REDIRECT`;constructor(e,t=302){super(`Redirect to ${e}`),this.target=e,this.status=t,this.name=`LoaderRedirect`}},t=class extends Error{resource;code=`LOADER_NOT_FOUND`;constructor(e){super(`Resource not found: ${e}`),this.resource=e,this.name=`LoaderNotFound`}},n=class extends Error{route;ms;code=`LOADER_TIMEOUT`;constructor(e,t){super(`Loader for "${e}" exceeded ${t}ms`),this.route=e,this.ms=t,this.name=`LoaderTimeout`}};function r(e,t,r,i){let a=i?.upstreamSignal;if(a?.aborted)return Promise.reject(a.reason??new DOMException(`The operation was aborted.`,`AbortError`));let o=new AbortController,s=a?AbortSignal.any([a,o.signal]):o.signal,c,l=new Promise((r,i)=>{c=setTimeout(()=>{let r=new n(e,t);o.abort(r),i(r)},t)}),u=(async()=>r({signal:s}))().finally(()=>{
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=class extends Error{target;status;code=`LOADER_REDIRECT`;constructor(e,t=302){super(`Redirect to ${e}`),this.target=e,this.status=t,this.name=`LoaderRedirect`}},t=class extends Error{resource;code=`LOADER_NOT_FOUND`;constructor(e){super(`Resource not found: ${e}`),this.resource=e,this.name=`LoaderNotFound`}},n=class extends Error{route;ms;code=`LOADER_TIMEOUT`;constructor(e,t){super(`Loader for "${e}" exceeded ${t}ms`),this.route=e,this.ms=t,this.name=`LoaderTimeout`}};function r(e,t,r,i){let a=i?.upstreamSignal;if(a?.aborted)return Promise.reject(a.reason??new DOMException(`The operation was aborted.`,`AbortError`));let o=new AbortController,s=a?AbortSignal.any([a,o.signal]):o.signal,c,l=new Promise((r,i)=>{c=setTimeout(()=>{let r=new n(e,t);o.abort(r),i(r)},t)}),u=(async()=>r({signal:s}))().finally(()=>{clearTimeout(c)});return Promise.race([u,l])}exports.LoaderNotFound=t,exports.LoaderRedirect=e,exports.LoaderTimeout=n,exports.withTimeout=r;
|
|
2
2
|
//# sourceMappingURL=errors.js.map
|
package/dist/cjs/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../../../../shared/ssr/errors.ts"],"sourcesContent":["/**\n * Typed loader errors that SSR pipelines translate into HTTP semantics.\n *\n * The `ssr-data-plugin` and `rsc-server-plugin` are intentionally\n * HTTP-agnostic — they only await the loader and write the resolved value\n * to `state.context.<namespace>`. Loaders bridge to HTTP status codes by\n * throwing one of these named errors; application-layer middleware catches\n * them and maps each `code` to the right status (302/308, 404, 504).\n *\n * Structural discrimination via `code` (not `instanceof`) so consumers\n * can match across realms / bundle boundaries without coupling to the\n * class identity.\n *\n * Re-exported from both plugins under the `./errors` subpath:\n * `@real-router/ssr-data-plugin/errors` and\n * `@real-router/rsc-server-plugin/errors`.\n */\n\nexport class LoaderRedirect extends Error {\n readonly code = \"LOADER_REDIRECT\";\n\n constructor(\n readonly target: string,\n readonly status: 301 | 302 | 307 | 308 = 302,\n ) {\n super(`Redirect to ${target}`);\n this.name = \"LoaderRedirect\";\n }\n}\n\nexport class LoaderNotFound extends Error {\n readonly code = \"LOADER_NOT_FOUND\";\n\n constructor(readonly resource: string) {\n super(`Resource not found: ${resource}`);\n this.name = \"LoaderNotFound\";\n }\n}\n\nexport class LoaderTimeout extends Error {\n readonly code = \"LOADER_TIMEOUT\";\n\n constructor(\n readonly route: string,\n readonly ms: number,\n ) {\n super(`Loader for \"${route}\" exceeded ${ms}ms`);\n this.name = \"LoaderTimeout\";\n }\n}\n\n/**\n * Race a loader against a deadline, with cooperative cancellation.\n *\n * The loader is invoked with `{ signal }` — a composed `AbortSignal` that\n * aborts on the first of:\n * - the deadline elapsing (`internalController.abort()` fires synchronously\n * *before* the race rejects with `LoaderTimeout`, so a loader that\n * threads `signal` into its I/O — e.g. `fetch(url, { signal })` — can\n * actually cancel the underlying work);\n * - `options.upstreamSignal` aborting (typically the request-scoped abort\n * wired by `cloneRouter(base, { abortSignal })` for client-disconnect).\n *\n * Composition uses `AbortSignal.any([upstream, internal])` (Node 20.3+).\n * If `upstreamSignal` is already aborted at call time, the loader is *not*\n * invoked and the timer is *not* started — the rejection mirrors\n * `upstreamSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\")`.\n *\n * On deadline, the same `LoaderTimeout` instance is used as both the\n * `signal.reason` and the rejection reason — they refer to one object.\n * On upstream abort during execution, the race rejects with the loader's\n * own error (typically `AbortError`), *not* `LoaderTimeout`.\n *\n * Cancellation is cooperative: loaders that don't propagate `signal` into\n * their I/O still run to completion in the background — the race result\n * is unaffected, but resources are not freed early.\n *\n * The `setTimeout` handle is cleared via `.finally()` on the work promise\n * so a fast-path success doesn't leak it. `Promise.race`'s internal\n * `Promise.resolve(p).then(resolve, reject)` consumes any late losing\n * rejection — no `unhandledRejection` for late loader settlements.\n *\n * Requires Node 20.3+ for `AbortSignal.any`.\n *\n * ### `ms` corner cases (Node `setTimeout` clamping)\n *\n * `ms` is forwarded verbatim to `setTimeout`, which means `Infinity`, `NaN`,\n * and negative values are **NOT** safe sentinels for \"no deadline\":\n *\n * - `withTimeout(\"r\", Infinity, …)` — Node clamps to `1` ms and emits a\n * `TimeoutOverflowWarning`. The race rejects with `LoaderTimeout` after\n * 1 ms, not \"never\". Use a separate code path (e.g. invoke the loader\n * directly without wrapping) when you genuinely want no deadline.\n * - `withTimeout(\"r\", NaN, …)` — same: Node clamps to `1` ms with a\n * warning.\n * - `withTimeout(\"r\", -1, …)` — Node clamps to `1` ms with a warning.\n * - `withTimeout(\"r\", 0, …)` — fires on the next tick. A synchronous-\n * resolving loader (`() => Promise.resolve(v)`) typically wins the race,\n * but any async I/O loses. Treat `0` as \"fire immediately\" rather than\n * \"no deadline\".\n *\n * No runtime guard is added — the clamping is a Node-level concern and\n * adding `if (!Number.isFinite(ms) || ms < 0) throw` would be a breaking\n * change for callers relying on the current clamp semantics.\n */\nexport function withTimeout<T>(\n routeName: string,\n ms: number,\n loader: (deps: { signal: AbortSignal }) => Promise<T>,\n options?: { upstreamSignal?: AbortSignal | null },\n): Promise<T> {\n const upstream = options?.upstreamSignal;\n\n if (upstream?.aborted) {\n // `signal.reason` is normally set automatically by the spec\n // (`controller.abort()` without an argument yields a `DOMException`),\n // but the field is writable, so we fall back to a fresh `AbortError`\n // if some caller produced an aborted signal with `reason === undefined`.\n return Promise.reject(\n upstream.reason ??\n new DOMException(\"The operation was aborted.\", \"AbortError\"),\n );\n }\n\n const internal = new AbortController();\n const composed = upstream\n ? AbortSignal.any([upstream, internal.signal])\n : internal.signal;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n const error = new LoaderTimeout(routeName, ms);\n internal.abort(error);\n reject(error);\n }, ms);\n });\n\n const work = (async () => loader({ signal: composed }))().finally(() => {\n
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../../../shared/ssr/errors.ts"],"sourcesContent":["/**\n * Typed loader errors that SSR pipelines translate into HTTP semantics.\n *\n * The `ssr-data-plugin` and `rsc-server-plugin` are intentionally\n * HTTP-agnostic — they only await the loader and write the resolved value\n * to `state.context.<namespace>`. Loaders bridge to HTTP status codes by\n * throwing one of these named errors; application-layer middleware catches\n * them and maps each `code` to the right status (302/308, 404, 504).\n *\n * Structural discrimination via `code` (not `instanceof`) so consumers\n * can match across realms / bundle boundaries without coupling to the\n * class identity.\n *\n * Re-exported from both plugins under the `./errors` subpath:\n * `@real-router/ssr-data-plugin/errors` and\n * `@real-router/rsc-server-plugin/errors`.\n */\n\nexport class LoaderRedirect extends Error {\n readonly code = \"LOADER_REDIRECT\";\n\n constructor(\n readonly target: string,\n readonly status: 301 | 302 | 307 | 308 = 302,\n ) {\n super(`Redirect to ${target}`);\n this.name = \"LoaderRedirect\";\n }\n}\n\nexport class LoaderNotFound extends Error {\n readonly code = \"LOADER_NOT_FOUND\";\n\n constructor(readonly resource: string) {\n super(`Resource not found: ${resource}`);\n this.name = \"LoaderNotFound\";\n }\n}\n\nexport class LoaderTimeout extends Error {\n readonly code = \"LOADER_TIMEOUT\";\n\n constructor(\n readonly route: string,\n readonly ms: number,\n ) {\n super(`Loader for \"${route}\" exceeded ${ms}ms`);\n this.name = \"LoaderTimeout\";\n }\n}\n\n/**\n * Race a loader against a deadline, with cooperative cancellation.\n *\n * The loader is invoked with `{ signal }` — a composed `AbortSignal` that\n * aborts on the first of:\n * - the deadline elapsing (`internalController.abort()` fires synchronously\n * *before* the race rejects with `LoaderTimeout`, so a loader that\n * threads `signal` into its I/O — e.g. `fetch(url, { signal })` — can\n * actually cancel the underlying work);\n * - `options.upstreamSignal` aborting (typically the request-scoped abort\n * wired by `cloneRouter(base, { abortSignal })` for client-disconnect).\n *\n * Composition uses `AbortSignal.any([upstream, internal])` (Node 20.3+).\n * If `upstreamSignal` is already aborted at call time, the loader is *not*\n * invoked and the timer is *not* started — the rejection mirrors\n * `upstreamSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\")`.\n *\n * On deadline, the same `LoaderTimeout` instance is used as both the\n * `signal.reason` and the rejection reason — they refer to one object.\n * On upstream abort during execution, the race rejects with the loader's\n * own error (typically `AbortError`), *not* `LoaderTimeout`.\n *\n * Cancellation is cooperative: loaders that don't propagate `signal` into\n * their I/O still run to completion in the background — the race result\n * is unaffected, but resources are not freed early.\n *\n * The `setTimeout` handle is cleared via `.finally()` on the work promise\n * so a fast-path success doesn't leak it. `Promise.race`'s internal\n * `Promise.resolve(p).then(resolve, reject)` consumes any late losing\n * rejection — no `unhandledRejection` for late loader settlements.\n *\n * Requires Node 20.3+ for `AbortSignal.any`.\n *\n * ### `ms` corner cases (Node `setTimeout` clamping)\n *\n * `ms` is forwarded verbatim to `setTimeout`, which means `Infinity`, `NaN`,\n * and negative values are **NOT** safe sentinels for \"no deadline\":\n *\n * - `withTimeout(\"r\", Infinity, …)` — Node clamps to `1` ms and emits a\n * `TimeoutOverflowWarning`. The race rejects with `LoaderTimeout` after\n * 1 ms, not \"never\". Use a separate code path (e.g. invoke the loader\n * directly without wrapping) when you genuinely want no deadline.\n * - `withTimeout(\"r\", NaN, …)` — same: Node clamps to `1` ms with a\n * warning.\n * - `withTimeout(\"r\", -1, …)` — Node clamps to `1` ms with a warning.\n * - `withTimeout(\"r\", 0, …)` — fires on the next tick. A synchronous-\n * resolving loader (`() => Promise.resolve(v)`) typically wins the race,\n * but any async I/O loses. Treat `0` as \"fire immediately\" rather than\n * \"no deadline\".\n *\n * No runtime guard is added — the clamping is a Node-level concern and\n * adding `if (!Number.isFinite(ms) || ms < 0) throw` would be a breaking\n * change for callers relying on the current clamp semantics.\n */\nexport function withTimeout<T>(\n routeName: string,\n ms: number,\n loader: (deps: { signal: AbortSignal }) => Promise<T>,\n options?: { upstreamSignal?: AbortSignal | null },\n): Promise<T> {\n const upstream = options?.upstreamSignal;\n\n if (upstream?.aborted) {\n // `signal.reason` is normally set automatically by the spec\n // (`controller.abort()` without an argument yields a `DOMException`),\n // but the field is writable, so we fall back to a fresh `AbortError`\n // if some caller produced an aborted signal with `reason === undefined`.\n return Promise.reject(\n upstream.reason ??\n new DOMException(\"The operation was aborted.\", \"AbortError\"),\n );\n }\n\n const internal = new AbortController();\n const composed = upstream\n ? AbortSignal.any([upstream, internal.signal])\n : internal.signal;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n const error = new LoaderTimeout(routeName, ms);\n internal.abort(error);\n reject(error);\n }, ms);\n });\n\n const work = (async () => loader({ signal: composed }))().finally(() => {\n // The Promise executor above runs synchronously, so `timer` is always\n // assigned by the time `finally` fires — and `clearTimeout` accepts\n // `undefined` anyway, so no guard is needed.\n clearTimeout(timer);\n });\n\n return Promise.race<T>([work, timeoutPromise]);\n}\n"],"mappings":"mEAkBA,IAAa,EAAb,cAAoC,KAAM,CAI7B,OACA,OAJX,KAAgB,kBAEhB,YACE,EACA,EAAyC,IACzC,CACA,MAAM,eAAe,GAAQ,EAHpB,KAAA,OAAA,EACA,KAAA,OAAA,EAGT,KAAK,KAAO,gBACd,CACF,EAEa,EAAb,cAAoC,KAAM,CAGnB,SAFrB,KAAgB,mBAEhB,YAAY,EAA2B,CACrC,MAAM,uBAAuB,GAAU,EADpB,KAAA,SAAA,EAEnB,KAAK,KAAO,gBACd,CACF,EAEa,EAAb,cAAmC,KAAM,CAI5B,MACA,GAJX,KAAgB,iBAEhB,YACE,EACA,EACA,CACA,MAAM,eAAe,EAAM,aAAa,EAAG,GAAG,EAHrC,KAAA,MAAA,EACA,KAAA,GAAA,EAGT,KAAK,KAAO,eACd,CACF,EAwDA,SAAgB,EACd,EACA,EACA,EACA,EACY,CACZ,IAAM,EAAW,GAAS,eAE1B,GAAI,GAAU,QAKZ,OAAO,QAAQ,OACb,EAAS,QACP,IAAI,aAAa,6BAA8B,YAAY,CAC/D,EAGF,IAAM,EAAW,IAAI,gBACf,EAAW,EACb,YAAY,IAAI,CAAC,EAAU,EAAS,MAAM,CAAC,EAC3C,EAAS,OAET,EACE,EAAiB,IAAI,SAAY,EAAG,IAAW,CACnD,EAAQ,eAAiB,CACvB,IAAM,EAAQ,IAAI,EAAc,EAAW,CAAE,EAC7C,EAAS,MAAM,CAAK,EACpB,EAAO,CAAK,CACd,EAAG,CAAE,CACP,CAAC,EAEK,GAAQ,SAAY,EAAO,CAAE,OAAQ,CAAS,CAAC,EAAA,CAAG,CAAC,CAAC,YAAc,CAItE,aAAa,CAAK,CACpB,CAAC,EAED,OAAO,QAAQ,KAAQ,CAAC,EAAM,CAAc,CAAC,CAC/C"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./deferRegistry-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./deferRegistry-BQgydXL6.js");let t=require("@real-router/core/api"),n=require("@real-router/core/validation");const r=`[@real-router/ssr-data-plugin]`,i=Symbol.for(`@real-router/ssr-data-plugin/defer`);function a(e){if(typeof e!=`object`||!e)throw TypeError("[defer] expected an object with `critical` and `deferred` fields");if(e.deferred===null||typeof e.deferred!=`object`||Array.isArray(e.deferred))throw TypeError("[defer] `deferred` must be a non-null, non-array object of promises");for(let[t,n]of Object.entries(e.deferred)){if(t===`__proto__`||t===`constructor`||t===`prototype`)throw TypeError(`[defer] \`deferred.${t}\` is reserved — choose a different key`);if(typeof n!=`object`||!n||typeof n.then!=`function`)throw TypeError(`[defer] \`deferred.${t}\` must be a Promise (got ${typeof n})`);typeof n.catch==`function`&&n.catch(()=>{})}return Object.freeze({critical:e.critical,deferred:Object.freeze({...e.deferred}),[i]:!0})}function o(e){return typeof e==`object`&&!!e&&Object.hasOwn(e,i)&&e[i]===!0}const s=new WeakMap;function c(e,t){let n=s.get(e);n===void 0&&(n=new Set,s.set(e,n)),n.add(t)}function l(e,t){return s.get(e)?.has(t)??!1}function u(e,t){s.get(e)?.delete(t)}const d=[`full`,`data-only`,`client-only`];function f(e,t,n,r,i){let a=new Map;for(let[o,s]of Object.entries(e)){let e=typeof s==`function`?{loader:s}:s,c;if(e.loader!==void 0){let i=e.loader(t,n);if(typeof i!=`function`)throw TypeError(`${r} factory for route "${o}" must return a function`);c=i}let l=null,u;typeof e.ssr==`function`?u=e.ssr:l=h(e.ssr,p,i,r,o),a.set(o,{staticMode:l,modeFn:u,loader:c})}return a}const p={name:``,params:{},path:``,transition:{phase:`activating`,reason:`success`,segments:{deactivated:[],activated:[],intersection:``}},context:{}};function m(e,t,n,r){throw TypeError(`${n} mode "${String(e)}" is not allowed for route "${r}". Allowed: ${t.join(`, `)}`)}function h(e,t,n,r,i){if(e===void 0||e===!0)return`full`;if(e===!1)return`client-only`;let a=typeof e==`function`?e(t):e;return(typeof a!=`string`||!n.includes(a))&&m(a,n,r,i),a}function g(r,i){if(i.deferredNamespace!==void 0!=(i.deferredKeysNamespace!==void 0))throw TypeError(`${i.errorPrefix} \`deferredNamespace\` and \`deferredKeysNamespace\` must be set together`);let a=i.deferredNamespace!==void 0&&i.deferredKeysNamespace!==void 0?{valueNamespace:i.deferredNamespace,keysNamespace:i.deferredKeysNamespace}:null;return(s,c)=>{let p=(0,t.getPluginApi)(s),m=i.allowedModes??d,g=[],_=e=>{let t=p.claimContextNamespace(e);return g.push(t),t},v=()=>{for(let e of g)e.release()},y,b,x=null,S;try{y=_(i.namespace),b=_(i.modeNamespace),a!==null&&(x={value:_(a.valueNamespace),keys:_(a.keysNamespace)}),S=f(r,s,c,i.errorPrefix,m)}catch(e){throw v(),e}let C=(0,n.getInternals)(s),w=(e,t)=>{if(x!==null&&o(t)){y.write(e,t.critical),x.value.write(e,t.deferred),x.keys.write(e,Object.keys(t.deferred));return}y.write(e,t)},T=(t,n)=>{if(a===null||x===null)return;let r=n[a.keysNamespace];if(!Array.isArray(r))return;let i=r.filter(e=>typeof e==`string`&&e!==`__proto__`&&e!==`constructor`&&e!==`prototype`);if(i.length===0)return;let o=Object.create(null);for(let t of i)o[t]=e.t(t);x.value.write(t,o),x.keys.write(t,i)},E=e=>{let t=S.get(e.name);if(!t)return null;let n=t.staticMode===null?h(t.modeFn,e,m,i.errorPrefix,e.name):t.staticMode;return b.write(e,n),n===`client-only`?null:t},D=p.addInterceptor(`start`,async(e,t)=>{let n=await e(t),r=E(n);if(r===null)return n;let a=C.hydrationState;return a!==null&&a.name===n.name&&i.namespace in a.context?(y.write(n,a.context[i.namespace]),T(n,a.context)):r.loader!==void 0&&w(n,await r.loader(n.params)),n}),O=s.subscribeLeave(async({nextRoute:e,signal:t})=>{if(!l(s,i.namespace))return;let n=E(e);if(n===null||n.loader===void 0)return;let r=await n.loader(e.params,{signal:t});t.aborted||(u(s,i.namespace),w(e,r))});return{teardown(){D(),O(),y.release(),b.release(),x?.value.release(),x?.keys.release()}}}}function _(e,t=d){return function(n){if(typeof n!=`object`||!n||Array.isArray(n))throw TypeError(`${e} loaders must be a non-null object`);for(let[r,i]of Object.entries(n)){if(typeof i==`function`)continue;if(typeof i!=`object`||!i||Array.isArray(i))throw TypeError(`${e} entry for route "${r}" must be a function or { ssr?, loader? } object`);for(let t of Object.keys(i))if(t!==`ssr`&&t!==`loader`)throw TypeError(`${e} unexpected key "${t}" in route "${r}" config`);let n=i;if(n.loader!==void 0&&typeof n.loader!=`function`)throw TypeError(`${e} loader for route "${r}" must be a function`);if(n.ssr!==void 0){let i=n.ssr;if(typeof i==`function`||typeof i==`boolean`)continue;if(typeof i==`string`){if(!t.includes(i))throw TypeError(`${e} mode "${i}" is not allowed for route "${r}". Allowed: ${t.join(`, `)}`);continue}throw TypeError(`${e} ssr for route "${r}" must be SsrMode string, boolean, or (state) => SsrMode`)}}}}const v=_(r);function y(e){return v(e),g(e,{namespace:`data`,modeNamespace:`ssrDataMode`,deferredNamespace:`ssrDataDeferred`,deferredKeysNamespace:`ssrDataDeferredKeys`,errorPrefix:r})}function b(e){let t=e.context.ssrDataMode;return typeof t==`string`&&d.includes(t)?t:`full`}function x(e,t){c(e,t)}exports.defer=a,exports.getSsrDataMode=b,exports.invalidate=x,exports.isDeferred=o,exports.ssrDataPluginFactory=y;
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["ensureRegistryPromise"],"sources":["../../src/constants.ts","../../../../shared/ssr/defer.ts","../../../../shared/ssr/staleRegistry.ts","../../../../shared/ssr/types.ts","../../../../shared/ssr/createSsrLoaderPlugin.ts","../../../../shared/ssr/createLoadersValidator.ts","../../src/factory.ts","../../src/getSsrDataMode.ts","../../src/invalidate.ts"],"sourcesContent":["const LOGGER_CONTEXT = \"ssr-data-plugin\";\n\nexport const ERROR_PREFIX = `[@real-router/${LOGGER_CONTEXT}]`;\n","/**\n * Marker symbol for `defer()` payloads. `Symbol.for` is used so the brand\n * survives across multiple module instances (a real concern in monorepo setups\n * with multiple `node_modules/@real-router/ssr-data-plugin` copies).\n */\nexport const DEFER_BRAND: unique symbol = Symbol.for(\n \"@real-router/ssr-data-plugin/defer\",\n);\n\nexport interface DeferredPayload<\n C,\n D extends Record<string, Promise<unknown>>,\n> {\n readonly critical: C;\n readonly deferred: D;\n readonly [DEFER_BRAND]: true;\n}\n\n/**\n * Wraps a loader return value to declare a critical/deferred split.\n *\n * - `critical` resolves before HTML render (blocks the shell).\n * - `deferred` is a record of named promises that the framework can stream\n * independently — `<Suspense>`, `<Await/>`, `{#await}`, etc.\n *\n * The plugin writes `critical` to `state.context.<namespace>` (e.g. `data`)\n * and the deferred promises to `state.context.<namespace>Deferred` (e.g.\n * `ssrDataDeferred`). Adapter-side `useDeferred(key)` reads from the same\n * shape and returns the matching promise for native framework awaiting.\n *\n * On the server: `state.context.ssrDataDeferred[key]` is the actual promise\n * the loader produced. On the client (post-hydration): the plugin reconstructs\n * promises from the global `__rrDeferRegistry__` that inline `__rrDefer__()`\n * scripts populate as the server stream lands.\n */\nexport function defer<\n const C,\n const D extends Record<string, Promise<unknown>>,\n>(options: { readonly critical: C; readonly deferred: D }): DeferredPayload<\n C,\n D\n> {\n if (options === null || typeof options !== \"object\") {\n throw new TypeError(\n \"[defer] expected an object with `critical` and `deferred` fields\",\n );\n }\n\n if (\n options.deferred === null ||\n typeof options.deferred !== \"object\" ||\n Array.isArray(options.deferred)\n ) {\n throw new TypeError(\n \"[defer] `deferred` must be a non-null, non-array object of promises\",\n );\n }\n\n for (const [key, value] of Object.entries(options.deferred)) {\n // Reserved keys would corrupt the prototype chain when the client-side\n // plugin reconstructs the deferred map via `[key] = ensureRegistryPromise(key)`.\n // The reconstruction path uses a null-prototype object as a defence-in-depth\n // measure, but rejecting these keys upstream keeps the wire-format\n // symmetric (server-side payload === client-side reconstruction).\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n throw new TypeError(\n `[defer] \\`deferred.${key}\\` is reserved — choose a different key`,\n );\n }\n\n if (\n value === null ||\n typeof value !== \"object\" ||\n typeof (value as { then?: unknown }).then !== \"function\"\n ) {\n throw new TypeError(\n `[defer] \\`deferred.${key}\\` must be a Promise (got ${typeof value})`,\n );\n }\n\n // Defensive sibling-handler: an eagerly-rejected promise (e.g.\n // `Promise.reject(new Error(...))` synchronously inside the loader)\n // races the server-side `injectDeferredScripts` `.then(...)`\n // attachment. Without a handler attached at construction time, Node\n // emits an `unhandledRejection` warning before the wire-format\n // settler can register. The no-op `.catch` does not consume the\n // rejection — it only marks the promise as \"handled\" for Node's\n // tracker, so the real settler still observes the rejection and\n // emits the `__rrDeferError__` script.\n //\n // Duck-typed thenables (no `.catch`) are skipped: Node's\n // unhandledRejection tracker only fires for native Promise objects,\n // so non-Promise thenables don't need the suppression anyway.\n const maybeCatch = (value as { catch?: unknown }).catch;\n\n if (typeof maybeCatch === \"function\") {\n (value as Promise<unknown>).catch(() => {\n /* no-op — see comment above */\n });\n }\n }\n\n // Freeze a *shallow clone* of the deferred map (rather than the user's\n // own reference) so:\n // 1. `Object.freeze` doesn't surprise the caller by freezing an object\n // they still hold a reference to.\n // 2. Post-`defer()` mutations to the user's original map (e.g.\n // `userMap.evil = somePromise`) cannot smuggle in entries that\n // bypass the validation/`.catch` loop above. Without this, a late\n // `userMap.__proto__ = …` or an eagerly-rejected promise added\n // after this call would land in `injectDeferredScripts` unchecked.\n // The clone is shallow — promise references are preserved, so the\n // settle pipeline observes the same Promise instances the validator\n // examined.\n return Object.freeze({\n critical: options.critical,\n deferred: Object.freeze({ ...options.deferred }) as D,\n [DEFER_BRAND]: true,\n });\n}\n\n/** Type guard — `true` iff `value` is a payload returned by `defer()`.\n *\n * The brand check uses `Object.hasOwn(value, DEFER_BRAND)` rather than a\n * plain property read so a prototype-chain inheritance bypass —\n * `Object.create({ [DEFER_BRAND]: true })` — does not falsely tag an\n * object as a deferred payload. The brand symbol is a `Symbol.for(...)`,\n * so a brand-marked object inherited by accident from a foreign realm\n * could otherwise sneak past `defer()`'s validation and land in\n * `processLoaderResult`'s slow path with no `critical`/`deferred` fields.\n */\nexport function isDeferred(\n value: unknown,\n): value is DeferredPayload<unknown, Record<string, Promise<unknown>>> {\n return (\n value !== null &&\n typeof value === \"object\" &&\n Object.hasOwn(value, DEFER_BRAND) &&\n (value as Record<symbol, unknown>)[DEFER_BRAND] === true\n );\n}\n","import type { Router } from \"@real-router/types\";\n\nconst staleByRouter = new WeakMap<Router, Set<string>>();\n\n/**\n * Mark a context namespace as stale on the given router. The next navigation\n * that lands on a route with a registered loader for this namespace consumes\n * the flag in the SSR loader plugin's `subscribeLeave` handler — runs the\n * loader, overwrites `state.context.<namespace>`, and clears the flag.\n *\n * Idempotent (Set-deduplicated). Survives navigations that cannot refresh:\n * routes without an entry, `client-only` mode, mode-only entries, and\n * cancelled navigations all preserve the flag for the next attempt. The\n * flag is cleared only after the loader successfully runs and writes data.\n *\n * Returns `void` (fire-and-forget). For an explicit synchronous round-trip,\n * compose with the existing core API:\n * ```ts\n * markStale(router, \"data\");\n * await router.navigate(state.name, state.params, { reload: true });\n * ```\n */\nexport function markStale(router: Router, namespace: string): void {\n let set = staleByRouter.get(router);\n\n if (set === undefined) {\n set = new Set<string>();\n staleByRouter.set(router, set);\n }\n\n set.add(namespace);\n}\n\n/** Plugin-internal: peek without consuming. */\nexport function isStale(router: Router, namespace: string): boolean {\n return staleByRouter.get(router)?.has(namespace) ?? false;\n}\n\n/** Plugin-internal: clear the flag (no-op if not set). */\nexport function clearStale(router: Router, namespace: string): void {\n staleByRouter.get(router)?.delete(namespace);\n}\n","import type {\n DefaultDependencies,\n Params,\n Router,\n State,\n} from \"@real-router/types\";\n\nexport type SsrMode = \"full\" | \"data-only\" | \"client-only\";\n\nexport const ALL_SSR_MODES: readonly SsrMode[] = [\n \"full\",\n \"data-only\",\n \"client-only\",\n];\n\n/**\n * Resolves the SSR mode for a route per-navigation.\n *\n * Receives the resolved post-routing `state` (with `name`, `params`, `path`)\n * and returns one of the allowed `SsrMode` values for the host plugin.\n *\n * The resolver is invoked **before** the plugin writes the mode marker to\n * `state.context.<modeNamespace>`, so reading `state.context.ssrDataMode` /\n * `state.context.ssrRscMode` here yields `undefined`. Branch on\n * `state.params`, `state.path`, or `state.name` instead.\n *\n * Throwing from the resolver propagates through `start()` (standard\n * navigation error pipeline) — no partial mode write occurs. Returning a\n * value outside the host plugin's `allowedModes` rejects with a typed\n * `TypeError` at runtime.\n */\nexport type SsrModeResolver<M extends SsrMode = SsrMode> = (state: State) => M;\n\nexport type SsrModeConfig<M extends SsrMode = SsrMode> =\n | M\n | boolean\n | SsrModeResolver<M>;\n\n/**\n * Optional context object passed to the loader. The `signal` field is the\n * navigation's `AbortController.signal` when the plugin's `subscribeLeave`\n * handler invokes the loader (#605 `invalidate()` → CSR refresh path);\n * `undefined` from the `start` interceptor (SSR boot path — apps that need\n * a request-scoped signal use `getDep(\"abortSignal\")` injected via\n * `cloneRouter(base, { abortSignal })`, see `createRequestScope` and\n * `withTimeout({ upstreamSignal })` patterns).\n *\n * Loaders ignoring the second argument remain compatible (TypeScript\n * contravariance).\n */\nexport interface SsrLoaderContext {\n signal: AbortSignal;\n}\n\nexport type SsrLoaderFn<T> = (\n params: Params,\n context?: SsrLoaderContext,\n) => Promise<T> | T;\n\nexport type SsrLoaderFnFactory<\n T,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> = (\n router: Router<Dependencies>,\n getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K],\n) => SsrLoaderFn<T>;\n\nexport interface SsrRouteEntryObject<\n T,\n M extends SsrMode = SsrMode,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n ssr?: SsrModeConfig<M>;\n loader?: SsrLoaderFnFactory<T, Dependencies>;\n}\n\nexport type SsrRouteEntry<\n T,\n M extends SsrMode = SsrMode,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> =\n | SsrLoaderFnFactory<T, Dependencies>\n | SsrRouteEntryObject<T, M, Dependencies>;\n\nexport type SsrLoaderFactoryMap<\n T,\n M extends SsrMode = SsrMode,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> = Record<string, SsrRouteEntry<T, M, Dependencies>>;\n\nexport interface SsrLoaderPluginConfig {\n namespace: string;\n modeNamespace: string;\n errorPrefix: string;\n allowedModes?: readonly SsrMode[];\n /**\n * When set, the plugin recognises `defer()` payloads from loaders.\n * Critical data is written to `<namespace>`, deferred promises to\n * `<deferredNamespace>`, and the deferred key list (for client-side\n * registry hydration) to `<deferredKeysNamespace>`.\n *\n * Both fields must be set together; one without the other rejects at\n * factory-time.\n */\n deferredNamespace?: string;\n deferredKeysNamespace?: string;\n}\n","import { getPluginApi } from \"@real-router/core/api\";\nimport { getInternals } from \"@real-router/core/validation\";\n\nimport { isDeferred } from \"./defer.js\";\nimport { ensureRegistryPromise } from \"./deferRegistry.js\";\nimport { clearStale, isStale } from \"./staleRegistry.js\";\nimport { ALL_SSR_MODES } from \"./types.js\";\n\nimport type {\n SsrLoaderFactoryMap,\n SsrLoaderFn,\n SsrLoaderPluginConfig,\n SsrMode,\n SsrModeConfig,\n} from \"./types.js\";\nimport type {\n ContextNamespaceClaim,\n DefaultDependencies,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/types\";\n\ninterface CompiledEntry<T> {\n /**\n * Pre-resolved mode for static `ssr` configs (undefined / boolean /\n * string). `null` marker means \"function-form resolver — must call\n * `resolveMode(modeFn, state, …)` at navigation time\". Pre-computing\n * skips the `resolveMode` walk on every `start()` + every stale-flag\n * leave handler invocation for the common static-config case.\n */\n staticMode: SsrMode | null;\n /**\n * Function-form mode resolver. Defined ONLY when `obj.ssr` is a\n * function; `undefined` for static forms (where `staticMode` is\n * authoritative). Kept as a typed field rather than reusing the\n * raw `obj.ssr` so the prepareEntry call site avoids a `typeof`\n * branch per navigation.\n */\n modeFn: ((state: State) => SsrMode) | undefined;\n loader: SsrLoaderFn<T> | undefined;\n}\n\n/**\n * Compile a `SsrLoaderFactoryMap` into a `Map<name, CompiledEntry>`.\n *\n * Extracted from the inline `for (const [name, raw] of …)` body that lived\n * inside `createSsrLoaderPlugin` so the main function reads top-down:\n * claims are acquired, compilation runs against this helper, and any throw\n * bubbles to the shared `rollback()` path. Tested in isolation by the same\n * functional + property suites that pin the previous inline behaviour.\n *\n * The compile step is pure — it touches no router state other than via the\n * caller-provided `router` + `getDependency` arguments, and it only walks\n * own-enumerable entries (`Object.entries`) so prototype pollution stays\n * structurally impossible.\n *\n * Mode pre-resolution: static `ssr` forms (`undefined` / boolean / string)\n * are resolved here at compile time and cached as `staticMode`. The\n * runtime path in `prepareEntry` then reuses the cached value on every\n * `start()` + stale-flag leave handler invocation, skipping the\n * `resolveMode` if/else chain. Function-form `ssr` keeps a typed\n * `modeFn` for per-navigation evaluation.\n */\nfunction compile<\n T,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n loaders: SsrLoaderFactoryMap<T, SsrMode, Dependencies>,\n router: Router<Dependencies>,\n getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K],\n errorPrefix: string,\n allowed: readonly SsrMode[],\n): Map<string, CompiledEntry<T>> {\n const compiled = new Map<string, CompiledEntry<T>>();\n\n for (const [name, raw] of Object.entries(loaders)) {\n const obj = typeof raw === \"function\" ? { loader: raw } : raw;\n\n let loader: SsrLoaderFn<T> | undefined;\n\n if (obj.loader !== undefined) {\n const fn = obj.loader(router, getDependency);\n\n if (typeof fn !== \"function\") {\n throw new TypeError(\n `${errorPrefix} factory for route \"${name}\" must return a function`,\n );\n }\n\n loader = fn;\n }\n\n // Pre-resolve static modes; defer function-form to navigation-time.\n // The `resolveMode` runtime helper still validates function-form\n // returns AND any forms that the validator passed but createSsrLoaderPlugin's\n // narrower `allowedModes` rejects (consumer-specific allow-list).\n let staticMode: SsrMode | null = null;\n let modeFn: ((state: State) => SsrMode) | undefined;\n\n if (typeof obj.ssr === \"function\") {\n modeFn = obj.ssr;\n } else {\n // Static — undefined/true/false/string. Pass a synthetic state;\n // resolveMode ignores `state` for non-function forms.\n staticMode = resolveMode(\n obj.ssr,\n SYNTHETIC_STATE,\n allowed,\n errorPrefix,\n name,\n );\n }\n\n compiled.set(name, { staticMode, modeFn, loader });\n }\n\n return compiled;\n}\n\n// Placeholder state for compile-time static-mode resolution. The\n// resolveMode function reads `state` only for the function-form branch,\n// so any non-null reference works for the static branches. Kept module-\n// level so all compile() calls share one allocation.\nconst SYNTHETIC_STATE = {\n name: \"\",\n params: {},\n path: \"\",\n transition: {\n phase: \"activating\",\n reason: \"success\",\n segments: { deactivated: [], activated: [], intersection: \"\" },\n },\n context: {},\n} as unknown as State;\n\nfunction rejectMode(\n value: unknown,\n allowed: readonly SsrMode[],\n prefix: string,\n route: string,\n): never {\n throw new TypeError(\n `${prefix} mode \"${String(value)}\" is not allowed for route \"${route}\". Allowed: ${allowed.join(\", \")}`,\n );\n}\n\nfunction resolveMode(\n ssr: SsrModeConfig | undefined,\n state: State,\n allowed: readonly SsrMode[],\n prefix: string,\n route: string,\n): SsrMode {\n if (ssr === undefined || ssr === true) return \"full\";\n\n if (ssr === false) {\n if (!allowed.includes(\"client-only\")) {\n rejectMode(\"client-only\", allowed, prefix, route);\n }\n\n return \"client-only\";\n }\n\n const value = typeof ssr === \"function\" ? ssr(state) : ssr;\n\n if (typeof value !== \"string\" || !allowed.includes(value as SsrMode)) {\n rejectMode(value, allowed, prefix, route);\n }\n\n return value;\n}\n\nexport function createSsrLoaderPlugin<\n T,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n loaders: SsrLoaderFactoryMap<T, SsrMode, Dependencies>,\n config: SsrLoaderPluginConfig,\n): PluginFactory<Dependencies> {\n if (\n (config.deferredNamespace !== undefined) !==\n (config.deferredKeysNamespace !== undefined)\n ) {\n throw new TypeError(\n `${config.errorPrefix} \\`deferredNamespace\\` and \\`deferredKeysNamespace\\` must be set together`,\n );\n }\n\n // Bundle the two namespace strings into a single nullable object so\n // downstream code narrows via `if (deferredConfig !== null)` instead\n // of the `config.deferredNamespace!` non-null assertion that TS can't\n // derive from the XOR check above.\n const deferredConfig =\n config.deferredNamespace !== undefined &&\n config.deferredKeysNamespace !== undefined\n ? {\n valueNamespace: config.deferredNamespace,\n keysNamespace: config.deferredKeysNamespace,\n }\n : null;\n\n return (router, getDependency): Plugin => {\n const api = getPluginApi(router);\n const allowed = config.allowedModes ?? ALL_SSR_MODES;\n\n // Sequential claim acquisition with all-or-nothing rollback. Any\n // failure (collision, validation error during compile loop) releases\n // every claim acquired so far and rethrows. This replaces the\n // previous 4 nested try/catch blocks with progressively-longer\n // release lists — same semantics, one shared rollback path.\n const acquired: ContextNamespaceClaim[] = [];\n const claim = (namespace: string): ContextNamespaceClaim => {\n const c = api.claimContextNamespace(namespace);\n acquired.push(c);\n return c;\n };\n const rollback = (): void => {\n for (const c of acquired) c.release();\n };\n\n let dataClaim: ContextNamespaceClaim;\n let modeClaim: ContextNamespaceClaim;\n let deferredClaims: {\n value: ContextNamespaceClaim;\n keys: ContextNamespaceClaim;\n } | null = null;\n let compiled: Map<string, CompiledEntry<T>>;\n\n try {\n dataClaim = claim(config.namespace);\n modeClaim = claim(config.modeNamespace);\n\n if (deferredConfig !== null) {\n deferredClaims = {\n value: claim(deferredConfig.valueNamespace),\n keys: claim(deferredConfig.keysNamespace),\n };\n }\n\n compiled = compile(\n loaders,\n router,\n getDependency,\n config.errorPrefix,\n allowed,\n );\n } catch (error) {\n rollback();\n\n throw error;\n }\n\n const internals = getInternals(router);\n\n // Hot path on every successful start() / subscribeLeave refresh. The\n // previous shape ran a `processLoaderResult` helper that always allocated\n // a `{ critical, deferred }` wrapper object — wasted on the common\n // plain-data path (and on every call from `rsc-server-plugin`, which\n // never opts into deferred support). Inlining the branch keeps the\n // fast path allocation-free and the slow path (defer payload) at one\n // intentional `Object.keys(...)` array allocation per loader.\n const writeLoaderResult = (state: State, value: T): void => {\n if (deferredClaims !== null && isDeferred(value)) {\n dataClaim.write(state, value.critical as T);\n deferredClaims.value.write(state, value.deferred);\n deferredClaims.keys.write(state, Object.keys(value.deferred));\n\n return;\n }\n\n dataClaim.write(state, value);\n };\n\n const reconstructDeferredFromHydration = (\n state: State,\n hydrated: Record<string, unknown>,\n ): void => {\n if (deferredConfig === null || deferredClaims === null) return;\n\n const keysRaw = hydrated[deferredConfig.keysNamespace];\n\n if (!Array.isArray(keysRaw)) return;\n\n const keys = keysRaw.filter(\n (k): k is string =>\n typeof k === \"string\" &&\n // Defensive: drop reserved keys that would corrupt the prototype\n // chain when assigned via `[key] = …`. `{ __proto__: x }` literal\n // does the same thing and would trigger the setter on the fresh\n // object below — turning useDeferred(\"then\") into a function ref\n // pulled from Promise.prototype. With a null-prototype object\n // (below) `__proto__` is just a property, but skipping these\n // keys outright keeps the surface predictable.\n k !== \"__proto__\" &&\n k !== \"constructor\" &&\n k !== \"prototype\",\n );\n\n if (keys.length === 0) return;\n\n // Null-prototype object so `[key] = …` cannot trigger the\n // `Object.prototype.__proto__` setter, even if the filter above is\n // bypassed by future refactors.\n const promises = Object.create(null) as Record<\n string,\n Promise<unknown>\n >;\n\n for (const key of keys) {\n promises[key] = ensureRegistryPromise(key);\n }\n\n deferredClaims.value.write(state, promises);\n deferredClaims.keys.write(state, keys);\n };\n\n // Shared between start interceptor (SSR boot path) and subscribeLeave\n // handler (CSR revalidation path). Returns the compiled entry only\n // when:\n // 1. the route is registered in this plugin's loaders map, AND\n // 2. the resolved mode is NOT \"client-only\".\n // In both successful cases the mode marker is published to\n // `state.context[modeNamespace]` BEFORE returning. Callers then own\n // the loader-invocation strategy (start path also checks the hydration\n // scratchpad; leave path gates on `entry.loader !== undefined`).\n const prepareEntry = (state: State): CompiledEntry<T> | null => {\n const entry = compiled.get(state.name);\n\n if (!entry) return null;\n\n // Static forms (the common case) — staticMode was pre-resolved at\n // compile time, skip the resolveMode if/else walk per navigation.\n // Function-form path: invoke modeFn with the resolved state and\n // re-validate via resolveMode (catches a resolver returning a\n // foreign string at runtime).\n const mode =\n entry.staticMode !== null\n ? entry.staticMode\n : resolveMode(\n entry.modeFn,\n state,\n allowed,\n config.errorPrefix,\n state.name,\n );\n\n modeClaim.write(state, mode);\n\n if (mode === \"client-only\") return null;\n\n return entry;\n };\n\n const removeStartInterceptor = api.addInterceptor(\n \"start\",\n async (next, path) => {\n const state = await next(path);\n const entry = prepareEntry(state);\n\n if (entry === null) return state;\n\n const hydrationState = internals.hydrationState;\n\n if (\n hydrationState !== null &&\n hydrationState.name === state.name &&\n // `in` — not `!== undefined` — is intentional. The contract is\n // \"scratchpad presence wins\": if the server explicitly serialised\n // a value into this namespace (even an `undefined` left over from\n // a programmatic state object), the plugin treats that as the\n // server's authoritative answer and skips re-running the loader\n // on the client. JSON-roundtrip strips `undefined` values, so in\n // practice this only matters for in-memory hydration paths —\n // see CLAUDE.md \"Gotchas → Hydration scratchpad: presence wins\".\n config.namespace in hydrationState.context\n ) {\n dataClaim.write(state, hydrationState.context[config.namespace] as T);\n reconstructDeferredFromHydration(state, hydrationState.context);\n } else if (entry.loader !== undefined) {\n writeLoaderResult(state, await entry.loader(state.params));\n }\n\n return state;\n },\n );\n\n // CSR revalidation channel for `invalidate(router, namespace)`.\n // Runs in the awaited LEAVE_APPROVE phase so fresh data lands on\n // `nextRoute.context` before `TRANSITION_SUCCESS` fires.\n // Flag is cleared only after a successful, non-cancelled loader write —\n // no-entry / client-only / cancelled navigations preserve it for retry.\n const removeLeaveListener = router.subscribeLeave(\n async ({ nextRoute, signal }) => {\n if (!isStale(router, config.namespace)) return;\n\n const entry = prepareEntry(nextRoute);\n\n if (entry === null || entry.loader === undefined) return;\n\n // Pass the navigation's signal so cancellation-aware loaders can\n // abort their in-flight work (fetch, DB query, etc.) when a newer\n // navigation supersedes this one. The post-await `signal.aborted`\n // check below remains as the final gate — loaders that ignore the\n // signal still benefit from the cancel-safety contract (#605).\n const data = await entry.loader(nextRoute.params, { signal });\n\n if (signal.aborted) return;\n\n clearStale(router, config.namespace);\n writeLoaderResult(nextRoute, data);\n },\n );\n\n return {\n teardown() {\n removeStartInterceptor();\n removeLeaveListener();\n dataClaim.release();\n modeClaim.release();\n deferredClaims?.value.release();\n deferredClaims?.keys.release();\n },\n };\n };\n}\n","import { ALL_SSR_MODES } from \"./types.js\";\n\nimport type { SsrMode } from \"./types.js\";\n\nexport function createLoadersValidator(\n errorPrefix: string,\n allowedModes: readonly SsrMode[] = ALL_SSR_MODES,\n) {\n return function validateLoaders(loaders: unknown): void {\n if (\n loaders === null ||\n typeof loaders !== \"object\" ||\n Array.isArray(loaders)\n ) {\n throw new TypeError(`${errorPrefix} loaders must be a non-null object`);\n }\n\n for (const [route, entry] of Object.entries(\n loaders as Record<string, unknown>,\n )) {\n if (typeof entry === \"function\") continue;\n\n if (entry === null || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new TypeError(\n `${errorPrefix} entry for route \"${route}\" must be a function or { ssr?, loader? } object`,\n );\n }\n\n for (const key of Object.keys(entry as Record<string, unknown>)) {\n if (key !== \"ssr\" && key !== \"loader\") {\n throw new TypeError(\n `${errorPrefix} unexpected key \"${key}\" in route \"${route}\" config`,\n );\n }\n }\n\n const obj = entry as { ssr?: unknown; loader?: unknown };\n\n if (obj.loader !== undefined && typeof obj.loader !== \"function\") {\n throw new TypeError(\n `${errorPrefix} loader for route \"${route}\" must be a function`,\n );\n }\n\n if (obj.ssr !== undefined) {\n const ssr = obj.ssr;\n\n if (typeof ssr === \"function\" || typeof ssr === \"boolean\") {\n continue;\n }\n\n if (typeof ssr === \"string\") {\n if (!(allowedModes as readonly string[]).includes(ssr)) {\n throw new TypeError(\n `${errorPrefix} mode \"${ssr}\" is not allowed for route \"${route}\". Allowed: ${allowedModes.join(\", \")}`,\n );\n }\n continue;\n }\n\n throw new TypeError(\n `${errorPrefix} ssr for route \"${route}\" must be SsrMode string, boolean, or (state) => SsrMode`,\n );\n }\n }\n };\n}\n","import { ERROR_PREFIX } from \"./constants\";\nimport { createLoadersValidator, createSsrLoaderPlugin } from \"./shared-ssr\";\n\nimport type { DataLoaderFactoryMap } from \"./types\";\nimport type { DefaultDependencies, PluginFactory } from \"@real-router/types\";\n\n// Inlined binding — symmetric with rsc-server-plugin's same merge.\nconst validateLoaders = createLoadersValidator(ERROR_PREFIX);\n\nexport function ssrDataPluginFactory<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(loaders: DataLoaderFactoryMap<Dependencies>): PluginFactory<Dependencies> {\n validateLoaders(loaders);\n\n return createSsrLoaderPlugin<unknown, Dependencies>(loaders, {\n namespace: \"data\",\n modeNamespace: \"ssrDataMode\",\n deferredNamespace: \"ssrDataDeferred\",\n deferredKeysNamespace: \"ssrDataDeferredKeys\",\n errorPrefix: ERROR_PREFIX,\n });\n}\n","import { ALL_SSR_MODES, type SsrMode } from \"./shared-ssr\";\n\nimport type { State } from \"@real-router/types\";\n\n/**\n * Returns the SSR mode resolved by `ssr-data-plugin` for the current state.\n * Falls back to `\"full\"` when the route has no plugin entry.\n *\n * Read this from `entry-server.tsx` to branch on full / data-only / client-only:\n * - `\"full\"` — render the full app, ship JSON + HTML.\n * - `\"data-only\"` — ship JSON only, render shell HTML.\n * - `\"client-only\"` — ship shell HTML and let the client fetch via its own mechanism.\n *\n * The mode is written to `state.context.ssrDataMode` by the plugin's `start`\n * interceptor for every route registered in the loaders map.\n *\n * Defensive read: if `state.context.ssrDataMode` was set to something outside\n * `ALL_SSR_MODES` by a TS-cast bypass or a foreign writer, the function\n * collapses it to `\"full\"` rather than returning the bad value. Without this\n * guard, a downstream `mode === \"full\"` branch would silently misbehave for\n * `0`, `false`, `\"\"`, `null`, or any unknown string.\n */\nexport function getSsrDataMode(state: State): SsrMode {\n const raw = (state.context as { ssrDataMode?: unknown }).ssrDataMode;\n\n return typeof raw === \"string\" && ALL_SSR_MODES.includes(raw as SsrMode)\n ? (raw as SsrMode)\n : \"full\";\n}\n","import { markStale } from \"./shared-ssr\";\n\nimport type { Router } from \"@real-router/types\";\n\n/**\n * Mark the `\"data\"` namespace as stale on the given router. The next\n * navigation (including a same-route reload) re-runs the loader for the\n * destination route and overwrites `state.context.data` (and the mode marker)\n * via the plugin's `subscribeLeave` listener.\n *\n * Honest fire-and-forget semantics — returns `void`. The flag is consumed in\n * the awaited LEAVE_APPROVE phase of the next navigation, so subscribers see\n * fresh data when the navigation completes. Behaviour during an in-flight\n * transition: the current transition completes unchanged; the flag is read\n * by the *following* navigation. This keeps the invariant\n * \"one transition = one `state.context` snapshot\" intact.\n *\n * Composability through the existing core API:\n *\n * ```ts\n * // Fire-and-forget: stale until the user navigates somewhere\n * invalidate(router, \"data\");\n *\n * // Explicit await — pair with a same-route reload\n * invalidate(router, \"data\");\n * await router.navigate(state.name, state.params, { reload: true });\n * ```\n *\n * Cheaper than `router.navigate({ reload: true })` alone in two ways:\n * 1. No fake transition fired when the application already has a navigation\n * in flight — the existing one consumes the flag.\n * 2. Surgical: only the `\"data\"` namespace re-runs. Companion plugins (e.g.\n * `rsc-server-plugin`) keep their cached `state.context.rsc` on this same\n * transition unless their own `invalidate()` was also called.\n */\nexport function invalidate(router: Router, namespace: \"data\"): void {\n markStale(router, namespace);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["ensureRegistryPromise"],"sources":["../../src/constants.ts","../../../../shared/ssr/defer.ts","../../../../shared/ssr/staleRegistry.ts","../../../../shared/ssr/types.ts","../../../../shared/ssr/createSsrLoaderPlugin.ts","../../../../shared/ssr/createLoadersValidator.ts","../../src/factory.ts","../../src/getSsrDataMode.ts","../../src/invalidate.ts"],"sourcesContent":["const LOGGER_CONTEXT = \"ssr-data-plugin\";\n\nexport const ERROR_PREFIX = `[@real-router/${LOGGER_CONTEXT}]`;\n","/**\n * Marker symbol for `defer()` payloads. `Symbol.for` is used so the brand\n * survives across multiple module instances (a real concern in monorepo setups\n * with multiple `node_modules/@real-router/ssr-data-plugin` copies).\n */\nexport const DEFER_BRAND: unique symbol = Symbol.for(\n \"@real-router/ssr-data-plugin/defer\",\n);\n\nexport interface DeferredPayload<\n C,\n D extends Record<string, Promise<unknown>>,\n> {\n readonly critical: C;\n readonly deferred: D;\n readonly [DEFER_BRAND]: true;\n}\n\n/**\n * Wraps a loader return value to declare a critical/deferred split.\n *\n * - `critical` resolves before HTML render (blocks the shell).\n * - `deferred` is a record of named promises that the framework can stream\n * independently — `<Suspense>`, `<Await/>`, `{#await}`, etc.\n *\n * The plugin writes `critical` to `state.context.<namespace>` (e.g. `data`)\n * and the deferred promises to `state.context.<namespace>Deferred` (e.g.\n * `ssrDataDeferred`). Adapter-side `useDeferred(key)` reads from the same\n * shape and returns the matching promise for native framework awaiting.\n *\n * On the server: `state.context.ssrDataDeferred[key]` is the actual promise\n * the loader produced. On the client (post-hydration): the plugin reconstructs\n * promises from the global `__rrDeferRegistry__` that inline `__rrDefer__()`\n * scripts populate as the server stream lands.\n */\nexport function defer<\n const C,\n const D extends Record<string, Promise<unknown>>,\n>(options: { readonly critical: C; readonly deferred: D }): DeferredPayload<\n C,\n D\n> {\n if (options === null || typeof options !== \"object\") {\n throw new TypeError(\n \"[defer] expected an object with `critical` and `deferred` fields\",\n );\n }\n\n if (\n options.deferred === null ||\n typeof options.deferred !== \"object\" ||\n Array.isArray(options.deferred)\n ) {\n throw new TypeError(\n \"[defer] `deferred` must be a non-null, non-array object of promises\",\n );\n }\n\n for (const [key, value] of Object.entries(options.deferred)) {\n // Reserved keys would corrupt the prototype chain when the client-side\n // plugin reconstructs the deferred map via `[key] = ensureRegistryPromise(key)`.\n // The reconstruction path uses a null-prototype object as a defence-in-depth\n // measure, but rejecting these keys upstream keeps the wire-format\n // symmetric (server-side payload === client-side reconstruction).\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n throw new TypeError(\n `[defer] \\`deferred.${key}\\` is reserved — choose a different key`,\n );\n }\n\n if (\n value === null ||\n typeof value !== \"object\" ||\n typeof (value as { then?: unknown }).then !== \"function\"\n ) {\n throw new TypeError(\n `[defer] \\`deferred.${key}\\` must be a Promise (got ${typeof value})`,\n );\n }\n\n // Defensive sibling-handler: an eagerly-rejected promise (e.g.\n // `Promise.reject(new Error(...))` synchronously inside the loader)\n // races the server-side `injectDeferredScripts` `.then(...)`\n // attachment. Without a handler attached at construction time, Node\n // emits an `unhandledRejection` warning before the wire-format\n // settler can register. The no-op `.catch` does not consume the\n // rejection — it only marks the promise as \"handled\" for Node's\n // tracker, so the real settler still observes the rejection and\n // emits the `__rrDeferError__` script.\n //\n // Duck-typed thenables (no `.catch`) are skipped: Node's\n // unhandledRejection tracker only fires for native Promise objects,\n // so non-Promise thenables don't need the suppression anyway.\n const maybeCatch = (value as { catch?: unknown }).catch;\n\n if (typeof maybeCatch === \"function\") {\n (value as Promise<unknown>).catch(() => {\n /* no-op — see comment above */\n });\n }\n }\n\n // Freeze a *shallow clone* of the deferred map (rather than the user's\n // own reference) so:\n // 1. `Object.freeze` doesn't surprise the caller by freezing an object\n // they still hold a reference to.\n // 2. Post-`defer()` mutations to the user's original map (e.g.\n // `userMap.evil = somePromise`) cannot smuggle in entries that\n // bypass the validation/`.catch` loop above. Without this, a late\n // `userMap.__proto__ = …` or an eagerly-rejected promise added\n // after this call would land in `injectDeferredScripts` unchecked.\n // The clone is shallow — promise references are preserved, so the\n // settle pipeline observes the same Promise instances the validator\n // examined.\n return Object.freeze({\n critical: options.critical,\n deferred: Object.freeze({ ...options.deferred }) as D,\n [DEFER_BRAND]: true,\n });\n}\n\n/** Type guard — `true` iff `value` is a payload returned by `defer()`.\n *\n * The brand check uses `Object.hasOwn(value, DEFER_BRAND)` rather than a\n * plain property read so a prototype-chain inheritance bypass —\n * `Object.create({ [DEFER_BRAND]: true })` — does not falsely tag an\n * object as a deferred payload. The brand symbol is a `Symbol.for(...)`,\n * so a brand-marked object inherited by accident from a foreign realm\n * could otherwise sneak past `defer()`'s validation and land in\n * `processLoaderResult`'s slow path with no `critical`/`deferred` fields.\n */\nexport function isDeferred(\n value: unknown,\n): value is DeferredPayload<unknown, Record<string, Promise<unknown>>> {\n return (\n value !== null &&\n typeof value === \"object\" &&\n Object.hasOwn(value, DEFER_BRAND) &&\n (value as Record<symbol, unknown>)[DEFER_BRAND] === true\n );\n}\n","import type { Router } from \"@real-router/types\";\n\nconst staleByRouter = new WeakMap<Router, Set<string>>();\n\n/**\n * Mark a context namespace as stale on the given router. The next navigation\n * that lands on a route with a registered loader for this namespace consumes\n * the flag in the SSR loader plugin's `subscribeLeave` handler — runs the\n * loader, overwrites `state.context.<namespace>`, and clears the flag.\n *\n * Idempotent (Set-deduplicated). Survives navigations that cannot refresh:\n * routes without an entry, `client-only` mode, mode-only entries, and\n * cancelled navigations all preserve the flag for the next attempt. The\n * flag is cleared only after the loader successfully runs and writes data.\n *\n * Returns `void` (fire-and-forget). For an explicit synchronous round-trip,\n * compose with the existing core API:\n * ```ts\n * markStale(router, \"data\");\n * await router.navigate(state.name, state.params, { reload: true });\n * ```\n */\nexport function markStale(router: Router, namespace: string): void {\n let set = staleByRouter.get(router);\n\n if (set === undefined) {\n set = new Set<string>();\n staleByRouter.set(router, set);\n }\n\n set.add(namespace);\n}\n\n/** Plugin-internal: peek without consuming. */\nexport function isStale(router: Router, namespace: string): boolean {\n return staleByRouter.get(router)?.has(namespace) ?? false;\n}\n\n/** Plugin-internal: clear the flag (no-op if not set). */\nexport function clearStale(router: Router, namespace: string): void {\n staleByRouter.get(router)?.delete(namespace);\n}\n","import type {\n DefaultDependencies,\n Params,\n Router,\n State,\n} from \"@real-router/types\";\n\nexport type SsrMode = \"full\" | \"data-only\" | \"client-only\";\n\nexport const ALL_SSR_MODES: readonly SsrMode[] = [\n \"full\",\n \"data-only\",\n \"client-only\",\n];\n\n/**\n * Resolves the SSR mode for a route per-navigation.\n *\n * Receives the resolved post-routing `state` (with `name`, `params`, `path`)\n * and returns one of the allowed `SsrMode` values for the host plugin.\n *\n * The resolver is invoked **before** the plugin writes the mode marker to\n * `state.context.<modeNamespace>`, so reading `state.context.ssrDataMode` /\n * `state.context.ssrRscMode` here yields `undefined`. Branch on\n * `state.params`, `state.path`, or `state.name` instead.\n *\n * Throwing from the resolver propagates through `start()` (standard\n * navigation error pipeline) — no partial mode write occurs. Returning a\n * value outside the host plugin's `allowedModes` rejects with a typed\n * `TypeError` at runtime.\n */\nexport type SsrModeResolver<M extends SsrMode = SsrMode> = (state: State) => M;\n\nexport type SsrModeConfig<M extends SsrMode = SsrMode> =\n | M\n | boolean\n | SsrModeResolver<M>;\n\n/**\n * Optional context object passed to the loader. The `signal` field is the\n * navigation's `AbortController.signal` when the plugin's `subscribeLeave`\n * handler invokes the loader (#605 `invalidate()` → CSR refresh path);\n * `undefined` from the `start` interceptor (SSR boot path — apps that need\n * a request-scoped signal use `getDep(\"abortSignal\")` injected via\n * `cloneRouter(base, { abortSignal })`, see `createRequestScope` and\n * `withTimeout({ upstreamSignal })` patterns).\n *\n * Loaders ignoring the second argument remain compatible (TypeScript\n * contravariance).\n */\nexport interface SsrLoaderContext {\n signal: AbortSignal;\n}\n\nexport type SsrLoaderFn<T> = (\n params: Params,\n context?: SsrLoaderContext,\n) => Promise<T> | T;\n\nexport type SsrLoaderFnFactory<\n T,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> = (\n router: Router<Dependencies>,\n getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K],\n) => SsrLoaderFn<T>;\n\nexport interface SsrRouteEntryObject<\n T,\n M extends SsrMode = SsrMode,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n ssr?: SsrModeConfig<M>;\n loader?: SsrLoaderFnFactory<T, Dependencies>;\n}\n\nexport type SsrRouteEntry<\n T,\n M extends SsrMode = SsrMode,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> =\n | SsrLoaderFnFactory<T, Dependencies>\n | SsrRouteEntryObject<T, M, Dependencies>;\n\nexport type SsrLoaderFactoryMap<\n T,\n M extends SsrMode = SsrMode,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> = Record<string, SsrRouteEntry<T, M, Dependencies>>;\n\nexport interface SsrLoaderPluginConfig {\n namespace: string;\n modeNamespace: string;\n errorPrefix: string;\n allowedModes?: readonly SsrMode[];\n /**\n * When set, the plugin recognises `defer()` payloads from loaders.\n * Critical data is written to `<namespace>`, deferred promises to\n * `<deferredNamespace>`, and the deferred key list (for client-side\n * registry hydration) to `<deferredKeysNamespace>`.\n *\n * Both fields must be set together; one without the other rejects at\n * factory-time.\n */\n deferredNamespace?: string;\n deferredKeysNamespace?: string;\n}\n","import { getPluginApi } from \"@real-router/core/api\";\nimport { getInternals } from \"@real-router/core/validation\";\n\nimport { isDeferred } from \"./defer.js\";\nimport { ensureRegistryPromise } from \"./deferRegistry.js\";\nimport { clearStale, isStale } from \"./staleRegistry.js\";\nimport { ALL_SSR_MODES } from \"./types.js\";\n\nimport type {\n SsrLoaderFactoryMap,\n SsrLoaderFn,\n SsrLoaderPluginConfig,\n SsrMode,\n SsrModeConfig,\n} from \"./types.js\";\nimport type {\n ContextNamespaceClaim,\n DefaultDependencies,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/types\";\n\ninterface CompiledEntry<T> {\n /**\n * Pre-resolved mode for static `ssr` configs (undefined / boolean /\n * string). `null` marker means \"function-form resolver — must call\n * `resolveMode(modeFn, state, …)` at navigation time\". Pre-computing\n * skips the `resolveMode` walk on every `start()` + every stale-flag\n * leave handler invocation for the common static-config case.\n */\n staticMode: SsrMode | null;\n /**\n * Function-form mode resolver. Defined ONLY when `obj.ssr` is a\n * function; `undefined` for static forms (where `staticMode` is\n * authoritative). Kept as a typed field rather than reusing the\n * raw `obj.ssr` so the prepareEntry call site avoids a `typeof`\n * branch per navigation.\n */\n modeFn: ((state: State) => SsrMode) | undefined;\n loader: SsrLoaderFn<T> | undefined;\n}\n\n/**\n * Compile a `SsrLoaderFactoryMap` into a `Map<name, CompiledEntry>`.\n *\n * Extracted from the inline `for (const [name, raw] of …)` body that lived\n * inside `createSsrLoaderPlugin` so the main function reads top-down:\n * claims are acquired, compilation runs against this helper, and any throw\n * bubbles to the shared `rollback()` path. Tested in isolation by the same\n * functional + property suites that pin the previous inline behaviour.\n *\n * The compile step is pure — it touches no router state other than via the\n * caller-provided `router` + `getDependency` arguments, and it only walks\n * own-enumerable entries (`Object.entries`) so prototype pollution stays\n * structurally impossible.\n *\n * Mode pre-resolution: static `ssr` forms (`undefined` / boolean / string)\n * are resolved here at compile time and cached as `staticMode`. The\n * runtime path in `prepareEntry` then reuses the cached value on every\n * `start()` + stale-flag leave handler invocation, skipping the\n * `resolveMode` if/else chain. Function-form `ssr` keeps a typed\n * `modeFn` for per-navigation evaluation.\n */\nfunction compile<\n T,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n loaders: SsrLoaderFactoryMap<T, SsrMode, Dependencies>,\n router: Router<Dependencies>,\n getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K],\n errorPrefix: string,\n allowed: readonly SsrMode[],\n): Map<string, CompiledEntry<T>> {\n const compiled = new Map<string, CompiledEntry<T>>();\n\n for (const [name, raw] of Object.entries(loaders)) {\n const obj = typeof raw === \"function\" ? { loader: raw } : raw;\n\n let loader: SsrLoaderFn<T> | undefined;\n\n if (obj.loader !== undefined) {\n const fn = obj.loader(router, getDependency);\n\n if (typeof fn !== \"function\") {\n throw new TypeError(\n `${errorPrefix} factory for route \"${name}\" must return a function`,\n );\n }\n\n loader = fn;\n }\n\n // Pre-resolve static modes; defer function-form to navigation-time.\n // The `resolveMode` runtime helper still validates function-form\n // returns AND any forms that the validator passed but createSsrLoaderPlugin's\n // narrower `allowedModes` rejects (consumer-specific allow-list).\n let staticMode: SsrMode | null = null;\n let modeFn: ((state: State) => SsrMode) | undefined;\n\n if (typeof obj.ssr === \"function\") {\n modeFn = obj.ssr;\n } else {\n // Static — undefined/true/false/string. Pass a synthetic state;\n // resolveMode ignores `state` for non-function forms.\n staticMode = resolveMode(\n obj.ssr,\n SYNTHETIC_STATE,\n allowed,\n errorPrefix,\n name,\n );\n }\n\n compiled.set(name, { staticMode, modeFn, loader });\n }\n\n return compiled;\n}\n\n// Placeholder state for compile-time static-mode resolution. The\n// resolveMode function reads `state` only for the function-form branch,\n// so any non-null reference works for the static branches. Kept module-\n// level so all compile() calls share one allocation.\nconst SYNTHETIC_STATE = {\n name: \"\",\n params: {},\n path: \"\",\n transition: {\n phase: \"activating\",\n reason: \"success\",\n segments: { deactivated: [], activated: [], intersection: \"\" },\n },\n context: {},\n} as unknown as State;\n\nfunction rejectMode(\n value: unknown,\n allowed: readonly SsrMode[],\n prefix: string,\n route: string,\n): never {\n throw new TypeError(\n `${prefix} mode \"${String(value)}\" is not allowed for route \"${route}\". Allowed: ${allowed.join(\", \")}`,\n );\n}\n\nfunction resolveMode(\n ssr: SsrModeConfig | undefined,\n state: State,\n allowed: readonly SsrMode[],\n prefix: string,\n route: string,\n): SsrMode {\n if (ssr === undefined || ssr === true) return \"full\";\n\n // `ssr: false` always means client-only. Both consumers of this factory\n // (ssr-data-plugin: all modes; rsc-server-plugin: [\"full\", \"client-only\"])\n // permit client-only, so there is no reachable config that would reject it\n // here — the former defensive `if (!allowed.includes(\"client-only\")) reject`\n // was dead code (verified by union coverage across both plugins, #809).\n if (ssr === false) return \"client-only\";\n\n const value = typeof ssr === \"function\" ? ssr(state) : ssr;\n\n if (typeof value !== \"string\" || !allowed.includes(value as SsrMode)) {\n rejectMode(value, allowed, prefix, route);\n }\n\n return value;\n}\n\nexport function createSsrLoaderPlugin<\n T,\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n loaders: SsrLoaderFactoryMap<T, SsrMode, Dependencies>,\n config: SsrLoaderPluginConfig,\n): PluginFactory<Dependencies> {\n if (\n (config.deferredNamespace !== undefined) !==\n (config.deferredKeysNamespace !== undefined)\n ) {\n throw new TypeError(\n `${config.errorPrefix} \\`deferredNamespace\\` and \\`deferredKeysNamespace\\` must be set together`,\n );\n }\n\n // Bundle the two namespace strings into a single nullable object so\n // downstream code narrows via `if (deferredConfig !== null)` instead\n // of the `config.deferredNamespace!` non-null assertion that TS can't\n // derive from the XOR check above.\n const deferredConfig =\n config.deferredNamespace !== undefined &&\n config.deferredKeysNamespace !== undefined\n ? {\n valueNamespace: config.deferredNamespace,\n keysNamespace: config.deferredKeysNamespace,\n }\n : null;\n\n return (router, getDependency): Plugin => {\n const api = getPluginApi(router);\n const allowed = config.allowedModes ?? ALL_SSR_MODES;\n\n // Sequential claim acquisition with all-or-nothing rollback. Any\n // failure (collision, validation error during compile loop) releases\n // every claim acquired so far and rethrows. This replaces the\n // previous 4 nested try/catch blocks with progressively-longer\n // release lists — same semantics, one shared rollback path.\n const acquired: ContextNamespaceClaim[] = [];\n const claim = (namespace: string): ContextNamespaceClaim => {\n const c = api.claimContextNamespace(namespace);\n acquired.push(c);\n return c;\n };\n const rollback = (): void => {\n for (const c of acquired) c.release();\n };\n\n let dataClaim: ContextNamespaceClaim;\n let modeClaim: ContextNamespaceClaim;\n let deferredClaims: {\n value: ContextNamespaceClaim;\n keys: ContextNamespaceClaim;\n } | null = null;\n let compiled: Map<string, CompiledEntry<T>>;\n\n try {\n dataClaim = claim(config.namespace);\n modeClaim = claim(config.modeNamespace);\n\n if (deferredConfig !== null) {\n deferredClaims = {\n value: claim(deferredConfig.valueNamespace),\n keys: claim(deferredConfig.keysNamespace),\n };\n }\n\n compiled = compile(\n loaders,\n router,\n getDependency,\n config.errorPrefix,\n allowed,\n );\n } catch (error) {\n rollback();\n\n throw error;\n }\n\n const internals = getInternals(router);\n\n // Hot path on every successful start() / subscribeLeave refresh. The\n // previous shape ran a `processLoaderResult` helper that always allocated\n // a `{ critical, deferred }` wrapper object — wasted on the common\n // plain-data path (and on every call from `rsc-server-plugin`, which\n // never opts into deferred support). Inlining the branch keeps the\n // fast path allocation-free and the slow path (defer payload) at one\n // intentional `Object.keys(...)` array allocation per loader.\n const writeLoaderResult = (state: State, value: T): void => {\n if (deferredClaims !== null && isDeferred(value)) {\n dataClaim.write(state, value.critical as T);\n deferredClaims.value.write(state, value.deferred);\n deferredClaims.keys.write(state, Object.keys(value.deferred));\n\n return;\n }\n\n dataClaim.write(state, value);\n };\n\n const reconstructDeferredFromHydration = (\n state: State,\n hydrated: Record<string, unknown>,\n ): void => {\n if (deferredConfig === null || deferredClaims === null) return;\n\n const keysRaw = hydrated[deferredConfig.keysNamespace];\n\n if (!Array.isArray(keysRaw)) return;\n\n const keys = keysRaw.filter(\n (k): k is string =>\n typeof k === \"string\" &&\n // Defensive: drop reserved keys that would corrupt the prototype\n // chain when assigned via `[key] = …`. `{ __proto__: x }` literal\n // does the same thing and would trigger the setter on the fresh\n // object below — turning useDeferred(\"then\") into a function ref\n // pulled from Promise.prototype. With a null-prototype object\n // (below) `__proto__` is just a property, but skipping these\n // keys outright keeps the surface predictable.\n k !== \"__proto__\" &&\n k !== \"constructor\" &&\n k !== \"prototype\",\n );\n\n if (keys.length === 0) return;\n\n // Null-prototype object so `[key] = …` cannot trigger the\n // `Object.prototype.__proto__` setter, even if the filter above is\n // bypassed by future refactors.\n const promises = Object.create(null) as Record<\n string,\n Promise<unknown>\n >;\n\n for (const key of keys) {\n promises[key] = ensureRegistryPromise(key);\n }\n\n deferredClaims.value.write(state, promises);\n deferredClaims.keys.write(state, keys);\n };\n\n // Shared between start interceptor (SSR boot path) and subscribeLeave\n // handler (CSR revalidation path). Returns the compiled entry only\n // when:\n // 1. the route is registered in this plugin's loaders map, AND\n // 2. the resolved mode is NOT \"client-only\".\n // In both successful cases the mode marker is published to\n // `state.context[modeNamespace]` BEFORE returning. Callers then own\n // the loader-invocation strategy (start path also checks the hydration\n // scratchpad; leave path gates on `entry.loader !== undefined`).\n const prepareEntry = (state: State): CompiledEntry<T> | null => {\n const entry = compiled.get(state.name);\n\n if (!entry) return null;\n\n // Static forms (the common case) — staticMode was pre-resolved at\n // compile time, skip the resolveMode if/else walk per navigation.\n // Function-form path: invoke modeFn with the resolved state and\n // re-validate via resolveMode (catches a resolver returning a\n // foreign string at runtime).\n const mode =\n entry.staticMode !== null\n ? entry.staticMode\n : resolveMode(\n entry.modeFn,\n state,\n allowed,\n config.errorPrefix,\n state.name,\n );\n\n modeClaim.write(state, mode);\n\n if (mode === \"client-only\") return null;\n\n return entry;\n };\n\n const removeStartInterceptor = api.addInterceptor(\n \"start\",\n async (next, path) => {\n const state = await next(path);\n const entry = prepareEntry(state);\n\n if (entry === null) return state;\n\n const hydrationState = internals.hydrationState;\n\n if (\n hydrationState !== null &&\n hydrationState.name === state.name &&\n // `in` — not `!== undefined` — is intentional. The contract is\n // \"scratchpad presence wins\": if the server explicitly serialised\n // a value into this namespace (even an `undefined` left over from\n // a programmatic state object), the plugin treats that as the\n // server's authoritative answer and skips re-running the loader\n // on the client. JSON-roundtrip strips `undefined` values, so in\n // practice this only matters for in-memory hydration paths —\n // see CLAUDE.md \"Gotchas → Hydration scratchpad: presence wins\".\n config.namespace in hydrationState.context\n ) {\n dataClaim.write(state, hydrationState.context[config.namespace] as T);\n reconstructDeferredFromHydration(state, hydrationState.context);\n } else if (entry.loader !== undefined) {\n writeLoaderResult(state, await entry.loader(state.params));\n }\n\n return state;\n },\n );\n\n // CSR revalidation channel for `invalidate(router, namespace)`.\n // Runs in the awaited LEAVE_APPROVE phase so fresh data lands on\n // `nextRoute.context` before `TRANSITION_SUCCESS` fires.\n // Flag is cleared only after a successful, non-cancelled loader write —\n // no-entry / client-only / cancelled navigations preserve it for retry.\n const removeLeaveListener = router.subscribeLeave(\n async ({ nextRoute, signal }) => {\n if (!isStale(router, config.namespace)) return;\n\n const entry = prepareEntry(nextRoute);\n\n if (entry === null || entry.loader === undefined) return;\n\n // Pass the navigation's signal so cancellation-aware loaders can\n // abort their in-flight work (fetch, DB query, etc.) when a newer\n // navigation supersedes this one. The post-await `signal.aborted`\n // check below remains as the final gate — loaders that ignore the\n // signal still benefit from the cancel-safety contract (#605).\n const data = await entry.loader(nextRoute.params, { signal });\n\n if (signal.aborted) return;\n\n clearStale(router, config.namespace);\n writeLoaderResult(nextRoute, data);\n },\n );\n\n return {\n teardown() {\n removeStartInterceptor();\n removeLeaveListener();\n dataClaim.release();\n modeClaim.release();\n deferredClaims?.value.release();\n deferredClaims?.keys.release();\n },\n };\n };\n}\n","import { ALL_SSR_MODES } from \"./types.js\";\n\nimport type { SsrMode } from \"./types.js\";\n\nexport function createLoadersValidator(\n errorPrefix: string,\n allowedModes: readonly SsrMode[] = ALL_SSR_MODES,\n) {\n return function validateLoaders(loaders: unknown): void {\n if (\n loaders === null ||\n typeof loaders !== \"object\" ||\n Array.isArray(loaders)\n ) {\n throw new TypeError(`${errorPrefix} loaders must be a non-null object`);\n }\n\n for (const [route, entry] of Object.entries(\n loaders as Record<string, unknown>,\n )) {\n if (typeof entry === \"function\") continue;\n\n if (entry === null || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new TypeError(\n `${errorPrefix} entry for route \"${route}\" must be a function or { ssr?, loader? } object`,\n );\n }\n\n for (const key of Object.keys(entry as Record<string, unknown>)) {\n if (key !== \"ssr\" && key !== \"loader\") {\n throw new TypeError(\n `${errorPrefix} unexpected key \"${key}\" in route \"${route}\" config`,\n );\n }\n }\n\n const obj = entry as { ssr?: unknown; loader?: unknown };\n\n if (obj.loader !== undefined && typeof obj.loader !== \"function\") {\n throw new TypeError(\n `${errorPrefix} loader for route \"${route}\" must be a function`,\n );\n }\n\n if (obj.ssr !== undefined) {\n const ssr = obj.ssr;\n\n if (typeof ssr === \"function\" || typeof ssr === \"boolean\") {\n continue;\n }\n\n if (typeof ssr === \"string\") {\n if (!(allowedModes as readonly string[]).includes(ssr)) {\n throw new TypeError(\n `${errorPrefix} mode \"${ssr}\" is not allowed for route \"${route}\". Allowed: ${allowedModes.join(\", \")}`,\n );\n }\n continue;\n }\n\n throw new TypeError(\n `${errorPrefix} ssr for route \"${route}\" must be SsrMode string, boolean, or (state) => SsrMode`,\n );\n }\n }\n };\n}\n","import { ERROR_PREFIX } from \"./constants\";\nimport { createLoadersValidator, createSsrLoaderPlugin } from \"./shared-ssr\";\n\nimport type { DataLoaderFactoryMap } from \"./types\";\nimport type { DefaultDependencies, PluginFactory } from \"@real-router/types\";\n\n// Inlined binding — symmetric with rsc-server-plugin's same merge.\nconst validateLoaders = createLoadersValidator(ERROR_PREFIX);\n\nexport function ssrDataPluginFactory<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(loaders: DataLoaderFactoryMap<Dependencies>): PluginFactory<Dependencies> {\n validateLoaders(loaders);\n\n return createSsrLoaderPlugin<unknown, Dependencies>(loaders, {\n namespace: \"data\",\n modeNamespace: \"ssrDataMode\",\n deferredNamespace: \"ssrDataDeferred\",\n deferredKeysNamespace: \"ssrDataDeferredKeys\",\n errorPrefix: ERROR_PREFIX,\n });\n}\n","import { ALL_SSR_MODES, type SsrMode } from \"./shared-ssr\";\n\nimport type { State } from \"@real-router/types\";\n\n/**\n * Returns the SSR mode resolved by `ssr-data-plugin` for the current state.\n * Falls back to `\"full\"` when the route has no plugin entry.\n *\n * Read this from `entry-server.tsx` to branch on full / data-only / client-only:\n * - `\"full\"` — render the full app, ship JSON + HTML.\n * - `\"data-only\"` — ship JSON only, render shell HTML.\n * - `\"client-only\"` — ship shell HTML and let the client fetch via its own mechanism.\n *\n * The mode is written to `state.context.ssrDataMode` by the plugin's `start`\n * interceptor for every route registered in the loaders map.\n *\n * Defensive read: if `state.context.ssrDataMode` was set to something outside\n * `ALL_SSR_MODES` by a TS-cast bypass or a foreign writer, the function\n * collapses it to `\"full\"` rather than returning the bad value. Without this\n * guard, a downstream `mode === \"full\"` branch would silently misbehave for\n * `0`, `false`, `\"\"`, `null`, or any unknown string.\n */\nexport function getSsrDataMode(state: State): SsrMode {\n const raw = (state.context as { ssrDataMode?: unknown }).ssrDataMode;\n\n return typeof raw === \"string\" && ALL_SSR_MODES.includes(raw as SsrMode)\n ? (raw as SsrMode)\n : \"full\";\n}\n","import { markStale } from \"./shared-ssr\";\n\nimport type { Router } from \"@real-router/types\";\n\n/**\n * Mark the `\"data\"` namespace as stale on the given router. The next\n * navigation (including a same-route reload) re-runs the loader for the\n * destination route and overwrites `state.context.data` (and the mode marker)\n * via the plugin's `subscribeLeave` listener.\n *\n * Honest fire-and-forget semantics — returns `void`. The flag is consumed in\n * the awaited LEAVE_APPROVE phase of the next navigation, so subscribers see\n * fresh data when the navigation completes. Behaviour during an in-flight\n * transition: the current transition completes unchanged; the flag is read\n * by the *following* navigation. This keeps the invariant\n * \"one transition = one `state.context` snapshot\" intact.\n *\n * Composability through the existing core API:\n *\n * ```ts\n * // Fire-and-forget: stale until the user navigates somewhere\n * invalidate(router, \"data\");\n *\n * // Explicit await — pair with a same-route reload\n * invalidate(router, \"data\");\n * await router.navigate(state.name, state.params, { reload: true });\n * ```\n *\n * Cheaper than `router.navigate({ reload: true })` alone in two ways:\n * 1. No fake transition fired when the application already has a navigation\n * in flight — the existing one consumes the flag.\n * 2. Surgical: only the `\"data\"` namespace re-runs. Companion plugins (e.g.\n * `rsc-server-plugin`) keep their cached `state.context.rsc` on this same\n * transition unless their own `invalidate()` was also called.\n */\nexport function invalidate(router: Router, namespace: \"data\"): void {\n markStale(router, namespace);\n}\n"],"mappings":"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"}
|
package/dist/cjs/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./deferRegistry-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./deferRegistry-BQgydXL6.js"),t=e=>e instanceof Error?JSON.stringify({name:e.name,message:e.message}):JSON.stringify({message:String(e)});function n(e,t){try{return e(t)}catch{return`{"name":"Error","message":"deferred serialization failed"}`}}function r(t,r,i,a,o){return t.map(([t,s])=>Promise.resolve(s).then(s=>{try{let n=i(s)??`null`;o(r.encode(e.n(t,n,!1)))}catch(i){let s=n(a,i);o(r.encode(e.n(t,s,!0)))}},i=>{let s=n(a,i);o(r.encode(e.n(t,s,!0)))}))}function i(t,n,r,i){t&&n>0&&i(r.encode(`<script>${e.r()}<\/script>`))}async function a(e,t,n){try{for(;!t.value;){let{done:t,value:r}=await e.read();if(t)break;n(r)}return{error:null}}catch(e){return{error:e}}}function o(e,n,o={}){let s=new TextEncoder,c=o.serialize??JSON.stringify,l=o.serializeError??t,u=o.bootstrap!==!1,d=Object.entries(n),f=null,p={value:!1};return new ReadableStream({async start(t){let n=!1,o=e=>{if(!n)try{t.enqueue(e)}catch{n=!0}};i(u,d.length,s,o);let m=r(d,s,c,l,o);f=e.getReader();let h=await a(f,p,o);if(h.error!==null){n=!0,t.error(h.error),f.releaseLock(),f=null;return}f?.releaseLock(),f=null,await Promise.allSettled(m),!n&&!p.value&&t.close()},async cancel(t){if(p.value=!0,f===null){try{await e.cancel(t)}catch{}return}try{await f.cancel(t)}catch{}try{f.releaseLock()}catch{}f=null}})}exports.getDeferBootstrapScript=e.r,exports.injectDeferredScripts=o;
|
|
2
2
|
//# sourceMappingURL=server.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=`__rrDeferRegistry__`,t=`__rrDefer__`,n=`__rrDeferError__`;function r(){return globalThis}function i(){let t=r(),n=t[e];return n===void 0&&(n=new Map,t[e]=n),n}function a(e){let t=i(),n=t.get(e);if(n===void 0){let r,i;n={promise:new Promise((e,t)=>{r=e,i=t}),resolve:r,reject:i},t.set(e,n)}return n.promise}function o(){return`(function(g){var R=g.${e};if(!R)R=g.${e}=new Map();function E(k){var e=R.get(k);if(!e){var rs,rj;var p=new Promise(function(r,j){rs=r;rj=j});e={promise:p,resolve:rs,reject:rj};R.set(k,e)}return e}g.${t}=function(k,j){E(k).resolve(JSON.parse(j))};g.${n}=function(k,j){var d=JSON.parse(j);var er=new Error(d&&d.message?d.message:"deferred error");if(d&&d.name)er.name=d.name;E(k).reject(er)}})(typeof globalThis!=='undefined'?globalThis:(typeof window!=='undefined'?window:self));`}const s=[[
|
|
2
|
-
//# sourceMappingURL=deferRegistry-
|
|
1
|
+
const e=`__rrDeferRegistry__`,t=`__rrDefer__`,n=`__rrDeferError__`;function r(){return globalThis}function i(){let t=r(),n=t[e];return n===void 0&&(n=new Map,t[e]=n),n}function a(e){let t=i(),n=t.get(e);if(n===void 0){let r,i;n={promise:new Promise((e,t)=>{r=e,i=t}),resolve:r,reject:i},t.set(e,n)}return n.promise}function o(){return`(function(g){var R=g.${e};if(!R)R=g.${e}=new Map();function E(k){var e=R.get(k);if(!e){var rs,rj;var p=new Promise(function(r,j){rs=r;rj=j});e={promise:p,resolve:rs,reject:rj};R.set(k,e)}return e}g.${t}=function(k,j){E(k).resolve(JSON.parse(j))};g.${n}=function(k,j){var d=JSON.parse(j);var er=new Error(d&&d.message?d.message:"deferred error");if(d&&d.name)er.name=d.name;E(k).reject(er)}})(typeof globalThis!=='undefined'?globalThis:(typeof window!=='undefined'?window:self));`}const s=[[`<`,String.raw`\u003c`],[`>`,String.raw`\u003e`],[`&`,String.raw`\u0026`],[String.fromCodePoint(8232),String.raw`\u2028`],[String.fromCodePoint(8233),String.raw`\u2029`]],c=Object.fromEntries(s),l=RegExp(`[${s.map(([e])=>e).join(``)}]`,`g`);function u(e){let t;try{t=JSON.stringify(e)}catch{t=void 0}return typeof t==`string`?t.replace(l,e=>c[e]??e):`null`}function d(e,r,i){return`<script>${i?n:t}(${u(e)},${u(r)})<\/script>`}export{d as n,o as r,a as t};
|
|
2
|
+
//# sourceMappingURL=deferRegistry-C4u1L5qm.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferRegistry-C4u1L5qm.mjs","names":[],"sources":["../../../../shared/ssr/deferRegistry.ts"],"sourcesContent":["/**\n * Client-side registry for deferred values streamed from the server.\n *\n * The contract spans three actors:\n *\n * 1. **Server stream injects `<script>__rrDefer__(\"key\", \"json\")</script>`\n * tags** as each loader-returned promise resolves. The bootstrap script\n * (also server-emitted) installs `__rrDefer__` and the registry on\n * `globalThis` before any settle script runs.\n *\n * 2. **Plugin start interceptor** (post-hydration scratchpad path) reads the\n * `<deferredKeysNamespace>` list from the hydrated state, then calls\n * `ensureRegistryPromise(key)` once per key to obtain the promise that\n * `useDeferred()` will return. This ensures a stable Promise reference\n * across the initial render and any inline-script settlements.\n *\n * 3. **Adapter `useDeferred(key)`** reads from `state.context.<deferredNamespace>`\n * which the plugin populated above. The returned Promise integrates with\n * React `use()`, Solid `<Await/>`, Svelte `{#await}`, etc.\n */\n\ninterface RegistryEntry {\n promise: Promise<unknown>;\n resolve: (value: unknown) => void;\n reject: (error: unknown) => void;\n}\n\nconst REGISTRY_GLOBAL_KEY = \"__rrDeferRegistry__\";\nconst SETTLE_FN_NAME = \"__rrDefer__\";\nconst REJECT_FN_NAME = \"__rrDeferError__\";\n\ninterface DeferGlobal {\n [REGISTRY_GLOBAL_KEY]?: Map<string, RegistryEntry>;\n [SETTLE_FN_NAME]?: (key: string, json: string) => void;\n [REJECT_FN_NAME]?: (key: string, json: string) => void;\n}\n\nfunction getGlobal(): DeferGlobal {\n return globalThis as unknown as DeferGlobal;\n}\n\nfunction getOrCreateRegistry(): Map<string, RegistryEntry> {\n const g = getGlobal();\n let registry = g[REGISTRY_GLOBAL_KEY];\n\n if (registry === undefined) {\n registry = new Map<string, RegistryEntry>();\n g[REGISTRY_GLOBAL_KEY] = registry;\n }\n\n return registry;\n}\n\n/**\n * Returns the registered Promise for `key`, creating a fresh pending entry on\n * first access. Stable across calls — `useDeferred` relies on Promise\n * reference identity for React `use()` to track resolution.\n */\nexport function ensureRegistryPromise(key: string): Promise<unknown> {\n const registry = getOrCreateRegistry();\n let entry = registry.get(key);\n\n if (entry === undefined) {\n let resolve!: (value: unknown) => void;\n let reject!: (error: unknown) => void;\n\n const promise = new Promise<unknown>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n\n entry = { promise, resolve, reject };\n registry.set(key, entry);\n }\n\n return entry.promise;\n}\n\n/**\n * Returns the inline bootstrap script (no `<script>` wrapper). Embed in a\n * `<script>` tag emitted **once before any `__rrDefer__()` call lands** in\n * the response stream. Idempotent — re-installing is a no-op.\n *\n * The script source is kept terse (ES5-ish, no template literals, no\n * arrow functions) so it works without transpilation in legacy browsers and\n * stays under ~600 bytes uncompressed.\n */\nexport function getDeferBootstrapScript(): string {\n // The script idempotently installs __rrDefer__/__rrDeferError__ on `g`. If\n // the registry already exists (e.g. from a prior call to\n // ensureRegistryPromise on the client adapter), reuse it — only the settle\n // functions are (re)assigned. This handles the realistic ordering:\n // adapter creates the registry during hydration; the first settle script\n // arriving in the response stream installs the global functions.\n return (\n \"(function(g){\" +\n `var R=g.${REGISTRY_GLOBAL_KEY};` +\n `if(!R)R=g.${REGISTRY_GLOBAL_KEY}=new Map();` +\n \"function E(k){\" +\n \"var e=R.get(k);\" +\n \"if(!e){\" +\n \"var rs,rj;\" +\n \"var p=new Promise(function(r,j){rs=r;rj=j});\" +\n \"e={promise:p,resolve:rs,reject:rj};\" +\n \"R.set(k,e)\" +\n \"}\" +\n \"return e\" +\n \"}\" +\n `g.${SETTLE_FN_NAME}=function(k,j){E(k).resolve(JSON.parse(j))};` +\n `g.${REJECT_FN_NAME}=function(k,j){` +\n \"var d=JSON.parse(j);\" +\n 'var er=new Error(d&&d.message?d.message:\"deferred error\");' +\n \"if(d&&d.name)er.name=d.name;\" +\n \"E(k).reject(er)\" +\n \"}\" +\n \"})(typeof globalThis!=='undefined'?globalThis:\" +\n \"(typeof window!=='undefined'?window:self));\"\n );\n}\n\n// Single-pass replacement table for the chars escapeForScript must encode\n// as `\\uXXXX` to keep them out of the raw HTML parser. Five consecutive\n// `replace` / `split`+`join` passes used to walk the string for each\n// codepoint; the regex + lookup form does it in one pass — ~1.6× faster\n// on large payloads, indistinguishable on short keys (the common case).\n//\n// Roundtrip + HTML-safety properties are pinned by the\n// `escapeForScript: pure-function security invariants` PBT block in\n// `tests/property/ssr-data.properties.ts` (numRuns: 1000).\n//\n// Built at module init via `String.fromCodePoint(...)` so the source file\n// itself never contains raw U+2028 / U+2029 codepoints (which would\n// terminate string literals / regex literals at parse time on legacy\n// JS engines and even in modern TS parsers under some configs).\nconst ESCAPE_FOR_SCRIPT_PAIRS: readonly (readonly [string, string])[] = [\n [\"<\", String.raw`\\u003c`],\n [\">\", String.raw`\\u003e`],\n [\"&\", String.raw`\\u0026`],\n [String.fromCodePoint(0x20_28), String.raw`\\u2028`],\n [String.fromCodePoint(0x20_29), String.raw`\\u2029`],\n] as const;\nconst ESCAPE_FOR_SCRIPT_TABLE: Record<string, string> = Object.fromEntries(\n ESCAPE_FOR_SCRIPT_PAIRS,\n);\nconst ESCAPE_FOR_SCRIPT_REGEX = new RegExp(\n `[${ESCAPE_FOR_SCRIPT_PAIRS.map(([c]) => c).join(\"\")}]`,\n \"g\",\n);\n\n/**\n * Encode an arbitrary string as a **JS string literal** that is also safe to\n * embed inside a `<script>...</script>` body. Returns the literal **with**\n * surrounding quotes — drop it directly into a script template.\n *\n * Encoding via Unicode escapes (`\\uXXXX`) means:\n * - The raw HTML parser sees no `<`, `>`, U+2028, or U+2029 — so it cannot\n * terminate the script tag prematurely (`</script>`, `<!--`) or trigger\n * legacy JS line-terminator interpretation.\n * - The JS parser interprets `<`/`>`/`U+2028`/`U+2029` back to\n * their original chars, so the runtime string value is bit-identical to\n * the input.\n * - Crucially, the same encoding works for two consumer paths:\n * 1. **Plain JS literal** (e.g. the deferred KEY): the JS parser hands\n * back the original string directly.\n * 2. **JS literal containing JSON** (e.g. the deferred VALUE): the JS\n * parser hands back a string with `<` text inside (the leading\n * `\\\\` of `\\\\u003c` escaped to `\\`, then `u003c` is plain text), and\n * `JSON.parse` then unescapes `<` → `<`. Net round-trip is\n * identity.\n * Both decode paths land on the original string — so the same\n * `escapeForScript` works for both keys (parsed as JS literal) and values\n * (parsed as JS literal containing JSON).\n *\n * The `&` → `&` substitution defends against `<![CDATA[` / template\n * engine post-processing that might re-interpret HTML entities; it is not\n * strictly necessary for `<script>` body parsing but cheap and conservative.\n */\nexport function escapeForScript(value: string): string {\n // The TS contract is `value: string`, but a cast at a callsite or a\n // misbehaving custom serializer can still smuggle a non-string through.\n // Three failure modes JSON.stringify can have on non-strings:\n // - returns `undefined` (`stringify(undefined)`, `stringify(symbol)`,\n // `stringify(function)`),\n // - throws (`stringify(bigint)` → `TypeError`,\n // `stringify(circular)` → `TypeError`),\n // - returns `\"null\"` (already safe for our pipeline).\n // Catch both and emit the JSON `null` literal — the safest single-token\n // representation that JSON.parse will accept downstream.\n let json: string | undefined;\n\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n\n if (typeof json !== \"string\") {\n return \"null\";\n }\n\n return json.replace(\n ESCAPE_FOR_SCRIPT_REGEX,\n // The `?? c` arm is structurally unreachable — the regex char class is\n // built from the table's own keys, so every match has a table entry; the\n // fallback only satisfies the `string | undefined` index signature.\n /* v8 ignore next -- @preserve: TS index-access fallback, regex ⊆ table keys */\n (c) => ESCAPE_FOR_SCRIPT_TABLE[c] ?? c,\n );\n}\n\n/**\n * Format a single settle script for one resolved promise.\n * Output: `<script>__rrDefer__(\"key\",\"jsonString\")</script>`. Both `key`\n * and `serializedValue` are user-controlled in the general case (route\n * params can flow into deferred-map keys; loader returns flow into values),\n * so both go through {@link escapeForScript}.\n */\nexport function formatSettleScript(\n key: string,\n serializedValue: string,\n isError: boolean,\n): string {\n const fn = isError ? REJECT_FN_NAME : SETTLE_FN_NAME;\n const safeKey = escapeForScript(key);\n const safeValue = escapeForScript(serializedValue);\n\n return `<script>${fn}(${safeKey},${safeValue})</script>`;\n}\n\n/** Test-only — clears the global registry. Not exported from index.ts. */\nexport function __resetRegistryForTests(): void {\n const g = getGlobal();\n delete g[REGISTRY_GLOBAL_KEY];\n delete g[SETTLE_FN_NAME];\n delete g[REJECT_FN_NAME];\n}\n"],"mappings":"AA2BA,MAAM,EAAsB,sBACtB,EAAiB,cACjB,EAAiB,mBAQvB,SAAS,GAAyB,CAChC,OAAO,UACT,CAEA,SAAS,GAAkD,CACzD,IAAM,EAAI,EAAU,EAChB,EAAW,EAAE,GAOjB,OALI,IAAa,IAAA,KACf,EAAW,IAAI,IACf,EAAE,GAAuB,GAGpB,CACT,CAOA,SAAgB,EAAsB,EAA+B,CACnE,IAAM,EAAW,EAAoB,EACjC,EAAQ,EAAS,IAAI,CAAG,EAE5B,GAAI,IAAU,IAAA,GAAW,CACvB,IAAI,EACA,EAOJ,EAAQ,CAAE,QAAA,IALU,SAAkB,EAAK,IAAQ,CACjD,EAAU,EACV,EAAS,CACX,CAEgB,EAAG,UAAS,QAAO,EACnC,EAAS,IAAI,EAAK,CAAK,CACzB,CAEA,OAAO,EAAM,OACf,CAWA,SAAgB,GAAkC,CAOhD,MACE,wBACW,EAAoB,aAClB,EAAoB,gKAW5B,EAAe,gDACf,EAAe,mOASxB,CAgBA,MAAM,EAAkE,CACtE,CAAC,IAAK,OAAO,GAAG,QAAQ,EACxB,CAAC,IAAK,OAAO,GAAG,QAAQ,EACxB,CAAC,IAAK,OAAO,GAAG,QAAQ,EACxB,CAAC,OAAO,cAAc,IAAO,EAAG,OAAO,GAAG,QAAQ,EAClD,CAAC,OAAO,cAAc,IAAO,EAAG,OAAO,GAAG,QAAQ,CACpD,EACM,EAAkD,OAAO,YAC7D,CACF,EACM,EAA8B,OAClC,IAAI,EAAwB,KAAK,CAAC,KAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,GACrD,GACF,EA8BA,SAAgB,EAAgB,EAAuB,CAWrD,IAAI,EAEJ,GAAI,CACF,EAAO,KAAK,UAAU,CAAK,CAC7B,MAAQ,CACN,EAAO,IAAA,EACT,CAMA,OAJI,OAAO,GAAS,SAIb,EAAK,QACV,EAKC,GAAM,EAAwB,IAAM,CACvC,EAVS,MAWX,CASA,SAAgB,EACd,EACA,EACA,EACQ,CAKR,MAAO,WAJI,EAAU,EAAiB,EAIjB,GAHL,EAAgB,CAGF,EAAE,GAFd,EAAgB,CAES,EAAE,YAC/C"}
|
package/dist/esm/errors.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=class extends Error{target;status;code=`LOADER_REDIRECT`;constructor(e,t=302){super(`Redirect to ${e}`),this.target=e,this.status=t,this.name=`LoaderRedirect`}},t=class extends Error{resource;code=`LOADER_NOT_FOUND`;constructor(e){super(`Resource not found: ${e}`),this.resource=e,this.name=`LoaderNotFound`}},n=class extends Error{route;ms;code=`LOADER_TIMEOUT`;constructor(e,t){super(`Loader for "${e}" exceeded ${t}ms`),this.route=e,this.ms=t,this.name=`LoaderTimeout`}};function r(e,t,r,i){let a=i?.upstreamSignal;if(a?.aborted)return Promise.reject(a.reason??new DOMException(`The operation was aborted.`,`AbortError`));let o=new AbortController,s=a?AbortSignal.any([a,o.signal]):o.signal,c,l=new Promise((r,i)=>{c=setTimeout(()=>{let r=new n(e,t);o.abort(r),i(r)},t)}),u=(async()=>r({signal:s}))().finally(()=>{
|
|
1
|
+
var e=class extends Error{target;status;code=`LOADER_REDIRECT`;constructor(e,t=302){super(`Redirect to ${e}`),this.target=e,this.status=t,this.name=`LoaderRedirect`}},t=class extends Error{resource;code=`LOADER_NOT_FOUND`;constructor(e){super(`Resource not found: ${e}`),this.resource=e,this.name=`LoaderNotFound`}},n=class extends Error{route;ms;code=`LOADER_TIMEOUT`;constructor(e,t){super(`Loader for "${e}" exceeded ${t}ms`),this.route=e,this.ms=t,this.name=`LoaderTimeout`}};function r(e,t,r,i){let a=i?.upstreamSignal;if(a?.aborted)return Promise.reject(a.reason??new DOMException(`The operation was aborted.`,`AbortError`));let o=new AbortController,s=a?AbortSignal.any([a,o.signal]):o.signal,c,l=new Promise((r,i)=>{c=setTimeout(()=>{let r=new n(e,t);o.abort(r),i(r)},t)}),u=(async()=>r({signal:s}))().finally(()=>{clearTimeout(c)});return Promise.race([u,l])}export{t as LoaderNotFound,e as LoaderRedirect,n as LoaderTimeout,r as withTimeout};
|
|
2
2
|
//# sourceMappingURL=errors.mjs.map
|
package/dist/esm/errors.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../shared/ssr/errors.ts"],"sourcesContent":["/**\n * Typed loader errors that SSR pipelines translate into HTTP semantics.\n *\n * The `ssr-data-plugin` and `rsc-server-plugin` are intentionally\n * HTTP-agnostic — they only await the loader and write the resolved value\n * to `state.context.<namespace>`. Loaders bridge to HTTP status codes by\n * throwing one of these named errors; application-layer middleware catches\n * them and maps each `code` to the right status (302/308, 404, 504).\n *\n * Structural discrimination via `code` (not `instanceof`) so consumers\n * can match across realms / bundle boundaries without coupling to the\n * class identity.\n *\n * Re-exported from both plugins under the `./errors` subpath:\n * `@real-router/ssr-data-plugin/errors` and\n * `@real-router/rsc-server-plugin/errors`.\n */\n\nexport class LoaderRedirect extends Error {\n readonly code = \"LOADER_REDIRECT\";\n\n constructor(\n readonly target: string,\n readonly status: 301 | 302 | 307 | 308 = 302,\n ) {\n super(`Redirect to ${target}`);\n this.name = \"LoaderRedirect\";\n }\n}\n\nexport class LoaderNotFound extends Error {\n readonly code = \"LOADER_NOT_FOUND\";\n\n constructor(readonly resource: string) {\n super(`Resource not found: ${resource}`);\n this.name = \"LoaderNotFound\";\n }\n}\n\nexport class LoaderTimeout extends Error {\n readonly code = \"LOADER_TIMEOUT\";\n\n constructor(\n readonly route: string,\n readonly ms: number,\n ) {\n super(`Loader for \"${route}\" exceeded ${ms}ms`);\n this.name = \"LoaderTimeout\";\n }\n}\n\n/**\n * Race a loader against a deadline, with cooperative cancellation.\n *\n * The loader is invoked with `{ signal }` — a composed `AbortSignal` that\n * aborts on the first of:\n * - the deadline elapsing (`internalController.abort()` fires synchronously\n * *before* the race rejects with `LoaderTimeout`, so a loader that\n * threads `signal` into its I/O — e.g. `fetch(url, { signal })` — can\n * actually cancel the underlying work);\n * - `options.upstreamSignal` aborting (typically the request-scoped abort\n * wired by `cloneRouter(base, { abortSignal })` for client-disconnect).\n *\n * Composition uses `AbortSignal.any([upstream, internal])` (Node 20.3+).\n * If `upstreamSignal` is already aborted at call time, the loader is *not*\n * invoked and the timer is *not* started — the rejection mirrors\n * `upstreamSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\")`.\n *\n * On deadline, the same `LoaderTimeout` instance is used as both the\n * `signal.reason` and the rejection reason — they refer to one object.\n * On upstream abort during execution, the race rejects with the loader's\n * own error (typically `AbortError`), *not* `LoaderTimeout`.\n *\n * Cancellation is cooperative: loaders that don't propagate `signal` into\n * their I/O still run to completion in the background — the race result\n * is unaffected, but resources are not freed early.\n *\n * The `setTimeout` handle is cleared via `.finally()` on the work promise\n * so a fast-path success doesn't leak it. `Promise.race`'s internal\n * `Promise.resolve(p).then(resolve, reject)` consumes any late losing\n * rejection — no `unhandledRejection` for late loader settlements.\n *\n * Requires Node 20.3+ for `AbortSignal.any`.\n *\n * ### `ms` corner cases (Node `setTimeout` clamping)\n *\n * `ms` is forwarded verbatim to `setTimeout`, which means `Infinity`, `NaN`,\n * and negative values are **NOT** safe sentinels for \"no deadline\":\n *\n * - `withTimeout(\"r\", Infinity, …)` — Node clamps to `1` ms and emits a\n * `TimeoutOverflowWarning`. The race rejects with `LoaderTimeout` after\n * 1 ms, not \"never\". Use a separate code path (e.g. invoke the loader\n * directly without wrapping) when you genuinely want no deadline.\n * - `withTimeout(\"r\", NaN, …)` — same: Node clamps to `1` ms with a\n * warning.\n * - `withTimeout(\"r\", -1, …)` — Node clamps to `1` ms with a warning.\n * - `withTimeout(\"r\", 0, …)` — fires on the next tick. A synchronous-\n * resolving loader (`() => Promise.resolve(v)`) typically wins the race,\n * but any async I/O loses. Treat `0` as \"fire immediately\" rather than\n * \"no deadline\".\n *\n * No runtime guard is added — the clamping is a Node-level concern and\n * adding `if (!Number.isFinite(ms) || ms < 0) throw` would be a breaking\n * change for callers relying on the current clamp semantics.\n */\nexport function withTimeout<T>(\n routeName: string,\n ms: number,\n loader: (deps: { signal: AbortSignal }) => Promise<T>,\n options?: { upstreamSignal?: AbortSignal | null },\n): Promise<T> {\n const upstream = options?.upstreamSignal;\n\n if (upstream?.aborted) {\n // `signal.reason` is normally set automatically by the spec\n // (`controller.abort()` without an argument yields a `DOMException`),\n // but the field is writable, so we fall back to a fresh `AbortError`\n // if some caller produced an aborted signal with `reason === undefined`.\n return Promise.reject(\n upstream.reason ??\n new DOMException(\"The operation was aborted.\", \"AbortError\"),\n );\n }\n\n const internal = new AbortController();\n const composed = upstream\n ? AbortSignal.any([upstream, internal.signal])\n : internal.signal;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n const error = new LoaderTimeout(routeName, ms);\n internal.abort(error);\n reject(error);\n }, ms);\n });\n\n const work = (async () => loader({ signal: composed }))().finally(() => {\n
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../shared/ssr/errors.ts"],"sourcesContent":["/**\n * Typed loader errors that SSR pipelines translate into HTTP semantics.\n *\n * The `ssr-data-plugin` and `rsc-server-plugin` are intentionally\n * HTTP-agnostic — they only await the loader and write the resolved value\n * to `state.context.<namespace>`. Loaders bridge to HTTP status codes by\n * throwing one of these named errors; application-layer middleware catches\n * them and maps each `code` to the right status (302/308, 404, 504).\n *\n * Structural discrimination via `code` (not `instanceof`) so consumers\n * can match across realms / bundle boundaries without coupling to the\n * class identity.\n *\n * Re-exported from both plugins under the `./errors` subpath:\n * `@real-router/ssr-data-plugin/errors` and\n * `@real-router/rsc-server-plugin/errors`.\n */\n\nexport class LoaderRedirect extends Error {\n readonly code = \"LOADER_REDIRECT\";\n\n constructor(\n readonly target: string,\n readonly status: 301 | 302 | 307 | 308 = 302,\n ) {\n super(`Redirect to ${target}`);\n this.name = \"LoaderRedirect\";\n }\n}\n\nexport class LoaderNotFound extends Error {\n readonly code = \"LOADER_NOT_FOUND\";\n\n constructor(readonly resource: string) {\n super(`Resource not found: ${resource}`);\n this.name = \"LoaderNotFound\";\n }\n}\n\nexport class LoaderTimeout extends Error {\n readonly code = \"LOADER_TIMEOUT\";\n\n constructor(\n readonly route: string,\n readonly ms: number,\n ) {\n super(`Loader for \"${route}\" exceeded ${ms}ms`);\n this.name = \"LoaderTimeout\";\n }\n}\n\n/**\n * Race a loader against a deadline, with cooperative cancellation.\n *\n * The loader is invoked with `{ signal }` — a composed `AbortSignal` that\n * aborts on the first of:\n * - the deadline elapsing (`internalController.abort()` fires synchronously\n * *before* the race rejects with `LoaderTimeout`, so a loader that\n * threads `signal` into its I/O — e.g. `fetch(url, { signal })` — can\n * actually cancel the underlying work);\n * - `options.upstreamSignal` aborting (typically the request-scoped abort\n * wired by `cloneRouter(base, { abortSignal })` for client-disconnect).\n *\n * Composition uses `AbortSignal.any([upstream, internal])` (Node 20.3+).\n * If `upstreamSignal` is already aborted at call time, the loader is *not*\n * invoked and the timer is *not* started — the rejection mirrors\n * `upstreamSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\")`.\n *\n * On deadline, the same `LoaderTimeout` instance is used as both the\n * `signal.reason` and the rejection reason — they refer to one object.\n * On upstream abort during execution, the race rejects with the loader's\n * own error (typically `AbortError`), *not* `LoaderTimeout`.\n *\n * Cancellation is cooperative: loaders that don't propagate `signal` into\n * their I/O still run to completion in the background — the race result\n * is unaffected, but resources are not freed early.\n *\n * The `setTimeout` handle is cleared via `.finally()` on the work promise\n * so a fast-path success doesn't leak it. `Promise.race`'s internal\n * `Promise.resolve(p).then(resolve, reject)` consumes any late losing\n * rejection — no `unhandledRejection` for late loader settlements.\n *\n * Requires Node 20.3+ for `AbortSignal.any`.\n *\n * ### `ms` corner cases (Node `setTimeout` clamping)\n *\n * `ms` is forwarded verbatim to `setTimeout`, which means `Infinity`, `NaN`,\n * and negative values are **NOT** safe sentinels for \"no deadline\":\n *\n * - `withTimeout(\"r\", Infinity, …)` — Node clamps to `1` ms and emits a\n * `TimeoutOverflowWarning`. The race rejects with `LoaderTimeout` after\n * 1 ms, not \"never\". Use a separate code path (e.g. invoke the loader\n * directly without wrapping) when you genuinely want no deadline.\n * - `withTimeout(\"r\", NaN, …)` — same: Node clamps to `1` ms with a\n * warning.\n * - `withTimeout(\"r\", -1, …)` — Node clamps to `1` ms with a warning.\n * - `withTimeout(\"r\", 0, …)` — fires on the next tick. A synchronous-\n * resolving loader (`() => Promise.resolve(v)`) typically wins the race,\n * but any async I/O loses. Treat `0` as \"fire immediately\" rather than\n * \"no deadline\".\n *\n * No runtime guard is added — the clamping is a Node-level concern and\n * adding `if (!Number.isFinite(ms) || ms < 0) throw` would be a breaking\n * change for callers relying on the current clamp semantics.\n */\nexport function withTimeout<T>(\n routeName: string,\n ms: number,\n loader: (deps: { signal: AbortSignal }) => Promise<T>,\n options?: { upstreamSignal?: AbortSignal | null },\n): Promise<T> {\n const upstream = options?.upstreamSignal;\n\n if (upstream?.aborted) {\n // `signal.reason` is normally set automatically by the spec\n // (`controller.abort()` without an argument yields a `DOMException`),\n // but the field is writable, so we fall back to a fresh `AbortError`\n // if some caller produced an aborted signal with `reason === undefined`.\n return Promise.reject(\n upstream.reason ??\n new DOMException(\"The operation was aborted.\", \"AbortError\"),\n );\n }\n\n const internal = new AbortController();\n const composed = upstream\n ? AbortSignal.any([upstream, internal.signal])\n : internal.signal;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n const error = new LoaderTimeout(routeName, ms);\n internal.abort(error);\n reject(error);\n }, ms);\n });\n\n const work = (async () => loader({ signal: composed }))().finally(() => {\n // The Promise executor above runs synchronously, so `timer` is always\n // assigned by the time `finally` fires — and `clearTimeout` accepts\n // `undefined` anyway, so no guard is needed.\n clearTimeout(timer);\n });\n\n return Promise.race<T>([work, timeoutPromise]);\n}\n"],"mappings":"AAkBA,IAAa,EAAb,cAAoC,KAAM,CAI7B,OACA,OAJX,KAAgB,kBAEhB,YACE,EACA,EAAyC,IACzC,CACA,MAAM,eAAe,GAAQ,EAHpB,KAAA,OAAA,EACA,KAAA,OAAA,EAGT,KAAK,KAAO,gBACd,CACF,EAEa,EAAb,cAAoC,KAAM,CAGnB,SAFrB,KAAgB,mBAEhB,YAAY,EAA2B,CACrC,MAAM,uBAAuB,GAAU,EADpB,KAAA,SAAA,EAEnB,KAAK,KAAO,gBACd,CACF,EAEa,EAAb,cAAmC,KAAM,CAI5B,MACA,GAJX,KAAgB,iBAEhB,YACE,EACA,EACA,CACA,MAAM,eAAe,EAAM,aAAa,EAAG,GAAG,EAHrC,KAAA,MAAA,EACA,KAAA,GAAA,EAGT,KAAK,KAAO,eACd,CACF,EAwDA,SAAgB,EACd,EACA,EACA,EACA,EACY,CACZ,IAAM,EAAW,GAAS,eAE1B,GAAI,GAAU,QAKZ,OAAO,QAAQ,OACb,EAAS,QACP,IAAI,aAAa,6BAA8B,YAAY,CAC/D,EAGF,IAAM,EAAW,IAAI,gBACf,EAAW,EACb,YAAY,IAAI,CAAC,EAAU,EAAS,MAAM,CAAC,EAC3C,EAAS,OAET,EACE,EAAiB,IAAI,SAAY,EAAG,IAAW,CACnD,EAAQ,eAAiB,CACvB,IAAM,EAAQ,IAAI,EAAc,EAAW,CAAE,EAC7C,EAAS,MAAM,CAAK,EACpB,EAAO,CAAK,CACd,EAAG,CAAE,CACP,CAAC,EAEK,GAAQ,SAAY,EAAO,CAAE,OAAQ,CAAS,CAAC,EAAA,CAAG,CAAC,CAAC,YAAc,CAItE,aAAa,CAAK,CACpB,CAAC,EAED,OAAO,QAAQ,KAAQ,CAAC,EAAM,CAAc,CAAC,CAC/C"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./deferRegistry-
|
|
1
|
+
import{t as e}from"./deferRegistry-C4u1L5qm.mjs";import{getPluginApi as t}from"@real-router/core/api";import{getInternals as n}from"@real-router/core/validation";const r=`[@real-router/ssr-data-plugin]`,i=Symbol.for(`@real-router/ssr-data-plugin/defer`);function a(e){if(typeof e!=`object`||!e)throw TypeError("[defer] expected an object with `critical` and `deferred` fields");if(e.deferred===null||typeof e.deferred!=`object`||Array.isArray(e.deferred))throw TypeError("[defer] `deferred` must be a non-null, non-array object of promises");for(let[t,n]of Object.entries(e.deferred)){if(t===`__proto__`||t===`constructor`||t===`prototype`)throw TypeError(`[defer] \`deferred.${t}\` is reserved — choose a different key`);if(typeof n!=`object`||!n||typeof n.then!=`function`)throw TypeError(`[defer] \`deferred.${t}\` must be a Promise (got ${typeof n})`);typeof n.catch==`function`&&n.catch(()=>{})}return Object.freeze({critical:e.critical,deferred:Object.freeze({...e.deferred}),[i]:!0})}function o(e){return typeof e==`object`&&!!e&&Object.hasOwn(e,i)&&e[i]===!0}const s=new WeakMap;function c(e,t){let n=s.get(e);n===void 0&&(n=new Set,s.set(e,n)),n.add(t)}function l(e,t){return s.get(e)?.has(t)??!1}function u(e,t){s.get(e)?.delete(t)}const d=[`full`,`data-only`,`client-only`];function f(e,t,n,r,i){let a=new Map;for(let[o,s]of Object.entries(e)){let e=typeof s==`function`?{loader:s}:s,c;if(e.loader!==void 0){let i=e.loader(t,n);if(typeof i!=`function`)throw TypeError(`${r} factory for route "${o}" must return a function`);c=i}let l=null,u;typeof e.ssr==`function`?u=e.ssr:l=h(e.ssr,p,i,r,o),a.set(o,{staticMode:l,modeFn:u,loader:c})}return a}const p={name:``,params:{},path:``,transition:{phase:`activating`,reason:`success`,segments:{deactivated:[],activated:[],intersection:``}},context:{}};function m(e,t,n,r){throw TypeError(`${n} mode "${String(e)}" is not allowed for route "${r}". Allowed: ${t.join(`, `)}`)}function h(e,t,n,r,i){if(e===void 0||e===!0)return`full`;if(e===!1)return`client-only`;let a=typeof e==`function`?e(t):e;return(typeof a!=`string`||!n.includes(a))&&m(a,n,r,i),a}function g(r,i){if(i.deferredNamespace!==void 0!=(i.deferredKeysNamespace!==void 0))throw TypeError(`${i.errorPrefix} \`deferredNamespace\` and \`deferredKeysNamespace\` must be set together`);let a=i.deferredNamespace!==void 0&&i.deferredKeysNamespace!==void 0?{valueNamespace:i.deferredNamespace,keysNamespace:i.deferredKeysNamespace}:null;return(s,c)=>{let p=t(s),m=i.allowedModes??d,g=[],_=e=>{let t=p.claimContextNamespace(e);return g.push(t),t},v=()=>{for(let e of g)e.release()},y,b,x=null,S;try{y=_(i.namespace),b=_(i.modeNamespace),a!==null&&(x={value:_(a.valueNamespace),keys:_(a.keysNamespace)}),S=f(r,s,c,i.errorPrefix,m)}catch(e){throw v(),e}let C=n(s),w=(e,t)=>{if(x!==null&&o(t)){y.write(e,t.critical),x.value.write(e,t.deferred),x.keys.write(e,Object.keys(t.deferred));return}y.write(e,t)},T=(t,n)=>{if(a===null||x===null)return;let r=n[a.keysNamespace];if(!Array.isArray(r))return;let i=r.filter(e=>typeof e==`string`&&e!==`__proto__`&&e!==`constructor`&&e!==`prototype`);if(i.length===0)return;let o=Object.create(null);for(let t of i)o[t]=e(t);x.value.write(t,o),x.keys.write(t,i)},E=e=>{let t=S.get(e.name);if(!t)return null;let n=t.staticMode===null?h(t.modeFn,e,m,i.errorPrefix,e.name):t.staticMode;return b.write(e,n),n===`client-only`?null:t},D=p.addInterceptor(`start`,async(e,t)=>{let n=await e(t),r=E(n);if(r===null)return n;let a=C.hydrationState;return a!==null&&a.name===n.name&&i.namespace in a.context?(y.write(n,a.context[i.namespace]),T(n,a.context)):r.loader!==void 0&&w(n,await r.loader(n.params)),n}),O=s.subscribeLeave(async({nextRoute:e,signal:t})=>{if(!l(s,i.namespace))return;let n=E(e);if(n===null||n.loader===void 0)return;let r=await n.loader(e.params,{signal:t});t.aborted||(u(s,i.namespace),w(e,r))});return{teardown(){D(),O(),y.release(),b.release(),x?.value.release(),x?.keys.release()}}}}function _(e,t=d){return function(n){if(typeof n!=`object`||!n||Array.isArray(n))throw TypeError(`${e} loaders must be a non-null object`);for(let[r,i]of Object.entries(n)){if(typeof i==`function`)continue;if(typeof i!=`object`||!i||Array.isArray(i))throw TypeError(`${e} entry for route "${r}" must be a function or { ssr?, loader? } object`);for(let t of Object.keys(i))if(t!==`ssr`&&t!==`loader`)throw TypeError(`${e} unexpected key "${t}" in route "${r}" config`);let n=i;if(n.loader!==void 0&&typeof n.loader!=`function`)throw TypeError(`${e} loader for route "${r}" must be a function`);if(n.ssr!==void 0){let i=n.ssr;if(typeof i==`function`||typeof i==`boolean`)continue;if(typeof i==`string`){if(!t.includes(i))throw TypeError(`${e} mode "${i}" is not allowed for route "${r}". Allowed: ${t.join(`, `)}`);continue}throw TypeError(`${e} ssr for route "${r}" must be SsrMode string, boolean, or (state) => SsrMode`)}}}}const v=_(r);function y(e){return v(e),g(e,{namespace:`data`,modeNamespace:`ssrDataMode`,deferredNamespace:`ssrDataDeferred`,deferredKeysNamespace:`ssrDataDeferredKeys`,errorPrefix:r})}function b(e){let t=e.context.ssrDataMode;return typeof t==`string`&&d.includes(t)?t:`full`}function x(e,t){c(e,t)}export{a as defer,b as getSsrDataMode,x as invalidate,o as isDeferred,y as ssrDataPluginFactory};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|