akanjs 2.2.12 → 2.2.13-rc.0

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.
@@ -3,9 +3,10 @@ import { type AkanTheme, pushRequestFallback, requestStorage } from "akanjs/fetc
3
3
  import { type ReactNode, use } from "react";
4
4
  import { renderToReadableStream } from "react-dom/server.browser";
5
5
  import { createFromNodeStream } from "react-server-dom-webpack/client.node";
6
- import type { SsrChunkRegistryStats, SsrFromRscInput } from "./ssrTypes";
6
+ import type { SsrChunkRegistryStats, SsrFromRscInput, SsrLateRedirect } from "./ssrTypes";
7
7
 
8
8
  const DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES = 1024;
9
+ const DEFAULT_MAX_PENDING_INLINE_RSC_SCRIPTS = 32;
9
10
 
10
11
  interface SsrChunkRegistryEntry<T> {
11
12
  keys: Set<string>;
@@ -108,6 +109,415 @@ export function createInlineRscScript(chunk: Uint8Array): string {
108
109
  return `<script>self.__RSC_PUSH__(${type},${htmlEscapeJsonString(data)})</script>`;
109
110
  }
110
111
 
112
+ export function createSoftRedirectScript(redirect: SsrLateRedirect): string {
113
+ const method = redirect.method === "push" ? "assign" : "replace";
114
+ return `<script>window.location.${method}(${htmlEscapeJsonString(redirect.location)})</script>`;
115
+ }
116
+
117
+ function sanitizeFlightRows(
118
+ stream: ReadableStream<Uint8Array>,
119
+ options: { rewriteStylesheetHints?: boolean } = {},
120
+ ): ReadableStream<Uint8Array> {
121
+ const decoder = new TextDecoder("utf-8", { fatal: true });
122
+ const encoder = new TextEncoder();
123
+ const hlStylesheetRe = /(:HL\["[^"\\]*(?:\\.[^"\\]*)*",)"stylesheet"(\])/g;
124
+ const redirectErrorRowRe = /^([0-9a-z]+):E(\{[^\n]*"digest":"AKAN_REDIRECT"[^\n]*\})(\n?)$/;
125
+ let buffered: Uint8Array<ArrayBuffer> = new Uint8Array(0);
126
+
127
+ const concatBytes = (left: Uint8Array, right: Uint8Array): Uint8Array<ArrayBuffer> => {
128
+ const combined = new Uint8Array(left.byteLength + right.byteLength);
129
+ combined.set(left, 0);
130
+ combined.set(right, left.byteLength);
131
+ return combined;
132
+ };
133
+
134
+ const sanitizeRow = (row: Uint8Array): Uint8Array => {
135
+ let text: string;
136
+ try {
137
+ text = decoder.decode(row);
138
+ } catch {
139
+ return row;
140
+ }
141
+ const sanitized = (options.rewriteStylesheetHints ? text.replace(hlStylesheetRe, `$1"style"$2`) : text).replace(
142
+ redirectErrorRowRe,
143
+ "$1:null$3",
144
+ );
145
+ return sanitized === text ? row : encoder.encode(sanitized);
146
+ };
147
+
148
+ const enqueueCompleteRows = (chunk: Uint8Array, controller: TransformStreamDefaultController<Uint8Array>) => {
149
+ buffered = concatBytes(buffered, chunk);
150
+ let rowStart = 0;
151
+ for (let index = 0; index < buffered.byteLength; index += 1) {
152
+ if (buffered[index] !== 10) continue;
153
+ controller.enqueue(sanitizeRow(buffered.slice(rowStart, index + 1)));
154
+ rowStart = index + 1;
155
+ }
156
+ buffered = rowStart === 0 ? buffered : buffered.slice(rowStart);
157
+ };
158
+
159
+ return stream.pipeThrough(
160
+ new TransformStream<Uint8Array, Uint8Array>({
161
+ transform(chunk, controller) {
162
+ enqueueCompleteRows(chunk, controller);
163
+ },
164
+ flush(controller) {
165
+ if (buffered.byteLength > 0) {
166
+ controller.enqueue(sanitizeRow(buffered));
167
+ buffered = new Uint8Array(0);
168
+ }
169
+ },
170
+ }),
171
+ );
172
+ }
173
+
174
+ export function sanitizeFlightForClientStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
175
+ return sanitizeFlightRows(stream, { rewriteStylesheetHints: true });
176
+ }
177
+
178
+ type StderrWrite = typeof process.stderr.write;
179
+
180
+ export class ExpectedLateRedirectStderrSuppressor {
181
+ static #active = new Set<ExpectedLateRedirectStderrSuppressor>();
182
+ static #originalWrite: StderrWrite | null = null;
183
+ static #buffer = "";
184
+ static #flushTimer: ReturnType<typeof setTimeout> | null = null;
185
+ static #suppressingBenignBlock = false;
186
+ static readonly #maxBufferLength = 64 * 1024;
187
+ #stopped = false;
188
+ #lateRedirect = false;
189
+ #lateControlSettled = false;
190
+
191
+ private constructor(lateControl: Promise<SsrLateRedirect | null>) {
192
+ lateControl
193
+ .then((control) => {
194
+ this.#lateRedirect = control?.type === "redirect";
195
+ })
196
+ .catch(() => {
197
+ this.#lateRedirect = false;
198
+ })
199
+ .finally(() => {
200
+ this.#lateControlSettled = true;
201
+ ExpectedLateRedirectStderrSuppressor.#tryResolveBufferedOutput();
202
+ });
203
+ }
204
+
205
+ static start(lateControl?: Promise<SsrLateRedirect | null>): ExpectedLateRedirectStderrSuppressor | null {
206
+ if (!lateControl) return null;
207
+
208
+ if (process.env.NODE_ENV === "production" && process.env.AKAN_SUPPRESS_LATE_REDIRECT_STDERR !== "1") return null;
209
+ const suppressor = new ExpectedLateRedirectStderrSuppressor(lateControl);
210
+ ExpectedLateRedirectStderrSuppressor.#active.add(suppressor);
211
+ ExpectedLateRedirectStderrSuppressor.#install();
212
+ return suppressor;
213
+ }
214
+
215
+ stop(): void {
216
+ if (this.#stopped) return;
217
+ this.#stopped = true;
218
+ ExpectedLateRedirectStderrSuppressor.#active.delete(this);
219
+ ExpectedLateRedirectStderrSuppressor.#tryResolveBufferedOutput();
220
+ if (ExpectedLateRedirectStderrSuppressor.#active.size === 0) ExpectedLateRedirectStderrSuppressor.#uninstall();
221
+ }
222
+
223
+ static #install(): void {
224
+ if (ExpectedLateRedirectStderrSuppressor.#originalWrite) return;
225
+ ExpectedLateRedirectStderrSuppressor.#originalWrite = process.stderr.write;
226
+ process.stderr.write = ((chunk: unknown, ...args: unknown[]) => {
227
+ ExpectedLateRedirectStderrSuppressor.#write(chunk, args);
228
+ return true;
229
+ }) as StderrWrite;
230
+ }
231
+
232
+ static #uninstall(): void {
233
+ ExpectedLateRedirectStderrSuppressor.#flushBufferedOutput();
234
+ if (!ExpectedLateRedirectStderrSuppressor.#originalWrite) return;
235
+ process.stderr.write = ExpectedLateRedirectStderrSuppressor.#originalWrite;
236
+ ExpectedLateRedirectStderrSuppressor.#originalWrite = null;
237
+ }
238
+
239
+ static #write(chunk: unknown, args: unknown[]): void {
240
+ const text =
241
+ typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString() : String(chunk);
242
+ ExpectedLateRedirectStderrSuppressor.#buffer += text;
243
+ const callback = args.find((arg): arg is () => void => typeof arg === "function");
244
+ callback?.();
245
+
246
+ if (ExpectedLateRedirectStderrSuppressor.#buffer.length > ExpectedLateRedirectStderrSuppressor.#maxBufferLength) {
247
+ ExpectedLateRedirectStderrSuppressor.#flushBufferedOutput();
248
+ return;
249
+ }
250
+ ExpectedLateRedirectStderrSuppressor.#tryResolveBufferedOutput();
251
+ }
252
+
253
+ static #tryResolveBufferedOutput(): void {
254
+ if (!ExpectedLateRedirectStderrSuppressor.#buffer) return;
255
+ const hasBenignClose = ExpectedLateRedirectStderrSuppressor.#isBenignRsdwConnectionClose(
256
+ ExpectedLateRedirectStderrSuppressor.#buffer,
257
+ );
258
+ if (hasBenignClose && ExpectedLateRedirectStderrSuppressor.#hasLateRedirectOrPending()) {
259
+ if (!ExpectedLateRedirectStderrSuppressor.#hasLateRedirect()) {
260
+ ExpectedLateRedirectStderrSuppressor.#scheduleFlush();
261
+ return;
262
+ }
263
+ ExpectedLateRedirectStderrSuppressor.#suppressingBenignBlock = true;
264
+ }
265
+
266
+ if (ExpectedLateRedirectStderrSuppressor.#suppressingBenignBlock) {
267
+ if (ExpectedLateRedirectStderrSuppressor.#buffer.includes("\n\n")) {
268
+ ExpectedLateRedirectStderrSuppressor.#clearBufferedOutput();
269
+ ExpectedLateRedirectStderrSuppressor.#suppressingBenignBlock = false;
270
+ }
271
+ return;
272
+ }
273
+
274
+ ExpectedLateRedirectStderrSuppressor.#scheduleFlush();
275
+ }
276
+
277
+ static #scheduleFlush(): void {
278
+ if (ExpectedLateRedirectStderrSuppressor.#flushTimer) return;
279
+ ExpectedLateRedirectStderrSuppressor.#flushTimer = setTimeout(() => {
280
+ ExpectedLateRedirectStderrSuppressor.#flushTimer = null;
281
+ if (
282
+ ExpectedLateRedirectStderrSuppressor.#isBenignRsdwConnectionClose(
283
+ ExpectedLateRedirectStderrSuppressor.#buffer,
284
+ ) &&
285
+ ExpectedLateRedirectStderrSuppressor.#hasLateRedirect()
286
+ ) {
287
+ ExpectedLateRedirectStderrSuppressor.#clearBufferedOutput();
288
+ return;
289
+ }
290
+ ExpectedLateRedirectStderrSuppressor.#flushBufferedOutput();
291
+ }, 25);
292
+ }
293
+
294
+ static #flushBufferedOutput(): void {
295
+ if (!ExpectedLateRedirectStderrSuppressor.#buffer) return;
296
+ const text = ExpectedLateRedirectStderrSuppressor.#buffer;
297
+ ExpectedLateRedirectStderrSuppressor.#clearBufferedOutput();
298
+ ExpectedLateRedirectStderrSuppressor.#originalWrite?.call(process.stderr, text);
299
+ }
300
+
301
+ static #clearBufferedOutput(): void {
302
+ ExpectedLateRedirectStderrSuppressor.#buffer = "";
303
+ if (ExpectedLateRedirectStderrSuppressor.#flushTimer) {
304
+ clearTimeout(ExpectedLateRedirectStderrSuppressor.#flushTimer);
305
+ ExpectedLateRedirectStderrSuppressor.#flushTimer = null;
306
+ }
307
+ }
308
+
309
+ static #isBenignRsdwConnectionClose(text: string): boolean {
310
+ return (
311
+ text.includes("Connection closed.") &&
312
+ (text.includes("react-server-dom-webpack-client.node") || text.includes("reportGlobalError"))
313
+ );
314
+ }
315
+
316
+ static #hasLateRedirect(): boolean {
317
+ return [...ExpectedLateRedirectStderrSuppressor.#active].some((suppressor) => suppressor.#lateRedirect);
318
+ }
319
+
320
+ static #hasLateRedirectOrPending(): boolean {
321
+ return [...ExpectedLateRedirectStderrSuppressor.#active].some(
322
+ (suppressor) => suppressor.#lateRedirect || !suppressor.#lateControlSettled,
323
+ );
324
+ }
325
+ }
326
+
327
+ export function interleaveRscScriptsWithHtml(
328
+ htmlStream: ReadableStream<Uint8Array>,
329
+ rscClientStream: ReadableStream<Uint8Array>,
330
+ options: {
331
+ bootstrapModuleScripts?: string;
332
+ lateControl?: Promise<SsrLateRedirect | null>;
333
+ maxPendingRscScripts?: number;
334
+ onPendingRscScriptsSize?: (size: number) => void;
335
+ onComplete?: () => void;
336
+ onCancel?: (reason?: unknown) => void;
337
+ request?: Request;
338
+ } = {},
339
+ ): ReadableStream<Uint8Array> {
340
+ const encoder = new TextEncoder();
341
+ const bootstrapDetector = new InlineBootstrapDetector();
342
+ const pendingRscScripts: Uint8Array[] = [];
343
+ const pendingControlScripts: Uint8Array[] = [];
344
+ const maxPendingRscScripts = SsrFromRscRendererConfig.maxPendingInlineRscScripts(options.maxPendingRscScripts);
345
+ const queueDrainResolvers: Array<() => void> = [];
346
+ const scriptAvailableResolvers: Array<() => void> = [];
347
+ let errored = false;
348
+ let rscDone = false;
349
+ let lateControlDone = !options.lateControl;
350
+ let htmlReader: ReadableStreamDefaultReader<Uint8Array> | null = null;
351
+ let rscReader: ReadableStreamDefaultReader<Uint8Array> | null = null;
352
+ let cancelled = false;
353
+
354
+ const cancelUpstream = (reason?: unknown) => {
355
+ if (cancelled) return;
356
+ cancelled = true;
357
+ while (queueDrainResolvers.length > 0) queueDrainResolvers.shift()?.();
358
+ while (scriptAvailableResolvers.length > 0) scriptAvailableResolvers.shift()?.();
359
+ if (htmlReader) void htmlReader.cancel(reason).catch(() => {});
360
+ if (rscReader) void rscReader.cancel(reason).catch(() => {});
361
+ options.onCancel?.(reason);
362
+ };
363
+
364
+ return new ReadableStream<Uint8Array>({
365
+ start(controller) {
366
+ const fail = (err: unknown) => {
367
+ if (errored) return;
368
+ errored = true;
369
+ cancelUpstream(err);
370
+ controller.error(err);
371
+ };
372
+
373
+ const flushPendingRscScripts = () => {
374
+ if (!bootstrapDetector.canFlushInlineScripts) return;
375
+ while (!errored && pendingControlScripts.length > 0) {
376
+ const script = pendingControlScripts.shift();
377
+ if (script) controller.enqueue(script);
378
+ }
379
+ while (!errored && pendingRscScripts.length > 0) {
380
+ const script = pendingRscScripts.shift();
381
+ if (script) controller.enqueue(script);
382
+ }
383
+ options.onPendingRscScriptsSize?.(pendingRscScripts.length);
384
+ while (queueDrainResolvers.length > 0) queueDrainResolvers.shift()?.();
385
+ };
386
+
387
+ const notifyScriptAvailable = () => {
388
+ while (scriptAvailableResolvers.length > 0) scriptAvailableResolvers.shift()?.();
389
+ };
390
+
391
+ const waitForRscQueueDrain = async () => {
392
+ while (!errored && pendingRscScripts.length >= maxPendingRscScripts) {
393
+ await new Promise<void>((resolve) => queueDrainResolvers.push(resolve));
394
+ }
395
+ };
396
+
397
+ const waitForScriptAvailable = () => new Promise<void>((resolve) => scriptAvailableResolvers.push(resolve));
398
+
399
+ const pumpRscScripts = async () => {
400
+ rscReader = rscClientStream.getReader();
401
+ try {
402
+ while (true) {
403
+ const { value, done } = await rscReader.read();
404
+ if (done || errored) break;
405
+ await waitForRscQueueDrain();
406
+ if (errored) break;
407
+ pendingRscScripts.push(encoder.encode(createInlineRscScript(value)));
408
+ options.onPendingRscScriptsSize?.(pendingRscScripts.length);
409
+ notifyScriptAvailable();
410
+ }
411
+ } finally {
412
+ rscDone = true;
413
+ notifyScriptAvailable();
414
+ rscReader.releaseLock();
415
+ rscReader = null;
416
+ }
417
+ };
418
+
419
+ const pump = async () => {
420
+ const rscPump = pumpRscScripts();
421
+ const lateControlPump = options.lateControl?.then((control) => {
422
+ try {
423
+ if (!control || errored) return;
424
+ pendingControlScripts.push(encoder.encode(createSoftRedirectScript(control)));
425
+ notifyScriptAvailable();
426
+ } finally {
427
+ lateControlDone = true;
428
+ notifyScriptAvailable();
429
+ }
430
+ });
431
+ if (!lateControlPump) lateControlDone = true;
432
+ void rscPump.catch(fail);
433
+ void lateControlPump?.catch(fail);
434
+
435
+ htmlReader = htmlStream.getReader();
436
+ try {
437
+ while (true) {
438
+ const { value, done } = await htmlReader.read();
439
+ if (done || errored) break;
440
+ controller.enqueue(value);
441
+ bootstrapDetector.observe(value);
442
+ flushPendingRscScripts();
443
+ }
444
+ } finally {
445
+ htmlReader.releaseLock();
446
+ htmlReader = null;
447
+ }
448
+
449
+ if (errored) return;
450
+ if (options.bootstrapModuleScripts) controller.enqueue(encoder.encode(options.bootstrapModuleScripts));
451
+ bootstrapDetector.forceAllow();
452
+ while (
453
+ !errored &&
454
+ (!rscDone || !lateControlDone || pendingControlScripts.length > 0 || pendingRscScripts.length > 0)
455
+ ) {
456
+ flushPendingRscScripts();
457
+ if (!rscDone || !lateControlDone) await waitForScriptAvailable();
458
+ }
459
+ await Promise.all([rscPump, lateControlPump]);
460
+ if (errored) return;
461
+ flushPendingRscScripts();
462
+ controller.enqueue(encoder.encode(`<script>self.__RSC_CLOSE__()</script>`));
463
+ controller.close();
464
+ };
465
+
466
+ const runPump = () => {
467
+ const cleanup = options.request ? pushRequestFallback(options.request) : undefined;
468
+ return pump()
469
+ .catch(fail)
470
+ .finally(() => {
471
+ cleanup?.();
472
+ options.onComplete?.();
473
+ });
474
+ };
475
+ if (options.request && requestStorage) void requestStorage.run(options.request, runPump);
476
+ else void runPump();
477
+ },
478
+ cancel(reason) {
479
+ errored = true;
480
+ cancelUpstream(reason);
481
+ options.onComplete?.();
482
+ },
483
+ });
484
+ }
485
+
486
+ class SsrFromRscRendererConfig {
487
+ static maxPendingInlineRscScripts(explicit?: number): number {
488
+ if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
489
+ const parsed = Number.parseInt(process.env.AKAN_MAX_PENDING_INLINE_RSC_SCRIPTS ?? "", 10);
490
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_PENDING_INLINE_RSC_SCRIPTS;
491
+ }
492
+ }
493
+
494
+ class InlineBootstrapDetector {
495
+ readonly #decoder = new TextDecoder();
496
+ #buffer = "";
497
+ #bootstrapSeen = false;
498
+ #canFlushInlineScripts = false;
499
+
500
+ get canFlushInlineScripts(): boolean {
501
+ return this.#canFlushInlineScripts;
502
+ }
503
+
504
+ observe(chunk: Uint8Array): void {
505
+ if (this.#canFlushInlineScripts) return;
506
+ this.#buffer = `${this.#buffer}${this.#decoder.decode(chunk, { stream: true })}`.slice(-8192);
507
+ if (!this.#bootstrapSeen) {
508
+ const bootstrapIndex = this.#buffer.indexOf("__RSC_PUSH__");
509
+ if (bootstrapIndex === -1) return;
510
+ this.#bootstrapSeen = true;
511
+ this.#buffer = this.#buffer.slice(bootstrapIndex);
512
+ }
513
+ if (this.#buffer.toLowerCase().includes("</script>")) this.#canFlushInlineScripts = true;
514
+ }
515
+
516
+ forceAllow(): void {
517
+ this.#canFlushInlineScripts = true;
518
+ }
519
+ }
520
+
111
521
  export class SsrFromRscRenderer {
112
522
  static readonly #chunkRegistryStats: SsrChunkRegistryStats = {
113
523
  ssrChunkRegistrySize: 0,
@@ -160,8 +570,12 @@ export class SsrFromRscRenderer {
160
570
 
161
571
  const [rscForSsr, rscForClient] = input.rscStream.tee();
162
572
 
163
- const ssrNodeStream = Readable.fromWeb(rscForSsr as never);
164
- const thenable = createFromNodeStream(ssrNodeStream, input.ssrManifest) as Promise<ReactNode>;
573
+ const ssrNodeStream = Readable.fromWeb(sanitizeFlightRows(rscForSsr) as never);
574
+ const stderrSuppressor = ExpectedLateRedirectStderrSuppressor.start(input.lateControl);
575
+ const thenable = SsrFromRscRenderer.#suppressExpectedLateRedirectError(
576
+ createFromNodeStream(ssrNodeStream, input.ssrManifest),
577
+ input.lateControl,
578
+ );
165
579
 
166
580
  function Root(): ReactNode {
167
581
  return use(thenable);
@@ -190,6 +604,9 @@ export class SsrFromRscRenderer {
190
604
  SsrFromRscRenderer.#sanitizeFlightForClient(rscForClient),
191
605
  input.bootstrapModules,
192
606
  input.request,
607
+ input.lateControl,
608
+ () => stderrSuppressor?.stop(),
609
+ input.onCancel,
193
610
  );
194
611
  }
195
612
 
@@ -224,6 +641,24 @@ export class SsrFromRscRenderer {
224
641
  };
225
642
  }
226
643
 
644
+ static #suppressExpectedLateRedirectError(
645
+ thenable: PromiseLike<ReactNode>,
646
+ lateControl?: Promise<SsrLateRedirect | null>,
647
+ ): Promise<ReactNode> {
648
+ const promise = Promise.resolve(thenable);
649
+ if (!lateControl) return promise;
650
+ return promise.catch(async (error) => {
651
+ const control = await lateControl;
652
+ if (control?.type === "redirect" && SsrFromRscRenderer.#isExpectedLateRedirectError(error)) return null;
653
+ throw error;
654
+ });
655
+ }
656
+
657
+ static #isExpectedLateRedirectError(error: unknown): boolean {
658
+ if (!(error instanceof Error)) return false;
659
+ return error.message === "Connection closed." || error.name === "AkanRedirectError";
660
+ }
661
+
227
662
  static #getSsrChunkRegistryMaxEntries(): number {
228
663
  const parsed = Number.parseInt(process.env.AKAN_SSR_CHUNK_REGISTRY_MAX_ENTRIES ?? "", 10);
229
664
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES;
@@ -328,27 +763,7 @@ export class SsrFromRscRenderer {
328
763
  }
329
764
 
330
765
  static #sanitizeFlightForClient(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
331
- const decoder = new TextDecoder();
332
- const encoder = new TextEncoder();
333
-
334
- const hlStylesheetRe = /(:HL\["[^"\\]*(?:\\.[^"\\]*)*",)"stylesheet"(\])/g;
335
-
336
- return stream.pipeThrough(
337
- new TransformStream<Uint8Array, Uint8Array>({
338
- transform(chunk, controller) {
339
- const text = decoder.decode(chunk, { stream: true });
340
- if (!text.includes(`"stylesheet"`)) {
341
- controller.enqueue(chunk);
342
- return;
343
- }
344
- controller.enqueue(encoder.encode(text.replace(hlStylesheetRe, `$1"style"$2`)));
345
- },
346
- flush(controller) {
347
- const tail = decoder.decode();
348
- if (tail) controller.enqueue(encoder.encode(tail));
349
- },
350
- }),
351
- );
766
+ return sanitizeFlightForClientStream(stream);
352
767
  }
353
768
 
354
769
  static #appendRscScriptsAfterHtml(
@@ -356,60 +771,18 @@ export class SsrFromRscRenderer {
356
771
  rscClientStream: ReadableStream<Uint8Array>,
357
772
  bootstrapModules?: string[],
358
773
  request?: Request,
774
+ lateControl?: Promise<SsrLateRedirect | null>,
775
+ onComplete?: () => void,
776
+ onCancel?: (reason?: unknown) => void,
359
777
  ): ReadableStream<Uint8Array> {
360
- const encoder = new TextEncoder();
361
778
  const bootstrapModuleScripts = SsrFromRscRenderer.#createBootstrapModuleScriptTags(bootstrapModules);
362
779
 
363
- return new ReadableStream<Uint8Array>({
364
- start(controller) {
365
- let errored = false;
366
- const fail = (err: unknown) => {
367
- if (errored) return;
368
- errored = true;
369
- controller.error(err);
370
- };
371
-
372
- const pump = async () => {
373
- const reader = htmlStream.getReader();
374
- try {
375
- while (true) {
376
- const { value, done } = await reader.read();
377
- if (done) break;
378
- if (errored) return;
379
- controller.enqueue(value);
380
- }
381
- } finally {
382
- reader.releaseLock();
383
- }
384
-
385
- if (bootstrapModuleScripts && !errored) controller.enqueue(encoder.encode(bootstrapModuleScripts));
386
-
387
- const rscReader = rscClientStream.getReader();
388
- try {
389
- while (true) {
390
- const { value, done } = await rscReader.read();
391
- if (done) break;
392
- if (errored) return;
393
- controller.enqueue(encoder.encode(createInlineRscScript(value)));
394
- }
395
- } finally {
396
- rscReader.releaseLock();
397
- }
398
- if (!errored) {
399
- controller.enqueue(encoder.encode(`<script>self.__RSC_CLOSE__()</script>`));
400
- controller.close();
401
- }
402
- };
403
-
404
- const runPump = () => {
405
- const cleanup = request ? pushRequestFallback(request) : undefined;
406
- return pump()
407
- .catch(fail)
408
- .finally(() => cleanup?.());
409
- };
410
- if (request && requestStorage) void requestStorage.run(request, runPump);
411
- else void runPump();
412
- },
780
+ return interleaveRscScriptsWithHtml(htmlStream, rscClientStream, {
781
+ bootstrapModuleScripts,
782
+ lateControl,
783
+ onComplete,
784
+ onCancel,
785
+ request,
413
786
  });
414
787
  }
415
788
  }
@@ -19,6 +19,13 @@ export interface SsrChunkRegistryStats {
19
19
  ssrChunkEvictionCount: number;
20
20
  }
21
21
 
22
+ export interface SsrLateRedirect {
23
+ type: "redirect";
24
+ location: string;
25
+ method: "replace" | "push";
26
+ status: 303 | 307 | 308;
27
+ }
28
+
22
29
  export interface SsrFromRscInput {
23
30
  request?: Request;
24
31
  rscStream: ReadableStream<Uint8Array>;
@@ -45,4 +52,6 @@ export interface SsrFromRscInput {
45
52
  importmap?: Record<string, string>;
46
53
  theme?: AkanTheme;
47
54
  injectThemeInitScript?: boolean;
55
+ lateControl?: Promise<SsrLateRedirect | null>;
56
+ onCancel?: (reason?: unknown) => void;
48
57
  }