@rshono/core 1.0.0-rc.11 → 1.0.0-rc.13

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.
Files changed (145) hide show
  1. package/README.md +2 -1
  2. package/bin/rshono.mjs +3 -4
  3. package/dist/builder/env-shadow-loader.cjs +5 -5
  4. package/dist/builder/page-files.js +5 -5
  5. package/dist/builder/page-files.js.map +1 -1
  6. package/dist/builder/public-env.d.ts +5 -4
  7. package/dist/builder/public-env.d.ts.map +1 -1
  8. package/dist/builder/public-env.js +5 -4
  9. package/dist/builder/public-env.js.map +1 -1
  10. package/dist/builder/rspack-config.d.ts +10 -0
  11. package/dist/builder/rspack-config.d.ts.map +1 -1
  12. package/dist/builder/rspack-config.js +38 -40
  13. package/dist/builder/rspack-config.js.map +1 -1
  14. package/dist/cli/build.js +4 -4
  15. package/dist/cli/build.js.map +1 -1
  16. package/dist/cli/dev.d.ts.map +1 -1
  17. package/dist/cli/dev.js +50 -37
  18. package/dist/cli/dev.js.map +1 -1
  19. package/dist/cli/index.js +4 -4
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/cli/start.d.ts.map +1 -1
  22. package/dist/cli/start.js +2 -3
  23. package/dist/cli/start.js.map +1 -1
  24. package/dist/config.d.ts +28 -31
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +2 -2
  27. package/dist/config.js.map +1 -1
  28. package/dist/deploy/aws-lambda/runtime.d.ts +4 -6
  29. package/dist/deploy/aws-lambda/runtime.d.ts.map +1 -1
  30. package/dist/deploy/aws-lambda/runtime.js +5 -8
  31. package/dist/deploy/aws-lambda/runtime.js.map +1 -1
  32. package/dist/deploy/build-marker.d.ts +3 -5
  33. package/dist/deploy/build-marker.d.ts.map +1 -1
  34. package/dist/deploy/build-marker.js +3 -5
  35. package/dist/deploy/build-marker.js.map +1 -1
  36. package/dist/deploy/cloudflare/build.d.ts.map +1 -1
  37. package/dist/deploy/cloudflare/build.js +6 -10
  38. package/dist/deploy/cloudflare/build.js.map +1 -1
  39. package/dist/deploy/cloudflare/runtime.d.ts +2 -5
  40. package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
  41. package/dist/deploy/cloudflare/runtime.js +19 -32
  42. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  43. package/dist/deploy/contract.d.ts +25 -42
  44. package/dist/deploy/contract.d.ts.map +1 -1
  45. package/dist/deploy/contract.js.map +1 -1
  46. package/dist/deploy/filesystem.d.ts +3 -5
  47. package/dist/deploy/filesystem.d.ts.map +1 -1
  48. package/dist/deploy/filesystem.js +12 -15
  49. package/dist/deploy/filesystem.js.map +1 -1
  50. package/dist/deploy/node/runtime.d.ts +4 -5
  51. package/dist/deploy/node/runtime.d.ts.map +1 -1
  52. package/dist/deploy/node/runtime.js +9 -15
  53. package/dist/deploy/node/runtime.js.map +1 -1
  54. package/dist/deploy/presets.d.ts +19 -29
  55. package/dist/deploy/presets.d.ts.map +1 -1
  56. package/dist/deploy/presets.js +18 -25
  57. package/dist/deploy/presets.js.map +1 -1
  58. package/dist/deploy/vercel/build.d.ts.map +1 -1
  59. package/dist/deploy/vercel/build.js +9 -12
  60. package/dist/deploy/vercel/build.js.map +1 -1
  61. package/dist/deploy/vercel/runtime.d.ts +4 -7
  62. package/dist/deploy/vercel/runtime.d.ts.map +1 -1
  63. package/dist/deploy/vercel/runtime.js +4 -7
  64. package/dist/deploy/vercel/runtime.js.map +1 -1
  65. package/dist/index.d.ts +13 -9
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +15 -12
  68. package/dist/index.js.map +1 -1
  69. package/dist/router.d.ts +65 -93
  70. package/dist/router.d.ts.map +1 -1
  71. package/dist/router.js +2 -5
  72. package/dist/router.js.map +1 -1
  73. package/dist/runtime/boundaries.d.ts +24 -30
  74. package/dist/runtime/boundaries.d.ts.map +1 -1
  75. package/dist/runtime/boundaries.js +15 -22
  76. package/dist/runtime/boundaries.js.map +1 -1
  77. package/dist/runtime/client.d.ts +16 -7
  78. package/dist/runtime/client.d.ts.map +1 -1
  79. package/dist/runtime/client.js +16 -7
  80. package/dist/runtime/client.js.map +1 -1
  81. package/dist/runtime/context.d.ts +89 -118
  82. package/dist/runtime/context.d.ts.map +1 -1
  83. package/dist/runtime/context.js +107 -167
  84. package/dist/runtime/context.js.map +1 -1
  85. package/dist/runtime/control.js +3 -3
  86. package/dist/runtime/control.js.map +1 -1
  87. package/dist/runtime/dev-protocol.d.ts +4 -8
  88. package/dist/runtime/dev-protocol.d.ts.map +1 -1
  89. package/dist/runtime/dev-protocol.js.map +1 -1
  90. package/dist/runtime/entry.client.d.ts.map +1 -1
  91. package/dist/runtime/entry.client.js +92 -120
  92. package/dist/runtime/entry.client.js.map +1 -1
  93. package/dist/runtime/entry.rsc.d.ts +5 -6
  94. package/dist/runtime/entry.rsc.d.ts.map +1 -1
  95. package/dist/runtime/entry.rsc.js +67 -122
  96. package/dist/runtime/entry.rsc.js.map +1 -1
  97. package/dist/runtime/entry.ssr.d.ts +7 -12
  98. package/dist/runtime/entry.ssr.d.ts.map +1 -1
  99. package/dist/runtime/entry.ssr.js +13 -24
  100. package/dist/runtime/entry.ssr.js.map +1 -1
  101. package/dist/runtime/flight-inject.d.ts +10 -17
  102. package/dist/runtime/flight-inject.d.ts.map +1 -1
  103. package/dist/runtime/flight-inject.js +35 -53
  104. package/dist/runtime/flight-inject.js.map +1 -1
  105. package/dist/runtime/hot-update.d.ts +45 -0
  106. package/dist/runtime/hot-update.d.ts.map +1 -0
  107. package/dist/runtime/hot-update.js +44 -0
  108. package/dist/runtime/hot-update.js.map +1 -0
  109. package/dist/runtime/navigation.d.ts +14 -19
  110. package/dist/runtime/navigation.d.ts.map +1 -1
  111. package/dist/runtime/navigation.js +9 -13
  112. package/dist/runtime/navigation.js.map +1 -1
  113. package/dist/runtime/request.d.ts +4 -6
  114. package/dist/runtime/request.d.ts.map +1 -1
  115. package/dist/runtime/request.js +2 -3
  116. package/dist/runtime/request.js.map +1 -1
  117. package/dist/runtime/server.d.ts +18 -10
  118. package/dist/runtime/server.d.ts.map +1 -1
  119. package/dist/runtime/server.js +21 -19
  120. package/dist/runtime/server.js.map +1 -1
  121. package/dist/server/headers.d.ts +8 -15
  122. package/dist/server/headers.d.ts.map +1 -1
  123. package/dist/server/headers.js +8 -15
  124. package/dist/server/headers.js.map +1 -1
  125. package/dist/server/load-config.d.ts +2 -2
  126. package/dist/server/load-config.d.ts.map +1 -1
  127. package/dist/server/load-config.js +6 -9
  128. package/dist/server/load-config.js.map +1 -1
  129. package/dist/server/prerendered.d.ts +30 -43
  130. package/dist/server/prerendered.d.ts.map +1 -1
  131. package/dist/server/prerendered.js +20 -29
  132. package/dist/server/prerendered.js.map +1 -1
  133. package/dist/server/server-config.d.ts +18 -25
  134. package/dist/server/server-config.d.ts.map +1 -1
  135. package/dist/server/server-config.js +7 -11
  136. package/dist/server/server-config.js.map +1 -1
  137. package/dist/server/shutdown.d.ts +2 -3
  138. package/dist/server/shutdown.d.ts.map +1 -1
  139. package/dist/server/shutdown.js +2 -3
  140. package/dist/server/shutdown.js.map +1 -1
  141. package/dist/server/ssg.d.ts +3 -6
  142. package/dist/server/ssg.d.ts.map +1 -1
  143. package/dist/server/ssg.js +11 -20
  144. package/dist/server/ssg.js.map +1 -1
  145. package/package.json +1 -1
