akanjs 2.2.11 → 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,13 +3,527 @@ 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
+
8
+ const DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES = 1024;
9
+ const DEFAULT_MAX_PENDING_INLINE_RSC_SCRIPTS = 32;
10
+
11
+ interface SsrChunkRegistryEntry<T> {
12
+ keys: Set<string>;
13
+ lruKey: string;
14
+ value: T;
15
+ }
16
+
17
+ export class SsrChunkRegistry<T> {
18
+ readonly #entriesByKey = new Map<string, SsrChunkRegistryEntry<T>>();
19
+ readonly #lru = new Map<string, SsrChunkRegistryEntry<T>>();
20
+ #evictionCount = 0;
21
+
22
+ constructor(readonly maxEntries = DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES) {}
23
+
24
+ get size(): number {
25
+ return this.#entriesByKey.size;
26
+ }
27
+
28
+ get evictionCount(): number {
29
+ return this.#evictionCount;
30
+ }
31
+
32
+ get(key: string): T | undefined {
33
+ const entry = this.#entriesByKey.get(key);
34
+ if (!entry) return undefined;
35
+ this.#touch(entry);
36
+ return entry.value;
37
+ }
38
+
39
+ set(keys: string[], value: T): void {
40
+ const uniqueKeys = [...new Set(keys)].filter(Boolean);
41
+ if (uniqueKeys.length === 0) return;
42
+
43
+ let entry = uniqueKeys
44
+ .map((key) => this.#entriesByKey.get(key))
45
+ .find((item): item is SsrChunkRegistryEntry<T> => Boolean(item));
46
+ if (!entry) {
47
+ entry = { keys: new Set(), lruKey: uniqueKeys[0] as string, value };
48
+ }
49
+ entry.value = value;
50
+
51
+ for (const key of uniqueKeys) {
52
+ const existing = this.#entriesByKey.get(key);
53
+ if (existing && existing !== entry) {
54
+ existing.keys.delete(key);
55
+ if (existing.keys.size === 0) this.#lru.delete(existing.lruKey);
56
+ }
57
+ entry.keys.add(key);
58
+ this.#entriesByKey.set(key, entry);
59
+ }
60
+
61
+ this.#touch(entry);
62
+ this.#evict(entry);
63
+ }
64
+
65
+ #touch(entry: SsrChunkRegistryEntry<T>): void {
66
+ this.#lru.delete(entry.lruKey);
67
+ this.#lru.set(entry.lruKey, entry);
68
+ }
69
+
70
+ #evict(protectedEntry: SsrChunkRegistryEntry<T>): void {
71
+ const maxEntries = this.maxEntries > 0 ? this.maxEntries : DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES;
72
+ while (this.#entriesByKey.size > maxEntries) {
73
+ const oldest = this.#lru.entries().next().value as [string, SsrChunkRegistryEntry<T>] | undefined;
74
+ if (!oldest) return;
75
+ const [lruKey, entry] = oldest;
76
+ if (entry === protectedEntry && this.#lru.size === 1) return;
77
+ if (entry === protectedEntry) {
78
+ this.#touch(entry);
79
+ continue;
80
+ }
81
+ this.#lru.delete(lruKey);
82
+ for (const key of entry.keys) this.#entriesByKey.delete(key);
83
+ this.#evictionCount += 1;
84
+ }
85
+ }
86
+ }
87
+
88
+ export type InlineRscChunk = readonly [1, string] | readonly [3, string];
89
+
90
+ export function encodeInlineRscChunk(chunk: Uint8Array): InlineRscChunk {
91
+ try {
92
+ return [1, new TextDecoder("utf-8", { fatal: true }).decode(chunk)];
93
+ } catch {
94
+ return [3, Buffer.from(chunk).toString("base64")];
95
+ }
96
+ }
97
+
98
+ export function htmlEscapeJsonString(value: string): string {
99
+ return JSON.stringify(value)
100
+ .replace(/</g, "\\u003c")
101
+ .replace(/>/g, "\\u003e")
102
+ .replace(/&/g, "\\u0026")
103
+ .replace(/\u2028/g, "\\u2028")
104
+ .replace(/\u2029/g, "\\u2029");
105
+ }
106
+
107
+ export function createInlineRscScript(chunk: Uint8Array): string {
108
+ const [type, data] = encodeInlineRscChunk(chunk);
109
+ return `<script>self.__RSC_PUSH__(${type},${htmlEscapeJsonString(data)})</script>`;
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
+ }
7
520
 
