| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 |
|
| 11 | import { escapeAttribute } from "../../dom/ssr/markup.js";
|
| 12 |
|
| 13 | const encoder = new TextEncoder();
|
| 14 | const trailer = "</body></html>";
|
| 15 | function injectRSCPayload(rscStream, { nonce } = {}) {
|
| 16 | let decoder = new TextDecoder();
|
| 17 | let resolveFlightDataPromise;
|
| 18 | let flightDataPromise = new Promise((resolve) => resolveFlightDataPromise = resolve);
|
| 19 | let startedRSC = false;
|
| 20 | let cancelled = false;
|
| 21 | let rscReader = null;
|
| 22 | let buffered = [];
|
| 23 | let timeout = null;
|
| 24 | function flushBufferedChunks(controller) {
|
| 25 | for (let chunk of buffered) {
|
| 26 | let buf = decoder.decode(chunk, { stream: true });
|
| 27 | if (buf.endsWith(trailer)) buf = buf.slice(0, -14);
|
| 28 | controller.enqueue(encoder.encode(buf));
|
| 29 | }
|
| 30 | buffered.length = 0;
|
| 31 | timeout = null;
|
| 32 | }
|
| 33 | return new TransformStream({
|
| 34 | transform(chunk, controller) {
|
| 35 | buffered.push(chunk);
|
| 36 | if (timeout) return;
|
| 37 | timeout = setTimeout(async () => {
|
| 38 | if (cancelled) return;
|
| 39 | flushBufferedChunks(controller);
|
| 40 | if (!startedRSC) {
|
| 41 | startedRSC = true;
|
| 42 | rscReader = rscStream.getReader();
|
| 43 | writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => controller.error(err)).then(resolveFlightDataPromise);
|
| 44 | }
|
| 45 | }, 0);
|
| 46 | },
|
| 47 | async flush(controller) {
|
| 48 | await flightDataPromise;
|
| 49 | if (timeout) {
|
| 50 | clearTimeout(timeout);
|
| 51 | flushBufferedChunks(controller);
|
| 52 | }
|
| 53 | controller.enqueue(encoder.encode("</body></html>"));
|
| 54 | },
|
| 55 | async cancel(reason) {
|
| 56 | cancelled = true;
|
| 57 | if (timeout) {
|
| 58 | clearTimeout(timeout);
|
| 59 | timeout = null;
|
| 60 | }
|
| 61 | buffered.length = 0;
|
| 62 | if (rscReader) await rscReader.cancel(reason).catch(() => {});
|
| 63 | else await rscStream.cancel(reason).catch(() => {});
|
| 64 | resolveFlightDataPromise();
|
| 65 | }
|
| 66 | });
|
| 67 | }
|
| 68 | async function writeRSCStream(reader, controller, isCancelled, nonce) {
|
| 69 | let decoder = new TextDecoder("utf-8", { fatal: true });
|
| 70 | try {
|
| 71 | let read;
|
| 72 | while ((read = await reader.read()) && !read.done) {
|
| 73 | if (isCancelled()) return;
|
| 74 | const chunk = read.value;
|
| 75 | try {
|
| 76 | writeChunk(JSON.stringify(decoder.decode(chunk, { stream: true })), controller, nonce);
|
| 77 | } catch {
|
| 78 | writeChunk(`Uint8Array.from(atob(${JSON.stringify(btoa(String.fromCodePoint(...chunk)))}), m => m.codePointAt(0))`, controller, nonce);
|
| 79 | }
|
| 80 | }
|
| 81 | } finally {
|
| 82 | reader.releaseLock();
|
| 83 | }
|
| 84 | let remaining = decoder.decode();
|
| 85 | if (remaining.length && !isCancelled()) writeChunk(JSON.stringify(remaining), controller, nonce);
|
| 86 | }
|
| 87 | function writeChunk(chunk, controller, nonce) {
|
| 88 | let nonceAttr = nonce == null ? "" : ` nonce="${escapeAttribute(nonce)}"`;
|
| 89 | controller.enqueue(encoder.encode(`<script${nonceAttr}>${escapeScript(`(self.__FLIGHT_DATA||=[]).push(${chunk})`)}<\/script>`));
|
| 90 | }
|
| 91 | function escapeScript(script) {
|
| 92 | return script.replace(/<!--/g, "<\\!--").replace(/<\/(script)/gi, "</\\$1");
|
| 93 | }
|
| 94 |
|
| 95 | export { injectRSCPayload };
|