@@ -4,27 +4,21 @@ import { renderToReadableStream } from 'react-dom/server';
4
4
  import { createFromReadableStream } from 'react-server-dom-rspack/client';
5
5
  import { isControlDigest } from './control.js';
6
6
  import { injectFlightPayload } from './flight-inject.js';
7
- // From the baked config, not `process.env.NODE_ENV`: this is a property of the build, and a deploy
8
- // target need not have a `process` at all.
7
+ // The baked config, not `process.env.NODE_ENV`: a deploy target need not have a `process`.
9
8
  const isDev = __RSHONO_CONFIG__.isDev;
10
9
  /** Escapes text going into HTML body content — a stack trace is untrusted input. */
11
10
  function escapeHtml(text) {
12
11
  return text.replace(/[&<>]/g, (char) => (char === '&' ? '&amp;' : char === '<' ? '&lt;' : '&gt;'));
13
12
  }
14
13
  /**
15
- * The last-resort 500 document, for when SSR fails before a single byte of the real shell was sent.
14
+ * The last-resort 500 document, for when SSR fails before a byte of the real shell was sent.
16
15
  *
17
- * Deliberately plain HTML with no client runtime attached: the flight payload comes from the same
18
- * failed render, so hydrating it would mismatch and React — whose root container is the whole
19
- * `document` — would tear the page down, blanking the very message being rendered here. Styling is
20
- * inline because the page's stylesheet links were part of the render that just failed.
16
+ * Plain HTML with no client runtime and no stylesheet links: both came from the render that just failed,
17
+ * and hydrating a mismatched payload would tear the page down over this very message. A string rather than
18
+ * a component for the same reason — React is what failed. It must end with the document trailer, which
19
+ * `injectFlightPayload` holds back and re-emits.
21
20
  *
22
- * A string rather than a component, for the same reason: React is what just failed, and rendering
23
- * this through it again is a dependency the fallback does not need. It must end with the document
24
- * trailer, which is what `injectFlightPayload` holds back and re-emits.
25
- *
26
- * The detail is dev-only. In production this stays a generic message, matching how the `error` page
27
- * from routes.ts redacts.
21
+ * The detail is dev-only, matching how the app's `error` page redacts.
28
22
  */
29
23
  function failureDocument(error) {
30
24
  const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;
@@ -59,13 +53,10 @@ export async function renderHTML(rscStream, options) {
59
53
  payload ??= createFromReadableStream(rscForSsr, options.nonce ? { nonce: options.nonce } : undefined);
60
54
  return React.use(payload).root;
61
55
  }
62
- // React hands `onError` every error it meets while streaming, including ones an error boundary
63
- // contained — and with no handler installed it logs each one itself. Almost all of them are errors
64
- // it read out of the flight payload, where they arrive as React's redacted stand-in: a `digest`
65
- // and no message. The RSC layer has already reported the real one in full, so the default handler
66
- // prints an alarming, detail-free duplicate for a request a boundary handled perfectly well. Only
67
- // an error carrying no digest started life in this render — a client component that threw during
68
- // SSR — and that one nothing else will report.
56
+ // React hands `onError` every error it meets while streaming, contained ones included. Almost all arrive
57
+ // out of the flight payload as its redacted stand-in — a `digest` and no message — and the RSC layer has
58
+ // already reported those in full. Only an error carrying no digest started life in this render, and that
59
+ // one nothing else will report.
69
60
  let reported;
70
61
  const onError = (error) => {
71
62
  if (typeof error?.digest === 'string')
@@ -83,16 +74,14 @@ export async function renderHTML(rscStream, options) {
83
74
  formState: options.formState,
84
75
  signal: options.signal,
85
76
  nonce: options.nonce,
86
- // Deliberately returns nothing, so the digest React gives the client's `onRecoverableError`
87
- // stays exactly what it was before a handler was installed here.
77
+ // Returns nothing, so the digest React gives the client's `onRecoverableError` is unchanged.
88
78
  onError,
89
79
  });
90
80
  }
