@rshono/core 1.0.0-rc.22 → 1.0.0-rc.24
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/README.md +3 -1
- package/dist/router.d.ts +5 -3
- package/dist/router.js.map +1 -1
- package/dist/runtime/entry.rsc.js +17 -5
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +11 -12
- package/dist/runtime/entry.ssr.js +18 -11
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +4 -1
- package/dist/runtime/flight-inject.js +70 -20
- package/dist/runtime/flight-inject.js.map +1 -1
- package/dist/runtime/navigation.d.ts +14 -3
- package/dist/runtime/navigation.js +38 -5
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/server/prerendered.d.ts +7 -11
- package/dist/server/prerendered.js +23 -9
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/ssg.js +40 -18
- package/dist/server/ssg.js.map +1 -1
- package/package.json +4 -4
|
@@ -79,8 +79,51 @@ function trailerPrefixLength(buffer, length) {
|
|
|
79
79
|
* where an escaped garbage nonce would be a silent one.
|
|
80
80
|
*/
|
|
81
81
|
const NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;
|
|
82
|
+
function createFlightCapture() {
|
|
83
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
84
|
+
// A render can be abandoned before the SSG pass receives this promise. Observing it here leaves the
|
|
85
|
+
// original promise rejected for a later await without risking an unhandled rejection in that gap.
|
|
86
|
+
void promise.catch(() => { });
|
|
87
|
+
let chunks = [];
|
|
88
|
+
let byteLength = 0;
|
|
89
|
+
const fail = (reason) => {
|
|
90
|
+
if (!chunks)
|
|
91
|
+
return;
|
|
92
|
+
chunks = undefined;
|
|
93
|
+
reject(reason ?? new Error('Flight capture was cancelled'));
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
promise,
|
|
97
|
+
push(chunk) {
|
|
98
|
+
if (!chunks)
|
|
99
|
+
return;
|
|
100
|
+
chunks.push(chunk);
|
|
101
|
+
byteLength += chunk.byteLength;
|
|
102
|
+
},
|
|
103
|
+
complete() {
|
|
104
|
+
const captured = chunks;
|
|
105
|
+
if (!captured)
|
|
106
|
+
return;
|
|
107
|
+
chunks = undefined;
|
|
108
|
+
try {
|
|
109
|
+
const bytes = new Uint8Array(byteLength);
|
|
110
|
+
let offset = 0;
|
|
111
|
+
for (const chunk of captured) {
|
|
112
|
+
bytes.set(chunk, offset);
|
|
113
|
+
offset += chunk.byteLength;
|
|
114
|
+
}
|
|
115
|
+
resolve(bytes);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
reject(error);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
fail,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
82
124
|
export function injectFlightPayload(rscStream, options = {}) {
|
|
83
125
|
const { nonce, onDone } = options;
|
|
126
|
+
const capture = options.captureFlight ? createFlightCapture() : undefined;
|
|
84
127
|
const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;
|
|
85
128
|
const scriptOpen = `<script${safeNonce ? ` nonce="${safeNonce}"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;
|
|
86
129
|
const scriptClose = ')</script>';
|
|
@@ -178,6 +221,7 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
178
221
|
carry = null;
|
|
179
222
|
for (const resolve of waiting.splice(0))
|
|
180
223
|
resolve();
|
|
224
|
+
capture?.fail(reason);
|
|
181
225
|
// Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's
|
|
182
226
|
// other half buffers every chunk waiting for this one to catch up.
|
|
183
227
|
flightReader?.cancel(reason).catch(() => { });
|
|
@@ -256,8 +300,10 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
256
300
|
if (cancelled)
|
|
257
301
|
return;
|
|
258
302
|
const { done, value } = await reader.read();
|
|
259
|
-
if (done)
|
|
303
|
+
if (done) {
|
|
304
|
+
capture?.complete();
|
|
260
305
|
break;
|
|
306
|
+
}
|
|
261
307
|
// Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by
|
|
262
308
|
// re-encoding the chunk and enqueueing it again.
|
|
263
309
|
let literal;
|
|
@@ -273,9 +319,11 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
273
319
|
// than whenever the payload would have ended on its own.
|
|
274
320
|
try {
|
|
275
321
|
await push(literal);
|
|
322
|
+
capture?.push(value);
|
|
276
323
|
}
|
|
277
|
-
catch {
|
|
324
|
+
catch (error) {
|
|
278
325
|
cancelled = true;
|
|
326
|
+
capture?.fail(error);
|
|
279
327
|
reader.cancel().catch(() => { });
|
|
280
328
|
return;
|
|
281
329
|
}
|
|
@@ -283,6 +331,21 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
283
331
|
// No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left
|
|
284
332
|
// to emit — and the flush this replaces could throw, erroring the response mid-document. See above.
|
|
285
333
|
}
|
|
334
|
+
function startFlight(controller) {
|
|
335
|
+
if (startedFlight)
|
|
336
|
+
return;
|
|
337
|
+
startedFlight = true;
|
|
338
|
+
if (cancelled) {
|
|
339
|
+
flightDone();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
void writeFlight(controller)
|
|
343
|
+
.catch((error) => {
|
|
344
|
+
capture?.fail(error);
|
|
345
|
+
controller.error(error);
|
|
346
|
+
})
|
|
347
|
+
.then(flightDone);
|
|
348
|
+
}
|
|
286
349
|
const transformer = {
|
|
287
350
|
async transform(chunk, controller) {
|
|
288
351
|
if (boundary) {
|
|
@@ -311,14 +374,8 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
311
374
|
teardown(error);
|
|
312
375
|
return;
|
|
313
376
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and
|
|
317
|
-
// the chain already routes a write failure to `controller.error` before settling `flightDone`.
|
|
318
|
-
void writeFlight(controller)
|
|
319
|
-
.catch((error) => controller.error(error))
|
|
320
|
-
.then(flightDone);
|
|
321
|
-
}
|
|
377
|
+
// Deliberately not awaited: this runs inside a scheduled callback with nothing to return to.
|
|
378
|
+
startFlight(controller);
|
|
322
379
|
});
|
|
323
380
|
},
|
|
324
381
|
async flush(controller) {
|
|
@@ -333,15 +390,7 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
333
390
|
// all — leaves nothing to have started the payload, and `flightDone` is only ever called from that
|
|
334
391
|
// chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the
|
|
335
392
|
// response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.
|
|
336
|
-
|
|
337
|
-
startedFlight = true;
|
|
338
|
-
if (cancelled)
|
|
339
|
-
flightDone();
|
|
340
|
-
else
|
|
341
|
-
void writeFlight(controller)
|
|
342
|
-
.catch((error) => controller.error(error))
|
|
343
|
-
.then(flightDone);
|
|
344
|
-
}
|
|
393
|
+
startFlight(controller);
|
|
345
394
|
}
|
|
346
395
|
catch (error) {
|
|
347
396
|
// The consumer went away while the batch was being emitted, so there is nothing left to write to and
|
|
@@ -398,6 +447,7 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
398
447
|
return innerReader.cancel(reason);
|
|
399
448
|
},
|
|
400
449
|
});
|
|
401
|
-
|
|
450
|
+
const pair = { readable, writable: inner.writable };
|
|
451
|
+
return capture ? { ...pair, capturedFlight: capture.promise } : pair;
|
|
402
452
|
}
|
|
403
453
|
//# sourceMappingURL=flight-inject.js.map
|
|
@@ -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 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\n/**\n * How many of `buffer`'s last bytes could still turn into the document trailer — the length of the longest\n * suffix of `buffer[0..length)` that is a prefix of it, `TRAILER_BYTES.length` for the whole thing.\n *\n * At most 14 bytes are ever held back, and in practice 0: a React flush ends with a closed tag, not with the\n * start of one. Cheaper than it looks, too — the first comparison rejects every suffix whose first byte is\n * not `<`.\n */\nfunction trailerPrefixLength(buffer: Uint8Array, length: number): number {\n candidate: for (let take = Math.min(length, TRAILER_BYTES.length); take > 0; take--) {\n const from = length - take;\n for (let i = 0; i < take; i++) if (buffer[from + i] !== TRAILER_BYTES[i]) continue candidate;\n return take;\n }\n return 0;\n}\n\n/**\n * The characters a CSP nonce may be made of: base64 and base64url, which is what every generator of one emits\n * — Hono's `secureHeaders()`, the only source the framework reads, produces base64 of 16 random bytes.\n *\n * The tag below is built by hand rather than by React, so this is the one attribute value in a rendered\n * document that nothing else escapes. The value is not attacker-controlled today, but the framework does not\n * own where it comes from: `c.get('secureHeadersNonce')` is an ordinary context variable that any middleware\n * can set, and a nonce carrying a `\"` would close the attribute and open a script-injection point in the\n * document. Anything outside this set is dropped rather than escaped, because a value made of other characters\n * is not a nonce at all: a page whose payload scripts the policy then refuses is the visible failure to have,\n * where an escaped garbage nonce would be a silent one.\n */\nconst NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): ReadableWritablePair<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;\n const scriptOpen = `<script${safeNonce ? ` nonce=\"${safeNonce}\"` : ''}>(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 * The tail of the last batch that could still be the start of the document trailer, carried into the next\n * one. See {@link emitBatch}.\n */\n let carry: Uint8Array | 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 * One permit per chunk the consumer has asked for: the backpressure a `TransformStream` alone does not give\n * this module.\n *\n * A transform's writable side parks a write while its readable's queue is over the mark, which covers the\n * HTML — but only once something has been enqueued, and `transform` defers that to a macrotask. So a\n * producer that writes a whole document without ever yielding the microtask queue is admitted in full, and\n * {@link writeFlight}, which pumps the payload into the same controller from a detached promise, is never\n * gated at all: measured, a consumer that read one chunk and stalled still pulled all 500 chunks of a test\n * payload into the queue, where this brings it down to 3. `controller.desiredSize` is no help — a transform\n * readable's high-water mark is 0, so it is never positive.\n *\n * The wrapper readable this function returns is the missing signal: its `pull` runs exactly when the\n * consumer wants another chunk, and releases one permit. Both producers — the HTML batcher and the payload\n * pump — take one before they enqueue, so a stalled client parks React instead of filling the process. The\n * Streams standard calls `pull` again only once the previous call has settled, and a call only settles once\n * a chunk has arrived, so permits are handed out one at a time and the queue is bounded at a chunk.\n *\n * What is left buffered is one React flush, because a flush has to leave here as a single chunk (see\n * {@link emitBatch}) — the same bound React itself holds while it builds one. The two enqueues in `flush`\n * are ungated for a related reason: the response is over by then, and parking its last two chunks on a\n * permit would only add a way for it not to end. They are also why the count can reach two at the very\n * end — an ungated enqueue settles a pull without spending its permit — which is harmless, there being\n * nothing left to produce.\n *\n * The cost is one extra stream hop and a microtask per chunk: 3.6ms → 4.0ms to push a 187 kB page through\n * this module with nothing else in the way, which is not the shape of a real response, where the socket\n * dominates by orders of magnitude.\n */\n let permits = 0;\n const waiting: Array<() => void> = [];\n\n /** Returns nothing when a permit was already free, so the common case costs no microtask. */\n function takePermit(): Promise<void> | undefined {\n // Nothing will release another permit once the consumer is gone, and there is nothing left to protect:\n // the caller goes on to a failing enqueue, which is what tears the pipeline down.\n if (cancelled) return;\n if (permits > 0) {\n permits--;\n return;\n }\n return new Promise<void>((resolve) => waiting.push(resolve));\n }\n\n function releasePermit(): void {\n const next = waiting.shift();\n if (next) next();\n else permits++;\n }\n\n /** Fires `onDone` at most once, however the response ended — both cancel paths can reach it. */\n let ended = false;\n function reportDone(): void {\n if (ended) return;\n ended = true;\n onDone?.();\n }\n\n /**\n * The consumer has gone away: stop producing, release everything the request was holding, and unpark both\n * producers so they see {@link cancelled} rather than a permit that will never come.\n *\n * Called from the wrapper readable's `cancel`, which is reliable, and from the transformer's, which the\n * standard skips once the close algorithm has started — so it has to be idempotent.\n */\n function teardown(reason?: unknown): void {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n carry = null;\n for (const resolve of waiting.splice(0)) resolve();\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 reportDone();\n }\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for `flush` to re-emit\n * 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 found.\n * A trailer split across two *batches* is not a shape React produces — it writes its final flush in one\n * synchronous run — but this injector exists because `rsc-html-stream` made a narrower version of that same\n * assumption and was wrong (see the module header), so anything that could still become a trailer is held\n * back in {@link carry} rather than assumed not to be. Only `final`, the call from `flush`, may emit such a\n * tail: by then there is no next batch for it to complete.\n *\n * A tail that never does complete therefore leaves after the payload scripts rather than before them, which\n * is the deliberate half of the trade: releasing it the moment a script wants to go out is the very bug this\n * guards, because the next batch may be the rest of the trailer. It costs at most 13 bytes of a truncated\n * document arriving late, still inside `<body>`.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>, final = false): void {\n boundary = null;\n let total = carry?.byteLength ?? 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 if (carry) {\n joined.set(carry, at);\n at += carry.byteLength;\n carry = null;\n }\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const held = final ? (endsWithTrailer(joined, total) ? TRAILER_BYTES.length : 0) : trailerPrefixLength(joined, total);\n const end = total - held;\n // Copied rather than a `subarray`, which would keep the whole joined flush alive for 14 bytes.\n if (held > 0 && !final) carry = joined.slice(end);\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 // One chunk at a time, deliberately *not* `{ stream: true }`: anything that is not valid UTF-8 on its\n // own — a binary row's bytes, or a chunk that split a multi-byte character — throws, and the catch below\n // re-encodes that whole chunk byte-exactly.\n //\n // Streaming is what makes that fallback wrong. A chunk ending mid-character leaves its lead bytes inside\n // the decoder to be emitted with the next chunk; when the next chunk is a binary row and throws, those\n // bytes are in neither `decode()`'s return nor `value`, and the client reassembles a payload short by\n // them. It also left the end-of-stream flush able to throw from outside any `try`.\n //\n // `ignoreBOM` because a per-chunk decode re-runs the BOM check on every call: without it any chunk whose\n // first three bytes are EF BB BF would silently lose them.\n const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });\n const push = async (literal: string): Promise<void> => {\n const permit = takePermit();\n if (permit) await permit;\n if (cancelled) return;\n controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n };\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)));\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 await push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n // No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left\n // to emit — and the flush this replaces could throw, erroring the response mid-document. See above.\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n async transform(chunk, controller) {\n if (boundary) {\n batch.push(chunk);\n return;\n }\n // The permit is taken once per *batch*, not once per chunk: React writes a whole flush in one microtask\n // cascade and the flush has to leave here as one chunk (see `emitBatch`), so a per-chunk gate would\n // hand the consumer one chunk per read instead. `transform` is never re-entered concurrently — the\n // writable side serializes it on the promise this returns — so nothing else can claim the batch in\n // between.\n const permit = takePermit();\n if (permit) await permit;\n batch.push(chunk);\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 // A dead consumer, all but always. `teardown` rather than `flightDone` alone: a payload pump parked\n // on a permit has to be unparked too, and the RSC branch it holds released.\n controller.error(error);\n teardown(error);\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 // Both of these have to happen *before* the await, and in this order.\n try {\n // Anything still batched — or carried — belongs ahead of the payload scripts, which sit at the end of\n // `<body>`. Called unconditionally: a `carry` outlives its boundary, and an empty batch returns early.\n if (boundary) unschedule(boundary);\n emitBatch(controller, true);\n // A writable side that closed without ever reaching `transform` — an HTML stream with no chunks at\n // all — leaves nothing to have started the payload, and `flightDone` is only ever called from that\n // chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the\n // response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.\n if (!startedFlight) {\n startedFlight = true;\n if (cancelled) flightDone();\n else\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n } catch (error) {\n // The consumer went away while the batch was being emitted, so there is nothing left to write to and\n // nothing to wait for. Settled explicitly rather than left dangling, so the payload chain is released.\n teardown(error);\n return;\n }\n\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 (!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 reportDone();\n }\n },\n cancel(reason) {\n teardown(reason);\n },\n };\n\n const inner = new TransformStream<Uint8Array, Uint8Array>(transformer);\n const innerReader = inner.readable.getReader();\n /**\n * A pull-driven wrapper around the transform's readable, and the only reason this function does not simply\n * return the transform: `pull` runs once per chunk the consumer takes, which is the demand signal the two\n * producers park on (see {@link takePermit}). It is also the one cancel notification the standard never\n * skips, which is what makes {@link teardown} reliable.\n */\n const readable = new ReadableStream<Uint8Array>({\n async pull(controller) {\n releasePermit();\n const { done, value } = await innerReader.read();\n try {\n if (done) controller.close();\n else controller.enqueue(value);\n } catch {\n // Cancelled while this pull was in flight. `teardown` has already run; there is nobody to hand it to.\n }\n },\n cancel(reason) {\n teardown(reason);\n return innerReader.cancel(reason);\n },\n });\n return { readable, writable: inner.writable };\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\n/**\n * How many of `buffer`'s last bytes could still turn into the document trailer — the length of the longest\n * suffix of `buffer[0..length)` that is a prefix of it, `TRAILER_BYTES.length` for the whole thing.\n *\n * At most 14 bytes are ever held back, and in practice 0: a React flush ends with a closed tag, not with the\n * start of one. Cheaper than it looks, too — the first comparison rejects every suffix whose first byte is\n * not `<`.\n */\nfunction trailerPrefixLength(buffer: Uint8Array, length: number): number {\n candidate: for (let take = Math.min(length, TRAILER_BYTES.length); take > 0; take--) {\n const from = length - take;\n for (let i = 0; i < take; i++) if (buffer[from + i] !== TRAILER_BYTES[i]) continue candidate;\n return take;\n }\n return 0;\n}\n\n/**\n * The characters a CSP nonce may be made of: base64 and base64url, which is what every generator of one emits\n * — Hono's `secureHeaders()`, the only source the framework reads, produces base64 of 16 random bytes.\n *\n * The tag below is built by hand rather than by React, so this is the one attribute value in a rendered\n * document that nothing else escapes. The value is not attacker-controlled today, but the framework does not\n * own where it comes from: `c.get('secureHeadersNonce')` is an ordinary context variable that any middleware\n * can set, and a nonce carrying a `\"` would close the attribute and open a script-injection point in the\n * document. Anything outside this set is dropped rather than escaped, because a value made of other characters\n * is not a nonce at all: a page whose payload scripts the policy then refuses is the visible failure to have,\n * where an escaped garbage nonce would be a silent one.\n */\nconst NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;\n\nfunction createFlightCapture() {\n const { promise, resolve, reject } = Promise.withResolvers<Uint8Array>();\n // A render can be abandoned before the SSG pass receives this promise. Observing it here leaves the\n // original promise rejected for a later await without risking an unhandled rejection in that gap.\n void promise.catch(() => {});\n let chunks: Uint8Array[] | undefined = [];\n let byteLength = 0;\n\n const fail = (reason?: unknown): void => {\n if (!chunks) return;\n chunks = undefined;\n reject(reason ?? new Error('Flight capture was cancelled'));\n };\n\n return {\n promise,\n push(chunk: Uint8Array) {\n if (!chunks) return;\n chunks.push(chunk);\n byteLength += chunk.byteLength;\n },\n complete() {\n const captured = chunks;\n if (!captured) return;\n chunks = undefined;\n try {\n const bytes = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of captured) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n resolve(bytes);\n } catch (error) {\n reject(error);\n }\n },\n fail,\n };\n}\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void; captureFlight?: boolean } = {},\n): ReadableWritablePair<Uint8Array, Uint8Array> & { capturedFlight?: Promise<Uint8Array> } {\n const { nonce, onDone } = options;\n const capture = options.captureFlight ? createFlightCapture() : undefined;\n const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;\n const scriptOpen = `<script${safeNonce ? ` nonce=\"${safeNonce}\"` : ''}>(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 * The tail of the last batch that could still be the start of the document trailer, carried into the next\n * one. See {@link emitBatch}.\n */\n let carry: Uint8Array | 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 * One permit per chunk the consumer has asked for: the backpressure a `TransformStream` alone does not give\n * this module.\n *\n * A transform's writable side parks a write while its readable's queue is over the mark, which covers the\n * HTML — but only once something has been enqueued, and `transform` defers that to a macrotask. So a\n * producer that writes a whole document without ever yielding the microtask queue is admitted in full, and\n * {@link writeFlight}, which pumps the payload into the same controller from a detached promise, is never\n * gated at all: measured, a consumer that read one chunk and stalled still pulled all 500 chunks of a test\n * payload into the queue, where this brings it down to 3. `controller.desiredSize` is no help — a transform\n * readable's high-water mark is 0, so it is never positive.\n *\n * The wrapper readable this function returns is the missing signal: its `pull` runs exactly when the\n * consumer wants another chunk, and releases one permit. Both producers — the HTML batcher and the payload\n * pump — take one before they enqueue, so a stalled client parks React instead of filling the process. The\n * Streams standard calls `pull` again only once the previous call has settled, and a call only settles once\n * a chunk has arrived, so permits are handed out one at a time and the queue is bounded at a chunk.\n *\n * What is left buffered is one React flush, because a flush has to leave here as a single chunk (see\n * {@link emitBatch}) — the same bound React itself holds while it builds one. The two enqueues in `flush`\n * are ungated for a related reason: the response is over by then, and parking its last two chunks on a\n * permit would only add a way for it not to end. They are also why the count can reach two at the very\n * end — an ungated enqueue settles a pull without spending its permit — which is harmless, there being\n * nothing left to produce.\n *\n * The cost is one extra stream hop and a microtask per chunk: 3.6ms → 4.0ms to push a 187 kB page through\n * this module with nothing else in the way, which is not the shape of a real response, where the socket\n * dominates by orders of magnitude.\n */\n let permits = 0;\n const waiting: Array<() => void> = [];\n\n /** Returns nothing when a permit was already free, so the common case costs no microtask. */\n function takePermit(): Promise<void> | undefined {\n // Nothing will release another permit once the consumer is gone, and there is nothing left to protect:\n // the caller goes on to a failing enqueue, which is what tears the pipeline down.\n if (cancelled) return;\n if (permits > 0) {\n permits--;\n return;\n }\n return new Promise<void>((resolve) => waiting.push(resolve));\n }\n\n function releasePermit(): void {\n const next = waiting.shift();\n if (next) next();\n else permits++;\n }\n\n /** Fires `onDone` at most once, however the response ended — both cancel paths can reach it. */\n let ended = false;\n function reportDone(): void {\n if (ended) return;\n ended = true;\n onDone?.();\n }\n\n /**\n * The consumer has gone away: stop producing, release everything the request was holding, and unpark both\n * producers so they see {@link cancelled} rather than a permit that will never come.\n *\n * Called from the wrapper readable's `cancel`, which is reliable, and from the transformer's, which the\n * standard skips once the close algorithm has started — so it has to be idempotent.\n */\n function teardown(reason?: unknown): void {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n carry = null;\n for (const resolve of waiting.splice(0)) resolve();\n capture?.fail(reason);\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 reportDone();\n }\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for `flush` to re-emit\n * 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 found.\n * A trailer split across two *batches* is not a shape React produces — it writes its final flush in one\n * synchronous run — but this injector exists because `rsc-html-stream` made a narrower version of that same\n * assumption and was wrong (see the module header), so anything that could still become a trailer is held\n * back in {@link carry} rather than assumed not to be. Only `final`, the call from `flush`, may emit such a\n * tail: by then there is no next batch for it to complete.\n *\n * A tail that never does complete therefore leaves after the payload scripts rather than before them, which\n * is the deliberate half of the trade: releasing it the moment a script wants to go out is the very bug this\n * guards, because the next batch may be the rest of the trailer. It costs at most 13 bytes of a truncated\n * document arriving late, still inside `<body>`.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>, final = false): void {\n boundary = null;\n let total = carry?.byteLength ?? 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 if (carry) {\n joined.set(carry, at);\n at += carry.byteLength;\n carry = null;\n }\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const held = final ? (endsWithTrailer(joined, total) ? TRAILER_BYTES.length : 0) : trailerPrefixLength(joined, total);\n const end = total - held;\n // Copied rather than a `subarray`, which would keep the whole joined flush alive for 14 bytes.\n if (held > 0 && !final) carry = joined.slice(end);\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 // One chunk at a time, deliberately *not* `{ stream: true }`: anything that is not valid UTF-8 on its\n // own — a binary row's bytes, or a chunk that split a multi-byte character — throws, and the catch below\n // re-encodes that whole chunk byte-exactly.\n //\n // Streaming is what makes that fallback wrong. A chunk ending mid-character leaves its lead bytes inside\n // the decoder to be emitted with the next chunk; when the next chunk is a binary row and throws, those\n // bytes are in neither `decode()`'s return nor `value`, and the client reassembles a payload short by\n // them. It also left the end-of-stream flush able to throw from outside any `try`.\n //\n // `ignoreBOM` because a per-chunk decode re-runs the BOM check on every call: without it any chunk whose\n // first three bytes are EF BB BF would silently lose them.\n const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });\n const push = async (literal: string): Promise<void> => {\n const permit = takePermit();\n if (permit) await permit;\n if (cancelled) return;\n controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n };\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) {\n capture?.complete();\n break;\n }\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)));\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 await push(literal);\n capture?.push(value);\n } catch (error) {\n cancelled = true;\n capture?.fail(error);\n reader.cancel().catch(() => {});\n return;\n }\n }\n // No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left\n // to emit — and the flush this replaces could throw, erroring the response mid-document. See above.\n }\n\n function startFlight(controller: TransformStreamDefaultController<Uint8Array>): void {\n if (startedFlight) return;\n startedFlight = true;\n if (cancelled) {\n flightDone();\n return;\n }\n void writeFlight(controller)\n .catch((error) => {\n capture?.fail(error);\n controller.error(error);\n })\n .then(flightDone);\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n async transform(chunk, controller) {\n if (boundary) {\n batch.push(chunk);\n return;\n }\n // The permit is taken once per *batch*, not once per chunk: React writes a whole flush in one microtask\n // cascade and the flush has to leave here as one chunk (see `emitBatch`), so a per-chunk gate would\n // hand the consumer one chunk per read instead. `transform` is never re-entered concurrently — the\n // writable side serializes it on the promise this returns — so nothing else can claim the batch in\n // between.\n const permit = takePermit();\n if (permit) await permit;\n batch.push(chunk);\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 // A dead consumer, all but always. `teardown` rather than `flightDone` alone: a payload pump parked\n // on a permit has to be unparked too, and the RSC branch it holds released.\n controller.error(error);\n teardown(error);\n return;\n }\n // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to.\n startFlight(controller);\n });\n },\n async flush(controller) {\n // Both of these have to happen *before* the await, and in this order.\n try {\n // Anything still batched — or carried — belongs ahead of the payload scripts, which sit at the end of\n // `<body>`. Called unconditionally: a `carry` outlives its boundary, and an empty batch returns early.\n if (boundary) unschedule(boundary);\n emitBatch(controller, true);\n // A writable side that closed without ever reaching `transform` — an HTML stream with no chunks at\n // all — leaves nothing to have started the payload, and `flightDone` is only ever called from that\n // chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the\n // response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.\n startFlight(controller);\n } catch (error) {\n // The consumer went away while the batch was being emitted, so there is nothing left to write to and\n // nothing to wait for. Settled explicitly rather than left dangling, so the payload chain is released.\n teardown(error);\n return;\n }\n\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 (!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 reportDone();\n }\n },\n cancel(reason) {\n teardown(reason);\n },\n };\n\n const inner = new TransformStream<Uint8Array, Uint8Array>(transformer);\n const innerReader = inner.readable.getReader();\n /**\n * A pull-driven wrapper around the transform's readable, and the only reason this function does not simply\n * return the transform: `pull` runs once per chunk the consumer takes, which is the demand signal the two\n * producers park on (see {@link takePermit}). It is also the one cancel notification the standard never\n * skips, which is what makes {@link teardown} reliable.\n */\n const readable = new ReadableStream<Uint8Array>({\n async pull(controller) {\n releasePermit();\n const { done, value } = await innerReader.read();\n try {\n if (done) controller.close();\n else controller.enqueue(value);\n } catch {\n // Cancelled while this pull was in flight. `teardown` has already run; there is nobody to hand it to.\n }\n },\n cancel(reason) {\n teardown(reason);\n return innerReader.cancel(reason);\n },\n });\n const pair = { readable, writable: inner.writable };\n return capture ? { ...pair, capturedFlight: capture.promise } : pair;\n}\n"]}
|
|
@@ -39,6 +39,12 @@ export interface NavigationState {
|
|
|
39
39
|
/**
|
|
40
40
|
* The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else
|
|
41
41
|
* — it is not written back to the address bar.
|
|
42
|
+
*
|
|
43
|
+
* The path, query and origin travel in the page payload, but the fragment cannot: a browser leaves `#…`
|
|
44
|
+
* out of the request line, so the server renders every page without one. `url.hash` is therefore read
|
|
45
|
+
* from the browser after hydration and follows `hashchange` — an in-page link, Back/Forward between
|
|
46
|
+
* anchors of one document, or opening the document at one. Nothing else about the URL is affected; see
|
|
47
|
+
* {@link useNavigation} for the `render: 'static'` case.
|
|
42
48
|
*/
|
|
43
49
|
url: URL;
|
|
44
50
|
/** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */
|
|
@@ -71,11 +77,16 @@ export declare function RouterProvider({ href, params, children }: {
|
|
|
71
77
|
* during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative
|
|
72
78
|
* actions plus a `pending` flag, `true` while a soft navigation is in flight.
|
|
73
79
|
*
|
|
80
|
+
* The fragment is the exception the payload cannot cover: a browser never sends `#…` to the server, so
|
|
81
|
+
* `url.hash` is read from the address bar after hydration and kept in sync on `hashchange` — an in-page
|
|
82
|
+
* link, Back/Forward between anchors, or opening the document at `#section` — with no request either way.
|
|
83
|
+
* Everything before the `#` remains what the payload carried.
|
|
84
|
+
*
|
|
74
85
|
* **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The
|
|
75
86
|
* payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own
|
|
76
|
-
* `PageProps.url` — the same value, not a live one. A page whose output depends on the
|
|
77
|
-
* `render: 'dynamic'`; a component that only needs it after hydration can read
|
|
78
|
-
* effect.
|
|
87
|
+
* `PageProps.url` — the same value, not a live one, `url.hash` aside. A page whose output depends on the
|
|
88
|
+
* query wants `render: 'dynamic'`; a component that only needs it after hydration can read
|
|
89
|
+
* `location.search` in an effect.
|
|
79
90
|
*
|
|
80
91
|
* Hooks can't run in a server component; read the same data there from `getRequestContext()`.
|
|
81
92
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useMemo } from 'react';
|
|
3
|
+
import { createContext, useContext, useMemo, useSyncExternalStore } from 'react';
|
|
4
4
|
const noop = () => { };
|
|
5
5
|
const defaultRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };
|
|
6
6
|
/**
|
|
@@ -10,6 +10,27 @@ const defaultRouter = { push: noop, replace: noop, back: noop, forward: noop, re
|
|
|
10
10
|
*/
|
|
11
11
|
export const RouterContext = createContext(defaultRouter);
|
|
12
12
|
const NavigationContext = createContext(null);
|
|
13
|
+
/**
|
|
14
|
+
* The browser's fragment navigation: the one part of the address a payload can never carry, and the one part
|
|
15
|
+
* that can move without a page data request. `hashchange` is every way it moves within a document — an
|
|
16
|
+
* in-page link, Back/Forward between anchors of one page, opening the document at `#section` covered by the
|
|
17
|
+
* post-hydration check `useSyncExternalStore` makes for a changed snapshot. A cross-page `#anchor` commits a
|
|
18
|
+
* payload instead; the re-render that follows reads the fragment then.
|
|
19
|
+
*/
|
|
20
|
+
function subscribeToHash(onStoreChange) {
|
|
21
|
+
window.addEventListener('hashchange', onStoreChange);
|
|
22
|
+
return () => window.removeEventListener('hashchange', onStoreChange);
|
|
23
|
+
}
|
|
24
|
+
/** The live fragment, `#…` included, or `''`. */
|
|
25
|
+
const readHash = () => window.location.hash;
|
|
26
|
+
/**
|
|
27
|
+
* What the fragment reads as while the server snapshot is in use — server render and hydration. The server
|
|
28
|
+
* never saw one, so the payload's URL has none; answering with the live fragment here would render markup
|
|
29
|
+
* the server did not and fail hydration. The empty string is exactly what the payload carries, and the
|
|
30
|
+
* post-hydration re-render that `useSyncExternalStore` performs for a changed snapshot is where the
|
|
31
|
+
* browser's own arrives.
|
|
32
|
+
*/
|
|
33
|
+
const readServerHash = () => '';
|
|
13
34
|
/**
|
|
14
35
|
* Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps
|
|
15
36
|
* every page in one.
|
|
@@ -18,7 +39,14 @@ const NavigationContext = createContext(null);
|
|
|
18
39
|
*/
|
|
19
40
|
export function RouterProvider({ href, params, children }) {
|
|
20
41
|
const router = useContext(RouterContext);
|
|
21
|
-
|
|
42
|
+
// `href` is the payload's URL — see `subscribeToHash` — so the browser's fragment is applied on top of it.
|
|
43
|
+
// This is the only client-side part of `url`; path, query and origin stay exactly what the payload said.
|
|
44
|
+
const hash = useSyncExternalStore(subscribeToHash, readHash, readServerHash);
|
|
45
|
+
const value = useMemo(() => {
|
|
46
|
+
const url = new URL(href);
|
|
47
|
+
url.hash = hash;
|
|
48
|
+
return { url, params, router };
|
|
49
|
+
}, [href, hash, params, router]);
|
|
22
50
|
return _jsx(NavigationContext.Provider, { value: value, children: children });
|
|
23
51
|
}
|
|
24
52
|
/**
|
|
@@ -29,11 +57,16 @@ export function RouterProvider({ href, params, children }) {
|
|
|
29
57
|
* during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative
|
|
30
58
|
* actions plus a `pending` flag, `true` while a soft navigation is in flight.
|
|
31
59
|
*
|
|
60
|
+
* The fragment is the exception the payload cannot cover: a browser never sends `#…` to the server, so
|
|
61
|
+
* `url.hash` is read from the address bar after hydration and kept in sync on `hashchange` — an in-page
|
|
62
|
+
* link, Back/Forward between anchors, or opening the document at `#section` — with no request either way.
|
|
63
|
+
* Everything before the `#` remains what the payload carried.
|
|
64
|
+
*
|
|
32
65
|
* **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The
|
|
33
66
|
* payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own
|
|
34
|
-
* `PageProps.url` — the same value, not a live one. A page whose output depends on the
|
|
35
|
-
* `render: 'dynamic'`; a component that only needs it after hydration can read
|
|
36
|
-
* effect.
|
|
67
|
+
* `PageProps.url` — the same value, not a live one, `url.hash` aside. A page whose output depends on the
|
|
68
|
+
* query wants `render: 'dynamic'`; a component that only needs it after hydration can read
|
|
69
|
+
* `location.search` in an effect.
|
|
37
70
|
*
|
|
38
71
|
* Hooks can't run in a server component; read the same data there from `getRequestContext()`.
|
|
39
72
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAE,oBAAoB,EAAkB,MAAM,OAAO,CAAC;AAyDjG,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAEtB,MAAM,aAAa,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAEhI;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAmB,aAAa,CAAC,CAAC;AAE5E,MAAM,iBAAiB,GAAG,aAAa,CAAyB,IAAI,CAAC,CAAC;AAEtE;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,aAAyB;IAChD,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;IACrD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AACvE,CAAC;AAED,iDAAiD;AACjD,MAAM,QAAQ,GAAG,GAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,cAAc,GAAG,GAAW,EAAE,CAAC,EAAE,CAAC;AAExC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAyE;IAC9H,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,2GAA2G;IAC3G,yGAAyG;IACzG,MAAM,IAAI,GAAG,oBAAoB,CAAC,eAAe,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,OAAO,CAAkB,GAAG,EAAE;QAC1C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAChB,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAEjC,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mKAAmK,CACpK,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, useSyncExternalStore, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * Every action is a **soft** navigation: the page's flight payload is fetched and applied in place, so\n * client component state outside the changed subtree survives. Off-site hrefs — and a traversal that leaves\n * the app — fall back to a full load.\n *\n * Soft navigation is the browser's\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is\n * missing, every action below is still correct and simply performs a real browser load.\n *\n * @example\n * ```tsx\n * const { router } = useNavigation();\n * router.push('/dashboard'); // navigate, new history entry\n * router.replace('/login'); // navigate, no new entry\n * router.back(); // one entry back, as the browser's button does\n * router.forward(); // one entry forward\n * router.refresh(); // re-run this route's server components\n * ```\n */\nexport interface NavigationRouter {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Steps one entry back in the browser's session history. Nothing to go back to is a no-op. */\n back(): void;\n /** Steps one entry forward in the browser's session history. A no-op on the newest entry. */\n forward(): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */\nexport interface NavigationState {\n /**\n * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else\n * — it is not written back to the address bar.\n *\n * The path, query and origin travel in the page payload, but the fragment cannot: a browser leaves `#…`\n * out of the request line, so the server renders every page without one. `url.hash` is therefore read\n * from the browser after hydration and follows `hashchange` — an in-page link, Back/Forward between\n * anchors of one document, or opening the document at one. Nothing else about the URL is affected; see\n * {@link useNavigation} for the `render: 'static'` case.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: NavigationRouter;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: NavigationRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.\n *\n * @internal\n */\nexport const RouterContext = createContext<NavigationRouter>(defaultRouter);\n\nconst NavigationContext = createContext<NavigationState | null>(null);\n\n/**\n * The browser's fragment navigation: the one part of the address a payload can never carry, and the one part\n * that can move without a page data request. `hashchange` is every way it moves within a document — an\n * in-page link, Back/Forward between anchors of one page, opening the document at `#section` covered by the\n * post-hydration check `useSyncExternalStore` makes for a changed snapshot. A cross-page `#anchor` commits a\n * payload instead; the re-render that follows reads the fragment then.\n */\nfunction subscribeToHash(onStoreChange: () => void): () => void {\n window.addEventListener('hashchange', onStoreChange);\n return () => window.removeEventListener('hashchange', onStoreChange);\n}\n\n/** The live fragment, `#…` included, or `''`. */\nconst readHash = (): string => window.location.hash;\n\n/**\n * What the fragment reads as while the server snapshot is in use — server render and hydration. The server\n * never saw one, so the payload's URL has none; answering with the live fragment here would render markup\n * the server did not and fail hydration. The empty string is exactly what the payload carries, and the\n * post-hydration re-render that `useSyncExternalStore` performs for a changed snapshot is where the\n * browser's own arrives.\n */\nconst readServerHash = (): string => '';\n\n/**\n * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps\n * every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n // `href` is the payload's URL — see `subscribeToHash` — so the browser's fragment is applied on top of it.\n // This is the only client-side part of `url`; path, query and origin stay exactly what the payload said.\n const hash = useSyncExternalStore(subscribeToHash, readHash, readServerHash);\n const value = useMemo<NavigationState>(() => {\n const url = new URL(href);\n url.hash = hash;\n return { url, params, router };\n }, [href, hash, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a\n * `'use client'` component.\n *\n * `url` and `params` are computed on the server and travel in the flight payload, so they are correct\n * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative\n * actions plus a `pending` flag, `true` while a soft navigation is in flight.\n *\n * The fragment is the exception the payload cannot cover: a browser never sends `#…` to the server, so\n * `url.hash` is read from the address bar after hydration and kept in sync on `hashchange` — an in-page\n * link, Back/Forward between anchors, or opening the document at `#section` — with no request either way.\n * Everything before the `#` remains what the payload carried.\n *\n * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The\n * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own\n * `PageProps.url` — the same value, not a live one, `url.hash` aside. A page whose output depends on the\n * query wants `render: 'dynamic'`; a component that only needs it after hydration can read\n * `location.search` in an effect.\n *\n * Hooks can't run in a server component; read the same data there from `getRequestContext()`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link NavigationState}: `url` and `params`, plus `router`\n * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}\n */\nexport function useNavigation(): NavigationState {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.\",\n );\n }\n return value;\n}\n"]}
|
|
@@ -15,30 +15,26 @@ export type PrerenderVariant = 'html' | 'flight';
|
|
|
15
15
|
export declare const VARIANTS: {
|
|
16
16
|
readonly html: {
|
|
17
17
|
readonly file: 'index.html';
|
|
18
|
-
readonly headers: {};
|
|
19
|
-
readonly contentType: 'text/html';
|
|
20
18
|
};
|
|
21
19
|
readonly flight: {
|
|
22
20
|
readonly file: 'index.rsc';
|
|
23
|
-
readonly headers: {
|
|
24
|
-
readonly RSC: '1';
|
|
25
|
-
};
|
|
26
|
-
readonly contentType: 'text/x-component';
|
|
27
21
|
};
|
|
28
22
|
};
|
|
29
23
|
/**
|
|
30
24
|
* How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.
|
|
31
25
|
*
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* about
|
|
26
|
+
* The pass cannot see the request it made: it renders through the app's own middleware inside the server
|
|
27
|
+
* bundle, in a module graph of its own, and keeps the body alone. What it needs to know is whether a
|
|
28
|
+
* *request* for this path will mint a nonce, because a document that will is one the framework re-renders per
|
|
29
|
+
* request — so the file the pass is about to write is one no deployment will read, and saying "prerendered"
|
|
30
|
+
* about it is a lie by omission.
|
|
37
31
|
*
|
|
38
32
|
* Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never
|
|
39
33
|
* reaches a deployed response; and never stored, since only the body is.
|
|
40
34
|
*/
|
|
41
35
|
export declare const PRERENDER_NONCE_HEADER = "x-rshono-prerender-nonce";
|
|
36
|
+
export declare function setPrerenderFlight(response: Response, flight: Promise<Uint8Array>): void;
|
|
37
|
+
export declare function getPrerenderFlight(response: Response): Promise<Uint8Array> | undefined;
|
|
42
38
|
/**
|
|
43
39
|
* The index the build leaves beside the pages, naming every file it wrote — one `files` array of
|
|
44
40
|
* {@link ssgFilePath} names.
|
|
@@ -7,24 +7,38 @@
|
|
|
7
7
|
* into a bundle that has neither.
|
|
8
8
|
*/
|
|
9
9
|
export const VARIANTS = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
html: { file: 'index.html', headers: {}, contentType: 'text/html' },
|
|
13
|
-
flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },
|
|
10
|
+
html: { file: 'index.html' },
|
|
11
|
+
flight: { file: 'index.rsc' },
|
|
14
12
|
};
|
|
15
13
|
/**
|
|
16
14
|
* How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.
|
|
17
15
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* about
|
|
16
|
+
* The pass cannot see the request it made: it renders through the app's own middleware inside the server
|
|
17
|
+
* bundle, in a module graph of its own, and keeps the body alone. What it needs to know is whether a
|
|
18
|
+
* *request* for this path will mint a nonce, because a document that will is one the framework re-renders per
|
|
19
|
+
* request — so the file the pass is about to write is one no deployment will read, and saying "prerendered"
|
|
20
|
+
* about it is a lie by omission.
|
|
23
21
|
*
|
|
24
22
|
* Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never
|
|
25
23
|
* reaches a deployed response; and never stored, since only the body is.
|
|
26
24
|
*/
|
|
27
25
|
export const PRERENDER_NONCE_HEADER = 'x-rshono-prerender-nonce';
|
|
26
|
+
/**
|
|
27
|
+
* Process-local bridge from the server bundle's build-time HTML render to the SSG writer.
|
|
28
|
+
*
|
|
29
|
+
* The CLI and the app bundle are separate module graphs, so a module-local symbol would not be shared between
|
|
30
|
+
* them. `Symbol.for` keeps this internal contract available to both graphs without putting the Flight bytes in
|
|
31
|
+
* an HTTP header or changing the public response body. It is attached to the response stream because Hono may
|
|
32
|
+
* copy the `Response` object while it finalizes a handler result.
|
|
33
|
+
*/
|
|
34
|
+
const PRERENDER_FLIGHT = Symbol.for('rshono.prerender.flight-stream');
|
|
35
|
+
export function setPrerenderFlight(response, flight) {
|
|
36
|
+
if (response.body)
|
|
37
|
+
Object.defineProperty(response.body, PRERENDER_FLIGHT, { value: flight });
|
|
38
|
+
}
|
|
39
|
+
export function getPrerenderFlight(response) {
|
|
40
|
+
return response.body?.[PRERENDER_FLIGHT];
|
|
41
|
+
}
|
|
28
42
|
/**
|
|
29
43
|
* The index the build leaves beside the pages, naming every file it wrote — one `files` array of
|
|
30
44
|
* {@link ssgFilePath} names.
|