8
521
  export class SsrFromRscRenderer {
9
522
  static readonly #chunkRegistryStats: SsrChunkRegistryStats = {
10
523
  ssrChunkRegistrySize: 0,
11
524
  ssrChunkLoadCount: 0,
12
525
  ssrChunkCacheHitCount: 0,
526
+ ssrChunkEvictionCount: 0,
13
527
  };
14
528
 
15
529
  static readonly #clientBootstrap = `(function(){
@@ -31,7 +545,7 @@ export class SsrFromRscRenderer {
31
545
  self.__webpack_get_script_filename__ = function(chunkId) { return chunkId; };
32
546
  self.__RSC_CHUNKS__ = [];
33
547
  self.__RSC_CLOSED__ = false;
34
- self.__RSC_PUSH__ = function(b64){ self.__RSC_CHUNKS__.push(b64); };
548
+ self.__RSC_PUSH__ = function(type,data){ self.__RSC_CHUNKS__.push([type,data]); };
35
549
  self.__RSC_CLOSE__ = function(){ self.__RSC_CLOSED__ = true; };
36
550
  })();`;
37
551
 
@@ -56,8 +570,12 @@ export class SsrFromRscRenderer {
56
570
 
57
571
  const [rscForSsr, rscForClient] = input.rscStream.tee();
58
572
 
59
- const ssrNodeStream = Readable.fromWeb(rscForSsr as never);
60
- 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
+ );
61
579
 
62
580
  function Root(): ReactNode {
63
581
  return use(thenable);
@@ -86,6 +604,9 @@ export class SsrFromRscRenderer {
86
604
  SsrFromRscRenderer.#sanitizeFlightForClient(rscForClient),
87
605
  input.bootstrapModules,
88
606
  input.request,
607
+ input.lateControl,
608
+ () => stderrSuppressor?.stop(),
609
+ input.onCancel,
89
610
  );
90
611
  }
91
612
 
@@ -98,18 +619,18 @@ export class SsrFromRscRenderer {
98
619
  if (g.__rsc_ssr_shims_installed__) return;
99
620
  g.__rsc_ssr_shims_installed__ = true;
100
621
 
101
- const registry = new Map<string, Record<string, unknown>>();
622
+ const registry = new SsrChunkRegistry<Record<string, unknown>>(SsrFromRscRenderer.#getSsrChunkRegistryMaxEntries());
102
623
  g.__webpack_chunk_load__ = async (chunkId: string) => {
103
- if (registry.has(chunkId)) {
624
+ if (registry.get(chunkId)) {
104
625
  SsrFromRscRenderer.#chunkRegistryStats.ssrChunkCacheHitCount += 1;
105
626
  return;
106
627
  }
107
628
  const mod = (await import(chunkId)) as Record<string, unknown>;
108
- registry.set(chunkId, mod);
109
629
  const canonical = chunkId.replace(/\?v=\d+$/, "");
110
- registry.set(canonical, mod);
630
+ registry.set([chunkId, canonical], mod);
111
631
  SsrFromRscRenderer.#chunkRegistryStats.ssrChunkLoadCount += 1;
112
632
  SsrFromRscRenderer.#chunkRegistryStats.ssrChunkRegistrySize = registry.size;
633
+ SsrFromRscRenderer.#chunkRegistryStats.ssrChunkEvictionCount = registry.evictionCount;
113
634
  };
114
635
  g.__webpack_require__ = (id: string) => {
115
636
  const mod = registry.get(id);
@@ -120,6 +641,29 @@ export class SsrFromRscRenderer {
120
641
  };
121
642
  }
122
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
+
662
+ static #getSsrChunkRegistryMaxEntries(): number {
663
+ const parsed = Number.parseInt(process.env.AKAN_SSR_CHUNK_REGISTRY_MAX_ENTRIES ?? "", 10);
664
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES;
665
+ }
666
+
123
667
  /**
124
668
  * Splice bootstrap-only head scripts immediately after the `<head>` opening
125
669
  * tag in the outgoing HTML stream.
@@ -219,27 +763,7 @@ export class SsrFromRscRenderer {
219
763
  }
220
764
 
221
765
  static #sanitizeFlightForClient(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
222
- const decoder = new TextDecoder();
223
- const encoder = new TextEncoder();
224
-
225
- const hlStylesheetRe = /(:HL\["[^"\\]*(?:\\.[^"\\]*)*",)"stylesheet"(\])/g;
226
-
227
- return stream.pipeThrough(
228
- new TransformStream<Uint8Array, Uint8Array>({
229
- transform(chunk, controller) {
230
- const text = decoder.decode(chunk, { stream: true });
231
- if (!text.includes(`"stylesheet"`)) {
232
- controller.enqueue(chunk);
233
- return;
234
- }
235
- controller.enqueue(encoder.encode(text.replace(hlStylesheetRe, `$1"style"$2`)));
236
- },
237
- flush(controller) {
238
- const tail = decoder.decode();
239
- if (tail) controller.enqueue(encoder.encode(tail));
240
- },
241
- }),
242
- );
766
+ return sanitizeFlightForClientStream(stream);
243
767
  }
244
768
 
245
769
  static #appendRscScriptsAfterHtml(
@@ -247,61 +771,18 @@ export class SsrFromRscRenderer {
247
771
  rscClientStream: ReadableStream<Uint8Array>,
248
772
  bootstrapModules?: string[],
249
773
  request?: Request,
774
+ lateControl?: Promise<SsrLateRedirect | null>,
775
+ onComplete?: () => void,
776
+ onCancel?: (reason?: unknown) => void,
250
777
  ): ReadableStream<Uint8Array> {
251
- const encoder = new TextEncoder();
252
778
  const bootstrapModuleScripts = SsrFromRscRenderer.#createBootstrapModuleScriptTags(bootstrapModules);
253
779
 
254
- return new ReadableStream<Uint8Array>({
255
- start(controller) {
256
- let errored = false;
257
- const fail = (err: unknown) => {
258
- if (errored) return;
259
- errored = true;
260
- controller.error(err);
261
- };
262
-
263
- const pump = async () => {
264
- const reader = htmlStream.getReader();
265
- try {
266
- while (true) {
267
- const { value, done } = await reader.read();
268
- if (done) break;
269
- if (errored) return;
270
- controller.enqueue(value);
271
- }
272
- } finally {
273
- reader.releaseLock();
274
- }
275
-
276
- if (bootstrapModuleScripts && !errored) controller.enqueue(encoder.encode(bootstrapModuleScripts));
277
-
278
- const rscReader = rscClientStream.getReader();
279
- try {
280
- while (true) {
281
- const { value, done } = await rscReader.read();
282
- if (done) break;
283
- if (errored) return;
284
- const b64 = Buffer.from(value).toString("base64");
285
- controller.enqueue(encoder.encode(`<script>self.__RSC_PUSH__(${JSON.stringify(b64)})</script>`));
286
- }
287
- } finally {
288
- rscReader.releaseLock();
289
- }
290
- if (!errored) {
291
- controller.enqueue(encoder.encode(`<script>self.__RSC_CLOSE__()</script>`));
292
- controller.close();
293
- }
294
- };
295
-
296
- const runPump = () => {
297
- const cleanup = request ? pushRequestFallback(request) : undefined;
298
- return pump()
299
- .catch(fail)
300
- .finally(() => cleanup?.());
301
- };
302
- if (request && requestStorage) void requestStorage.run(request, runPump);
303
- else void runPump();
304
- },
780
+ return interleaveRscScriptsWithHtml(htmlStream, rscClientStream, {
781
+ bootstrapModuleScripts,
782
+ lateControl,
783
+ onComplete,
784
+ onCancel,
785
+ request,
305
786
  });
306
787
  }
307
788
  }