91
81
  catch (error) {
92
82
  if (isControlDigest(error?.digest))
93
83
  throw error;
94
- // `onError` runs first for the failure that aborts the shell, so this reports only what it let
95
- // through: an error out of the flight payload, whose detail the RSC layer alone has.
84
+ // `onError` runs first for the failure that aborts the shell, so this reports only what it let through.
96
85
  if (!options.signal?.aborted && error !== reported)
97
86
  options.onShellError?.(error);
98
87
  status = 500;
@@ -1 +1 @@
1
- {"version":3,"file":"entry.ssr.js","sourceRoot":"","sources":["../../src/runtime/entry.ssr.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport type { ReactFormState } from 'react-dom/client';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { createFromReadableStream } from 'react-server-dom-rspack/client';\nimport { isControlDigest } from './control.js';\nimport { injectFlightPayload } from './flight-inject.js';\nimport type { RscPayload } from './entry.rsc.js';\n\nexport interface RenderHTMLOptions {\n bootstrapScripts?: string[];\n formState?: ReactFormState;\n signal?: AbortSignal;\n nonce?: string;\n /**\n * Called when SSR fails before the shell is sent. Reporting is the RSC layer's job — this module\n * is compiled into the SSR layer, which gets its own instance of every module it imports, so a\n * handler registered through `@rshono/core/server` isn't reachable from in here.\n */\n onShellError?: (error: unknown) => void;\n /**\n * Called for an error that happened during SSR and *originated* in SSR — a client component that\n * threw while rendering on the server, whether a boundary went on to contain it or it took the\n * shell down with it. See {@link renderHTML} for why the ones that didn't originate here are\n * dropped instead.\n */\n onError?: (error: unknown) => void;\n /**\n * Called once the response stream has ended, however it ended.\n *\n * The RSC layer uses it to detach the listener forwarding client-disconnect aborts into this render —\n * see `renderComponent`. Without a hook here that listener outlives the request and retains the whole\n * rendered tree, so this is load-bearing rather than a convenience.\n */\n onDone?: () => void;\n}\n\n// From the baked config, not `process.env.NODE_ENV`: this is a property of the build, and a deploy\n// target need not have a `process` at all.\nconst isDev = __RSHONO_CONFIG__.isDev;\n\n/** Escapes text going into HTML body content — a stack trace is untrusted input. */\nfunction escapeHtml(text: string): string {\n return text.replace(/[&<>]/g, (char) => (char === '&' ? '&amp;' : char === '<' ? '&lt;' : '&gt;'));\n}\n\n/**\n * The last-resort 500 document, for when SSR fails before a single byte of the real shell was sent.\n *\n * Deliberately plain HTML with no client runtime attached: the flight payload comes from the same\n * failed render, so hydrating it would mismatch and React — whose root container is the whole\n * `document` — would tear the page down, blanking the very message being rendered here. Styling is\n * inline because the page's stylesheet links were part of the render that just failed.\n *\n * A string rather than a component, for the same reason: React is what just failed, and rendering\n * this through it again is a dependency the fallback does not need. It must end with the document\n * trailer, which is what `injectFlightPayload` holds back and re-emits.\n *\n * The detail is dev-only. In production this stays a generic message, matching how the `error` page\n * from routes.ts redacts.\n */\nfunction failureDocument(error: unknown): ReadableStream<Uint8Array> {\n const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;\n const html =\n '<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n '<title>500 — Internal Server Error</title></head>' +\n '<body style=\"margin:0;padding:2rem;font:16px/1.6 system-ui,-apple-system,sans-serif;color:#18181b\">' +\n '<h1 style=\"margin:0 0 .5rem;font-size:1.25rem\">500 — Internal Server Error</h1>' +\n '<p style=\"margin:0 0 1.5rem;color:#52525b\">' +\n (isDev\n ? 'Server-side rendering failed before the page shell could be sent, so the app’s error page could not be reached either.'\n : 'Something went wrong while rendering this page. Please try again.') +\n '</p>' +\n (detail\n ? '<pre style=\"margin:0;padding:1rem;overflow:auto;background:#f4f4f5;border-left:3px solid #ef4444;' +\n `font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word\">${escapeHtml(detail)}</pre>`\n : '') +\n '</body></html>';\n const bytes = new TextEncoder().encode(html);\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n}\n\nexport async function renderHTML(rscStream: ReadableStream<Uint8Array>, options: RenderHTMLOptions) {\n // One copy is rendered to HTML here; the other rides along in that HTML for the client to hydrate from.\n const [rscForSsr, rscForClient] = rscStream.tee();\n\n let payload: Promise<RscPayload>;\n function SsrRoot() {\n payload ??= createFromReadableStream<RscPayload>(rscForSsr, options.nonce ? { nonce: options.nonce } : undefined);\n return React.use(payload).root;\n }\n\n // React hands `onError` every error it meets while streaming, including ones an error boundary\n // contained — and with no handler installed it logs each one itself. Almost all of them are errors\n // it read out of the flight payload, where they arrive as React's redacted stand-in: a `digest`\n // and no message. The RSC layer has already reported the real one in full, so the default handler\n // prints an alarming, detail-free duplicate for a request a boundary handled perfectly well. Only\n // an error carrying no digest started life in this render — a client component that threw during\n // SSR — and that one nothing else will report.\n let reported: unknown;\n const onError = (error: unknown): void => {\n if (typeof (error as { digest?: unknown } | null)?.digest === 'string') return;\n if (options.signal?.aborted) return; // an abort is the client leaving, not a fault\n reported = error;\n options.onError?.(error);\n };\n\n let htmlStream: ReadableStream<Uint8Array>;\n let status: number | undefined;\n try {\n htmlStream = await renderToReadableStream(<SsrRoot />, {\n bootstrapScripts: options.bootstrapScripts,\n formState: options.formState,\n signal: options.signal,\n nonce: options.nonce,\n // Deliberately returns nothing, so the digest React gives the client's `onRecoverableError`\n // stays exactly what it was before a handler was installed here.\n onError,\n });\n } catch (error) {\n if (isControlDigest((error as { digest?: unknown } | null)?.digest)) throw error;\n // `onError` runs first for the failure that aborts the shell, so this reports only what it let\n // through: an error out of the flight payload, whose detail the RSC layer alone has.\n if (!options.signal?.aborted && error !== reported) options.onShellError?.(error);\n status = 500;\n htmlStream = failureDocument(error);\n }\n\n const responseStream = htmlStream.pipeThrough(injectFlightPayload(rscForClient, { nonce: options.nonce, onDone: options.onDone }));\n\n return { stream: responseStream, status };\n}\n"]}
1
+ {"version":3,"file":"entry.ssr.js","sourceRoot":"","sources":["../../src/runtime/entry.ssr.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport type { ReactFormState } from 'react-dom/client';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { createFromReadableStream } from 'react-server-dom-rspack/client';\nimport { isControlDigest } from './control.js';\nimport { injectFlightPayload } from './flight-inject.js';\nimport type { RscPayload } from './entry.rsc.js';\n\nexport interface RenderHTMLOptions {\n bootstrapScripts?: string[];\n formState?: ReactFormState;\n signal?: AbortSignal;\n nonce?: string;\n /**\n * Called when SSR fails before the shell is sent. Reporting is the RSC layer's job: this module is\n * compiled into the SSR layer, which gets its own instance of every module it imports, so a handler\n * registered through `@rshono/core/server` is not reachable from here.\n */\n onShellError?: (error: unknown) => void;\n /**\n * Called for an error that *originated* in SSR — a client component that threw while rendering on the\n * server. See {@link renderHTML} for why the others are dropped.\n */\n onError?: (error: unknown) => void;\n /**\n * Called once the response stream has ended, however it ended. Load-bearing: the RSC layer uses it to\n * detach the abort forwarder that would otherwise retain the whole rendered tree.\n */\n onDone?: () => void;\n}\n\n// The baked config, not `process.env.NODE_ENV`: a deploy target need not have a `process`.\nconst isDev = __RSHONO_CONFIG__.isDev;\n\n/** Escapes text going into HTML body content — a stack trace is untrusted input. */\nfunction escapeHtml(text: string): string {\n return text.replace(/[&<>]/g, (char) => (char === '&' ? '&amp;' : char === '<' ? '&lt;' : '&gt;'));\n}\n\n/**\n * The last-resort 500 document, for when SSR fails before a byte of the real shell was sent.\n *\n * Plain HTML with no client runtime and no stylesheet links: both came from the render that just failed,\n * and hydrating a mismatched payload would tear the page down over this very message. A string rather than\n * a component for the same reason — React is what failed. It must end with the document trailer, which\n * `injectFlightPayload` holds back and re-emits.\n *\n * The detail is dev-only, matching how the app's `error` page redacts.\n */\nfunction failureDocument(error: unknown): ReadableStream<Uint8Array> {\n const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;\n const html =\n '<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n '<title>500 — Internal Server Error</title></head>' +\n '<body style=\"margin:0;padding:2rem;font:16px/1.6 system-ui,-apple-system,sans-serif;color:#18181b\">' +\n '<h1 style=\"margin:0 0 .5rem;font-size:1.25rem\">500 — Internal Server Error</h1>' +\n '<p style=\"margin:0 0 1.5rem;color:#52525b\">' +\n (isDev\n ? 'Server-side rendering failed before the page shell could be sent, so the app’s error page could not be reached either.'\n : 'Something went wrong while rendering this page. Please try again.') +\n '</p>' +\n (detail\n ? '<pre style=\"margin:0;padding:1rem;overflow:auto;background:#f4f4f5;border-left:3px solid #ef4444;' +\n `font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word\">${escapeHtml(detail)}</pre>`\n : '') +\n '</body></html>';\n const bytes = new TextEncoder().encode(html);\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n}\n\nexport async function renderHTML(rscStream: ReadableStream<Uint8Array>, options: RenderHTMLOptions) {\n // One copy is rendered to HTML here; the other rides along in that HTML for the client to hydrate from.\n const [rscForSsr, rscForClient] = rscStream.tee();\n\n let payload: Promise<RscPayload>;\n function SsrRoot() {\n payload ??= createFromReadableStream<RscPayload>(rscForSsr, options.nonce ? { nonce: options.nonce } : undefined);\n return React.use(payload).root;\n }\n\n // React hands `onError` every error it meets while streaming, contained ones included. Almost all arrive\n // out of the flight payload as its redacted stand-in — a `digest` and no message — and the RSC layer has\n // already reported those in full. Only an error carrying no digest started life in this render, and that\n // one nothing else will report.\n let reported: unknown;\n const onError = (error: unknown): void => {\n if (typeof (error as { digest?: unknown } | null)?.digest === 'string') return;\n if (options.signal?.aborted) return; // an abort is the client leaving, not a fault\n reported = error;\n options.onError?.(error);\n };\n\n let htmlStream: ReadableStream<Uint8Array>;\n let status: number | undefined;\n try {\n htmlStream = await renderToReadableStream(<SsrRoot />, {\n bootstrapScripts: options.bootstrapScripts,\n formState: options.formState,\n signal: options.signal,\n nonce: options.nonce,\n // Returns nothing, so the digest React gives the client's `onRecoverableError` is unchanged.\n onError,\n });\n } catch (error) {\n if (isControlDigest((error as { digest?: unknown } | null)?.digest)) throw error;\n // `onError` runs first for the failure that aborts the shell, so this reports only what it let through.\n if (!options.signal?.aborted && error !== reported) options.onShellError?.(error);\n status = 500;\n htmlStream = failureDocument(error);\n }\n\n const responseStream = htmlStream.pipeThrough(injectFlightPayload(rscForClient, { nonce: options.nonce, onDone: options.onDone }));\n\n return { stream: responseStream, status };\n}\n"]}
@@ -1,25 +1,18 @@
1
1
  /**
2
- * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it
3
- * already has rather than fetching the page a second time.
2
+ * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has
3
+ * rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.
4
4
  *
5
- * The wire format is `rsc-html-stream`'s — a run of
6
- * `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>` tags — but the implementation is
7
- * first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document
8
- * trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write
9
- * that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it
10
- * back, and the document ends up with two trailers and the payload script after the first of them.
11
- * A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is
12
- * malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.
13
- *
14
- * The reader for this format is `readFlightPayload` in `entry.client.tsx`.
5
+ * The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>`
6
+ * tags — but the implementation is first-party, because that package tests each HTML chunk for the
7
+ * document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so
8
+ * `</body></html>` can arrive as two chunks and the document ends up with two trailers.
15
9
  */
16
10
  /**
17
- * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side.
11
+ * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side —
12
+ * declared here because the bundled lib types have not caught up with what Node calls.
18
13
  *
19
- * Node calls it (verified on 22.x) but the bundled lib types have not caught up, so it is declared
20
- * here rather than reached for with an `any`. It matters because `cancel` is the only notification
21
- * that a response ended *without* finishing, and both users of it release a listener that would
22
- * otherwise outlive the request.
14
+ * It matters because `cancel` is the only notification that a response ended *without* finishing, and both
15
+ * users of it release a listener that would otherwise outlive the request.
23
16
  */
24
17
  export type CancellableTransformer<I, O> = Transformer<I, O> & {
25
18
  cancel?: (reason?: unknown) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"flight-inject.d.ts","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,CAAC,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CAAC;AAwDvG,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GACpD,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAwJzC"}
1
+ {"version":3,"file":"flight-inject.d.ts","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,CAAC,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CAAC;AAkDvG,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GACpD,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAiJzC"}
@@ -1,17 +1,11 @@
1
1
  /**
2
- * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it
3
- * already has rather than fetching the page a second time.
2
+ * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has
3
+ * rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.
4
4
  *
5
- * The wire format is `rsc-html-stream`'s — a run of
6
- * `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>` tags — but the implementation is
7
- * first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document
8
- * trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write
9
- * that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it
10
- * back, and the document ends up with two trailers and the payload script after the first of them.
11
- * A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is
12
- * malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.
13
- *
14
- * The reader for this format is `readFlightPayload` in `entry.client.tsx`.
5
+ * The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>`
6
+ * tags — but the implementation is first-party, because that package tests each HTML chunk for the
7
+ * document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so
8
+ * `</body></html>` can arrive as two chunks and the document ends up with two trailers.
15
9
  */
16
10
  const encoder = new TextEncoder();
17
11
  /** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */
@@ -26,21 +20,16 @@ const unschedule = (handle) => {
26
20
  clearTimeout(handle);
27
21
  };
28
22
  /**
29
- * Escapes the two sequences that would end a `<script>` element early.
30
- *
31
- * Guarded by an `includes('<')` because a flight payload usually has no `<` in it at all, and the
32
- * scan is far cheaper than two regex passes over 30 kB. `</script` becomes `</\script` rather than
33
- * `<\/script`, which would break the valid JS `0</script/` (a regexp literal).
23
+ * Escapes the two sequences that would end a `<script>` element early. Guarded by `includes('<')`, which is
24
+ * far cheaper than two regex passes over 30 kB of payload that usually has no `<` in it. `</script` becomes
25
+ * `</\script`, not `<\/script`, which would break the valid JS `0</script/`.
34
26
  */
35
27
  function escapeScript(script) {
36
28
  return script.includes('<') ? script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1') : script;
37
29
  }
38
30
  /**
39
- * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes.
40
- *
41
- * Sliced rather than `String.fromCharCode(...chunk)`, which passes one argument per byte and
42
- * overflows the call stack on a chunk of any size. The slice width is a stack-safety choice, not a
43
- * tuned one — this is only reached for a chunk that split a multi-byte character.
31
+ * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes. Sliced because
32
+ * `String.fromCharCode(...chunk)` passes one argument per byte and overflows the stack.
44
33
  */
45
34
  function latin1(chunk) {
46
35
  let out = '';
@@ -68,13 +57,11 @@ export function injectFlightPayload(rscStream, options = {}) {
68
57
  const batch = [];
69
58
  let boundary = null;
70
59
  /**
71
- * Set once the consumer has gone away, so nothing downstream tries to enqueue into a readable that
72
- * can no longer take it.
60
+ * Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.
73
61
  *
74
- * It cannot simply be the `cancel` hook that sets this. Per the Streams standard, cancelling the
75
- * readable *after* the close algorithm has started returns the pending finish promise without
76
- * running the transformer's `cancel` at all — and `flush` awaiting the whole flight payload is
77
- * precisely that window. So a failed enqueue is also treated as the signal, wherever one happens.
62
+ * The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the
63
+ * close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole
64
+ * payload is precisely that window. So a failed enqueue counts as the signal too.
78
65
  */
79
66
  let cancelled = false;
80
67
  /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */
@@ -83,10 +70,9 @@ export function injectFlightPayload(rscStream, options = {}) {
83
70
  * Emits the HTML buffered since the last boundary, holding back the document trailer for
84
71
  * {@link TransformStream.flush} to re-emit after the payload scripts.
85
72
  *
86
- * The batch is joined before the trailer is looked for, so a trailer React split across two views
87
- * is still found — that is the whole reason this module is not `rsc-html-stream/server`. React
88
- * writes its final flush in one synchronous run, so every chunk of the trailer lands in the same
89
- * batch; a trailer split *across* batches is not a shape React produces.
73
+ * The batch is joined before the trailer is looked for, so one React split across two views is still
74
+ * found. React writes its final flush in one synchronous run, so a trailer split across *batches* is not
75
+ * a shape it produces.
90
76
  */
91
77
  function emitBatch(controller) {
92
78
  boundary = null;
@@ -110,8 +96,8 @@ export function injectFlightPayload(rscStream, options = {}) {
110
96
  }
111
97
  async function writeFlight(controller) {
112
98
  const reader = (flightReader = rscStream.getReader());
113
- // `fatal`, so a chunk that split a multi-byte character throws rather than emitting U+FFFD and
114
- // corrupting the payload — the catch below falls back to a byte-exact encoding for it.
99
+ // `fatal`, so a chunk that split a multi-byte character throws instead of emitting U+FFFD; the catch
100
+ // below falls back to a byte-exact encoding for it.
115
101
  const decoder = new TextDecoder('utf-8', { fatal: true });
116
102
  const push = (literal) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));
117
103
  for (;;) {
@@ -120,9 +106,8 @@ export function injectFlightPayload(rscStream, options = {}) {
120
106
  const { done, value } = await reader.read();
121
107
  if (done)
122
108
  break;
123
- // Only the *decode* is guarded. Wrapping the `push` in the same `try` conflated a split
124
- // multi-byte character with a controller nobody is reading any more, and answered the second by
125
- // re-encoding the chunk and enqueueing it again — which throws in turn, out of the catch.
109
+ // Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by
110
+ // re-encoding the chunk and enqueueing it again.
126
111
  let literal;
127
112
  try {
128
113
  literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));
@@ -132,8 +117,8 @@ export function injectFlightPayload(rscStream, options = {}) {
132
117
  }
133
118
  if (cancelled)
134
119
  return;
135
- // A failed enqueue means the consumer is gone. Stop pumping and release the RSC branch, so
136
- // `flush` unparks now rather than whenever the flight payload would have ended on its own.
120
+ // A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather
121
+ // than whenever the payload would have ended on its own.
137
122
  try {
138
123
  push(literal);
139
124
  }
@@ -154,10 +139,8 @@ export function injectFlightPayload(rscStream, options = {}) {
154
139
  batch.push(chunk);
155
140
  if (boundary)
156
141
  return;
157
- // A macrotask, not a microtask: React writes a whole flush into its stream in one synchronous
158
- // run, but `pipeThrough` delivers those chunks to us one microtask at a time. Only a task
159
- // boundary guarantees the flush has arrived in full — a script injected between two of its
160
- // chunks would land inside a tag.
142
+ // A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`
143
+ // delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.
161
144
  boundary = schedule(() => {
162
145
  try {
163
146
  emitBatch(controller);
@@ -169,7 +152,9 @@ export function injectFlightPayload(rscStream, options = {}) {
169
152
  }
170
153
  if (!startedFlight) {
171
154
  startedFlight = true;
172
- writeFlight(controller)
155
+ // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and
156
+ // the chain already routes a write failure to `controller.error` before settling `flightDone`.
157
+ void writeFlight(controller)
173
158
  .catch((error) => controller.error(error))
174
159
  .then(flightDone);
175
160
  }
@@ -177,11 +162,9 @@ export function injectFlightPayload(rscStream, options = {}) {
177
162
  },
178
163
  async flush(controller) {
179
164
  await flightWritten;
180
- // That await spans the entire flight payload, and the consumer can go away inside it — a
181
- // browser's stop button, a navigation away, a proxy timeout. `cancel` below is *not* what tells
182
- // us so (see `cancelled`), which leaves the enqueue throwing `ERR_INVALID_STATE` as the only
183
- // signal. Unguarded it rejects `flush`, and nothing owns that rejection: it surfaces as an
184
- // unhandled one and, where the host does not swallow it, takes the process down.
165
+ // That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped
166
+ // (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which
167
+ // nothing owns and which surfaces as an unhandled rejection.
185
168
  try {
186
169
  if (boundary) {
187
170
  unschedule(boundary);
@@ -196,8 +179,7 @@ export function injectFlightPayload(rscStream, options = {}) {
196
179
  flightReader?.cancel().catch(() => { });
197
180
  }
198
181
  finally {
199
- // Unconditional, and the reason this is a `finally`: `onDone` is what releases the abort
200
- // forwarder in `renderComponent`, so it has to run however the response ended.
182
+ // A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.
201
183
  onDone?.();
202
184
  }
203
185
  },
@@ -208,8 +190,8 @@ export function injectFlightPayload(rscStream, options = {}) {
208
190
  boundary = null;
209
191
  }
210
192
  batch.length = 0;
211
- // Without this the teed RSC branch keeps being pumped for a response nobody will read, and the
212
- // tee's other half buffers every chunk waiting for this one to catch up.
193
+ // Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's
194
+ // other half buffers every chunk waiting for this one to catch up.
213
195
  flightReader?.cancel(reason).catch(() => { });
214
196
  // Unparks `flush` if it is waiting on a payload that will now never arrive.
215
197
  flightDone();
@@ -1 +1 @@
1
- {"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it\n * already has rather than fetching the page a second time.\n *\n * The wire format is `rsc-html-stream`'s — a run of\n * `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>` tags — but the implementation is\n * first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document\n * trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write\n * that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it\n * back, and the document ends up with two trailers and the payload script after the first of them.\n * A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is\n * malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.\n *\n * The reader for this format is `readFlightPayload` in `entry.client.tsx`.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side.\n *\n * Node calls it (verified on 22.x) but the bundled lib types have not caught up, so it is declared\n * here rather than reached for with an `any`. It matters because `cancel` is the only notification\n * that a response ended *without* finishing, and both users of it release a listener that would\n * otherwise outlive the request.\n */\nexport type CancellableTransformer<I, O> = Transformer<I, O> & { cancel?: (reason?: unknown) => void };\n\nconst encoder = new TextEncoder();\n\n/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */\nconst TRAILER = '</body></html>';\nconst TRAILER_BYTES = encoder.encode(TRAILER);\n\n/**\n * A macrotask boundary. `setImmediate` where there is one (Node, Bun, Deno's node compat), which is\n * the current turn's check phase rather than a timer; `setTimeout` is the portable fallback that\n * every other runtime has. A Node timer has a 1ms floor and every HTML flush would pay it — worth\n * the two lines: uncontended time-to-last-byte on a 30 kB payload, 4.7ms → 3.0ms.\n */\ntype TaskHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;\nconst hasSetImmediate = typeof setImmediate === 'function';\nconst schedule: (fn: () => void) => TaskHandle = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);\nconst unschedule = (handle: TaskHandle): void => {\n if (hasSetImmediate) clearImmediate(handle as ReturnType<typeof setImmediate>);\n else clearTimeout(handle as ReturnType<typeof setTimeout>);\n};\n\n/**\n * Escapes the two sequences that would end a `<script>` element early.\n *\n * Guarded by an `includes('<')` because a flight payload usually has no `<` in it at all, and the\n * scan is far cheaper than two regex passes over 30 kB. `</script` becomes `</\\script` rather than\n * `<\\/script`, which would break the valid JS `0</script/` (a regexp literal).\n */\nfunction escapeScript(script: string): string {\n return script.includes('<') ? script.replace(/<!--/g, '<\\\\!--').replace(/<\\/(script)/gi, '</\\\\$1') : script;\n}\n\n/**\n * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes.\n *\n * Sliced rather than `String.fromCharCode(...chunk)`, which passes one argument per byte and\n * overflows the call stack on a chunk of any size. The slice width is a stack-safety choice, not a\n * tuned one — this is only reached for a chunk that split a multi-byte character.\n */\nfunction latin1(chunk: Uint8Array): string {\n let out = '';\n for (let at = 0; at < chunk.length; at += 8192) out += String.fromCharCode(...chunk.subarray(at, at + 8192));\n return out;\n}\n\n/** Whether `buffer`'s first `length` bytes end with the document trailer. */\nfunction endsWithTrailer(buffer: Uint8Array, length: number): boolean {\n if (length < TRAILER_BYTES.length) return false;\n const from = length - TRAILER_BYTES.length;\n for (let i = 0; i < TRAILER_BYTES.length; i++) {\n if (buffer[from + i] !== TRAILER_BYTES[i]) return false;\n }\n return true;\n}\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): TransformStream<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const scriptOpen = `<script${nonce ? ` nonce=\"${nonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing downstream tries to enqueue into a readable that\n * can no longer take it.\n *\n * It cannot simply be the `cancel` hook that sets this. Per the Streams standard, cancelling the\n * readable *after* the close algorithm has started returns the pending finish promise without\n * running the transformer's `cancel` at all — and `flush` awaiting the whole flight payload is\n * precisely that window. So a failed enqueue is also treated as the signal, wherever one happens.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for\n * {@link TransformStream.flush} to re-emit after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so a trailer React split across two views\n * is still found — that is the whole reason this module is not `rsc-html-stream/server`. React\n * writes its final flush in one synchronous run, so every chunk of the trailer lands in the same\n * batch; a trailer split *across* batches is not a shape React produces.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>): void {\n boundary = null;\n let total = 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // `fatal`, so a chunk that split a multi-byte character throws rather than emitting U+FFFD and\n // corrupting the payload — the catch below falls back to a byte-exact encoding for it.\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const push = (literal: string) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the *decode* is guarded. Wrapping the `push` in the same `try` conflated a split\n // multi-byte character with a controller nobody is reading any more, and answered the second by\n // re-encoding the chunk and enqueueing it again — which throws in turn, out of the catch.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone. Stop pumping and release the RSC branch, so\n // `flush` unparks now rather than whenever the flight payload would have ended on its own.\n try {\n push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n if (cancelled) return;\n const remaining = decoder.decode();\n if (remaining.length) push(escapeScript(JSON.stringify(remaining)));\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n transform(chunk, controller) {\n batch.push(chunk);\n if (boundary) return;\n // A macrotask, not a microtask: React writes a whole flush into its stream in one synchronous\n // run, but `pipeThrough` delivers those chunks to us one microtask at a time. Only a task\n // boundary guarantees the flush has arrived in full — a script injected between two of its\n // chunks would land inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n controller.error(error);\n flightDone();\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n await flightWritten;\n // That await spans the entire flight payload, and the consumer can go away inside it — a\n // browser's stop button, a navigation away, a proxy timeout. `cancel` below is *not* what tells\n // us so (see `cancelled`), which leaves the enqueue throwing `ERR_INVALID_STATE` as the only\n // signal. Unguarded it rejects `flush`, and nothing owns that rejection: it surfaces as an\n // unhandled one and, where the host does not swallow it, takes the process down.\n try {\n if (boundary) {\n unschedule(boundary);\n emitBatch(controller);\n }\n if (!cancelled) controller.enqueue(encoder.encode(TRAILER));\n } catch {\n // Nowhere left to put the trailer. A response the client abandoned is not a fault.\n cancelled = true;\n flightReader?.cancel().catch(() => {});\n } finally {\n // Unconditional, and the reason this is a `finally`: `onDone` is what releases the abort\n // forwarder in `renderComponent`, so it has to run however the response ended.\n onDone?.();\n }\n },\n cancel(reason) {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n // Without this the teed RSC branch keeps being pumped for a response nobody will read, and the\n // tee's other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n onDone?.();\n },\n };\n return new TransformStream<Uint8Array, Uint8Array>(transformer);\n}\n"]}
1
+ {"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has\n * rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.\n *\n * The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>`\n * tags — but the implementation is first-party, because that package tests each HTML chunk for the\n * document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so\n * `</body></html>` can arrive as two chunks and the document ends up with two trailers.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side —\n * declared here because the bundled lib types have not caught up with what Node calls.\n *\n * It matters because `cancel` is the only notification that a response ended *without* finishing, and both\n * users of it release a listener that would otherwise outlive the request.\n */\nexport type CancellableTransformer<I, O> = Transformer<I, O> & { cancel?: (reason?: unknown) => void };\n\nconst encoder = new TextEncoder();\n\n/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */\nconst TRAILER = '</body></html>';\nconst TRAILER_BYTES = encoder.encode(TRAILER);\n\n/**\n * A macrotask boundary: `setImmediate` where there is one, which is the current turn's check phase rather\n * than a timer, and `setTimeout` as the portable fallback. A Node timer has a 1ms floor that every HTML\n * flush would pay — 4.7ms → 3.0ms time-to-last-byte on a 30 kB payload.\n */\ntype TaskHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;\nconst hasSetImmediate = typeof setImmediate === 'function';\nconst schedule: (fn: () => void) => TaskHandle = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);\nconst unschedule = (handle: TaskHandle): void => {\n if (hasSetImmediate) clearImmediate(handle as ReturnType<typeof setImmediate>);\n else clearTimeout(handle as ReturnType<typeof setTimeout>);\n};\n\n/**\n * Escapes the two sequences that would end a `<script>` element early. Guarded by `includes('<')`, which is\n * far cheaper than two regex passes over 30 kB of payload that usually has no `<` in it. `</script` becomes\n * `</\\script`, not `<\\/script`, which would break the valid JS `0</script/`.\n */\nfunction escapeScript(script: string): string {\n return script.includes('<') ? script.replace(/<!--/g, '<\\\\!--').replace(/<\\/(script)/gi, '</\\\\$1') : script;\n}\n\n/**\n * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes. Sliced because\n * `String.fromCharCode(...chunk)` passes one argument per byte and overflows the stack.\n */\nfunction latin1(chunk: Uint8Array): string {\n let out = '';\n for (let at = 0; at < chunk.length; at += 8192) out += String.fromCharCode(...chunk.subarray(at, at + 8192));\n return out;\n}\n\n/** Whether `buffer`'s first `length` bytes end with the document trailer. */\nfunction endsWithTrailer(buffer: Uint8Array, length: number): boolean {\n if (length < TRAILER_BYTES.length) return false;\n const from = length - TRAILER_BYTES.length;\n for (let i = 0; i < TRAILER_BYTES.length; i++) {\n if (buffer[from + i] !== TRAILER_BYTES[i]) return false;\n }\n return true;\n}\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): TransformStream<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const scriptOpen = `<script${nonce ? ` nonce=\"${nonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.\n *\n * The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the\n * close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole\n * payload is precisely that window. So a failed enqueue counts as the signal too.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for\n * {@link TransformStream.flush} to re-emit after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so one React split across two views is still\n * found. React writes its final flush in one synchronous run, so a trailer split across *batches* is not\n * a shape it produces.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>): void {\n boundary = null;\n let total = 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // `fatal`, so a chunk that split a multi-byte character throws instead of emitting U+FFFD; the catch\n // below falls back to a byte-exact encoding for it.\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const push = (literal: string) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by\n // re-encoding the chunk and enqueueing it again.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather\n // than whenever the payload would have ended on its own.\n try {\n push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n if (cancelled) return;\n const remaining = decoder.decode();\n if (remaining.length) push(escapeScript(JSON.stringify(remaining)));\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n transform(chunk, controller) {\n batch.push(chunk);\n if (boundary) return;\n // A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`\n // delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n controller.error(error);\n flightDone();\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and\n // the chain already routes a write failure to `controller.error` before settling `flightDone`.\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n await flightWritten;\n // That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped\n // (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which\n // nothing owns and which surfaces as an unhandled rejection.\n try {\n if (boundary) {\n unschedule(boundary);\n emitBatch(controller);\n }\n if (!cancelled) controller.enqueue(encoder.encode(TRAILER));\n } catch {\n // Nowhere left to put the trailer. A response the client abandoned is not a fault.\n cancelled = true;\n flightReader?.cancel().catch(() => {});\n } finally {\n // A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.\n onDone?.();\n }\n },\n cancel(reason) {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n // Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's\n // other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n onDone?.();\n },\n };\n return new TransformStream<Uint8Array, Uint8Array>(transformer);\n}\n"]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Walking the page from the build it is running to the build the dev server has announced. Its own module
3
+ * because every decision in it is about *giving up correctly*, which is worth testing on its own.
4
+ *
5
+ * Dev-only: the sole caller sits behind `import.meta.webpackHot`, which a production build replaces with
6
+ * `false`.
7
+ */
8
+ /** The slice of `import.meta.webpackHot` the walk uses. */
9
+ export interface HotRuntime {
10
+ /**
11
+ * Fetches the update manifest for the build the page is running and, with `autoApply`, applies it.
12
+ *
13
+ * Resolves with the updated module ids, or with **null** when the manifest 404s — which the runtime
14
+ * treats as "nothing to do" rather than an error. Rejects only when an update was found and could not be
15
+ * applied.
16
+ */
17
+ check(autoApply?: boolean): Promise<Array<string | number> | null>;
18
+ status(): string;
19
+ }
20
+ /** Why the page has to be reloaded rather than patched — `error` only where one was thrown. */
21
+ export interface ReloadReason {
22
+ reason: string;
23
+ error?: unknown;
24
+ }
25
+ /**
26
+ * Advances the page to `targetHash()`, applying one build's worth of updates per round, and reports whether
27
+ * it got there.
28
+ *
29
+ * Both hashes are read through functions because both move underneath this loop: an applied update rewrites
30
+ * `__webpack_hash__`, and a save landing mid-walk moves the target further out. Reading them fresh each
31
+ * round is what lets one walk absorb a burst of saves.
32
+ *
33
+ * Returns `null` once the page is on the target build, or the reason it cannot get there — every one of
34
+ * which is a reload, because the alternative is a page that silently stops updating:
35
+ *
36
+ * - **An update was already in flight.** `check` may only be called from `idle`.
37
+ * - **`check` rejected.** An update was found and could not be applied — usually a module that declines
38
+ * them, which is the ordinary cost of changing something react-refresh cannot patch.
39
+ * - **`check` resolved with null, or applied without moving the hash.** The chain of `*.hot-update.json`
40
+ * files leading to the target is broken — as it is after a dev-server restart, whose first act is to wipe
41
+ * the output directory an already-open tab would ask for. The hash cannot move from there, so retrying
42
+ * would re-request the same 404 for as long as the tab stays open.
43
+ */
44
+ export declare function walkHotUpdates(hot: HotRuntime, currentHash: () => string, targetHash: () => string | undefined): Promise<ReloadReason | null>;
45
+ //# sourceMappingURL=hot-update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hot-update.d.ts","sourceRoot":"","sources":["../../src/runtime/hot-update.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,2DAA2D;AAC3D,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IACnE,MAAM,IAAI,MAAM,CAAC;CAClB;AAED,+FAA+F;AAC/F,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,MAAM,EAAE,UAAU,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAanJ"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Walking the page from the build it is running to the build the dev server has announced. Its own module
3
+ * because every decision in it is about *giving up correctly*, which is worth testing on its own.
4
+ *
5
+ * Dev-only: the sole caller sits behind `import.meta.webpackHot`, which a production build replaces with
6
+ * `false`.
7
+ */
8
+ /**
9
+ * Advances the page to `targetHash()`, applying one build's worth of updates per round, and reports whether
10
+ * it got there.
11
+ *
12
+ * Both hashes are read through functions because both move underneath this loop: an applied update rewrites
13
+ * `__webpack_hash__`, and a save landing mid-walk moves the target further out. Reading them fresh each
14
+ * round is what lets one walk absorb a burst of saves.
15
+ *
16
+ * Returns `null` once the page is on the target build, or the reason it cannot get there — every one of
17
+ * which is a reload, because the alternative is a page that silently stops updating:
18
+ *
19
+ * - **An update was already in flight.** `check` may only be called from `idle`.
20
+ * - **`check` rejected.** An update was found and could not be applied — usually a module that declines
21
+ * them, which is the ordinary cost of changing something react-refresh cannot patch.
22
+ * - **`check` resolved with null, or applied without moving the hash.** The chain of `*.hot-update.json`
23
+ * files leading to the target is broken — as it is after a dev-server restart, whose first act is to wipe
24
+ * the output directory an already-open tab would ask for. The hash cannot move from there, so retrying
25
+ * would re-request the same 404 for as long as the tab stays open.
26
+ */
27
+ export async function walkHotUpdates(hot, currentHash, targetHash) {
28
+ while (targetHash() !== undefined && targetHash() !== currentHash()) {
29
+ if (hot.status() !== 'idle')
30
+ return { reason: 'a hot update was already in flight' };
31
+ const before = currentHash();
32
+ let applied;
33
+ try {
34
+ applied = await hot.check(true);
35
+ }
36
+ catch (error) {
37
+ return { reason: 'a hot update failed to apply', error };
38
+ }
39
+ if (applied === null || currentHash() === before)
40
+ return { reason: 'this build cannot be applied on top of the page' };
41
+ }
42
+ return null;
43
+ }
44
+ //# sourceMappingURL=hot-update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hot-update.js","sourceRoot":"","sources":["../../src/runtime/hot-update.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAe,EAAE,WAAyB,EAAE,UAAoC;IACnH,OAAO,UAAU,EAAE,KAAK,SAAS,IAAI,UAAU,EAAE,KAAK,WAAW,EAAE,EAAE,CAAC;QACpE,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,oCAAoC,EAAE,CAAC;QACrF,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;QAC7B,IAAI,OAAsC,CAAC;QAC3C,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,MAAM,EAAE,8BAA8B,EAAE,KAAK,EAAE,CAAC;QAC3D,CAAC;QACD,IAAI,OAAO,KAAK,IAAI,IAAI,WAAW,EAAE,KAAK,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,iDAAiD,EAAE,CAAC;IACzH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * Walking the page from the build it is running to the build the dev server has announced. Its own module\n * because every decision in it is about *giving up correctly*, which is worth testing on its own.\n *\n * Dev-only: the sole caller sits behind `import.meta.webpackHot`, which a production build replaces with\n * `false`.\n */\n\n/** The slice of `import.meta.webpackHot` the walk uses. */\nexport interface HotRuntime {\n /**\n * Fetches the update manifest for the build the page is running and, with `autoApply`, applies it.\n *\n * Resolves with the updated module ids, or with **null** when the manifest 404s — which the runtime\n * treats as \"nothing to do\" rather than an error. Rejects only when an update was found and could not be\n * applied.\n */\n check(autoApply?: boolean): Promise<Array<string | number> | null>;\n status(): string;\n}\n\n/** Why the page has to be reloaded rather than patched — `error` only where one was thrown. */\nexport interface ReloadReason {\n reason: string;\n error?: unknown;\n}\n\n/**\n * Advances the page to `targetHash()`, applying one build's worth of updates per round, and reports whether\n * it got there.\n *\n * Both hashes are read through functions because both move underneath this loop: an applied update rewrites\n * `__webpack_hash__`, and a save landing mid-walk moves the target further out. Reading them fresh each\n * round is what lets one walk absorb a burst of saves.\n *\n * Returns `null` once the page is on the target build, or the reason it cannot get there — every one of\n * which is a reload, because the alternative is a page that silently stops updating:\n *\n * - **An update was already in flight.** `check` may only be called from `idle`.\n * - **`check` rejected.** An update was found and could not be applied — usually a module that declines\n * them, which is the ordinary cost of changing something react-refresh cannot patch.\n * - **`check` resolved with null, or applied without moving the hash.** The chain of `*.hot-update.json`\n * files leading to the target is broken — as it is after a dev-server restart, whose first act is to wipe\n * the output directory an already-open tab would ask for. The hash cannot move from there, so retrying\n * would re-request the same 404 for as long as the tab stays open.\n */\nexport async function walkHotUpdates(hot: HotRuntime, currentHash: () => string, targetHash: () => string | undefined): Promise<ReloadReason | null> {\n while (targetHash() !== undefined && targetHash() !== currentHash()) {\n if (hot.status() !== 'idle') return { reason: 'a hot update was already in flight' };\n const before = currentHash();\n let applied: Array<string | number> | null;\n try {\n applied = await hot.check(true);\n } catch (error) {\n return { reason: 'a hot update failed to apply', error };\n }\n if (applied === null || currentHash() === before) return { reason: 'this build cannot be applied on top of the page' };\n }\n return null;\n}\n"]}
@@ -2,9 +2,9 @@ import { type ReactNode } from 'react';
2
2
  /**
3
3
  * Imperative navigation actions, reached as `useNavigation().router`.
4
4
  *
5
- * `push` / `replace` / `refresh` are **soft** navigations: the new page's flight
6
- * payload is fetched and applied in place, so client component state outside the
7
- * changed subtree survives. Off-site or non-HTTP hrefs fall back to a full load.
5
+ * `push` / `replace` / `refresh` are **soft** navigations: the new page's flight payload is fetched and
6
+ * applied in place, so client component state outside the changed subtree survives. Off-site hrefs fall
7
+ * back to a full load.
8
8
  *
9
9
  * @example
10
10
  * ```tsx
@@ -27,9 +27,8 @@ export interface NavigationRouter {
27
27
  /** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */
28
28
  export interface NavigationState {
29
29
  /**
30
- * The full current {@link URL} — read `url.pathname`, `url.searchParams` and the
31
- * rest off it. A fresh instance per navigation, so mutating it affects nothing
32
- * else; it is not written back to the address bar either.
30
+ * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else
31
+ * — it is not written back to the address bar.
33
32
  */
34
33
  url: URL;
35
34
  /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */
@@ -38,16 +37,14 @@ export interface NavigationState {
38
37
  router: NavigationRouter;
39
38
  }
40
39
  /**
41
- * Carries the live {@link NavigationRouter} implementation from the hydration runtime down
42
- * to {@link RouterProvider}. Framework internal — read the router through
43
- * {@link useNavigation} instead.
40
+ * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.
44
41
  *
45
42
  * @internal
46
43
  */
47
44
  export declare const RouterContext: import("react").Context<NavigationRouter>;
48
45
  /**
49
- * Publishes the per-render location and params so {@link useNavigation} can read
50
- * them. Framework internal — the RSC entry wraps every page in one.
46
+ * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps
47
+ * every page in one.
51
48
  *
52
49
  * @internal
53
50
  */
@@ -57,16 +54,14 @@ export declare function RouterProvider({ href, params, children }: {
57
54
  children: ReactNode;
58
55
  }): import("react").JSX.Element;
59
56
  /**
60
- * Reactive access to the current URL and programmatic navigation, in one hook.
57
+ * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a
58
+ * `'use client'` component.
61
59
  *
62
- * Call it from a `'use client'` component. The location fields (`url` and
63
- * `params`) are computed on the server and travel in the flight payload, so they
64
- * are correct during SSR — no hydration flicker — and update automatically on
65
- * every navigation. The `router` sub-object holds the imperative actions plus a
66
- * `pending` flag that is `true` while a client navigation is in flight.
60
+ * `url` and `params` are computed on the server and travel in the flight payload, so they are correct
61
+ * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative
62
+ * actions plus a `pending` flag, `true` while a soft navigation is in flight.
67
63
  *
68
- * Hooks can't run in a server component; read the same URL data there from
69
- * `getRequestContext()` (`@rshono/core/server`) instead.
64
+ * Hooks can't run in a server component; read the same data there from `getRequestContext()`.
70
65
  *
71
66
  * @example
72
67
  * ```tsx