@thomas.f/prnext 0.1.20-alpha → 0.1.21-alpha
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/build/app.mjs +4 -4
- package/build/build-timings.mjs +38 -0
- package/build/bundle-runtime.mjs +8 -8
- package/build/compiler.mjs +2 -1
- package/build/flight-summary.mjs +64 -0
- package/build/index.mjs +32 -28
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +7 -2
- package/build/native-discovery.mjs +18 -6
- package/build/npm-packages.mjs +2 -2
- package/build/source-analysis.mjs +54 -1
- package/build/standalone-copy.mjs +48 -0
- package/build/standalone.mjs +2 -22
- package/build/transform-cache.mjs +17 -3
- package/cli.mjs +13 -0
- package/compat/diagnostics.cjs +5 -4
- package/compat/runtime-sample.cjs +56 -0
- package/compat/use-cache.cjs +5 -2
- package/docs/ai/compatibility.md +3 -3
- package/docs/ai/deployment.md +8 -5
- package/docs/ai/troubleshooting.md +22 -0
- package/package.json +20 -20
- package/runtime/app-client.mjs +106 -38
- package/runtime/app-render.mjs +31 -6
- package/runtime/app-stream-channel.mjs +17 -4
- package/runtime/app-stream-html.mjs +30 -11
- package/runtime/cache-components-rsc.mjs +26 -6
- package/runtime/cache-result.mjs +58 -0
- package/runtime/dev-client.mjs +26 -5
- package/runtime/dev-compiler-broker.mjs +20 -23
- package/runtime/dev-gateway.mjs +22 -6
- package/runtime/development-flight.mjs +59 -0
- package/runtime/development-queue.mjs +33 -0
- package/runtime/development.mjs +11 -9
- package/runtime/flight-diagnostics.mjs +48 -0
- package/runtime/flight-tap.mjs +10 -5
- package/runtime/html-envelope.mjs +34 -0
- package/runtime/loopback-rpc.mjs +92 -0
- package/runtime/native-ppr.mjs +57 -4
- package/runtime/persistent/client.mjs +1 -1
- package/runtime/persistent/common.mjs +7 -18
- package/runtime/persistent/daemon.mjs +1 -1
- package/runtime/persistent/native-proxy.mjs +1 -1
- package/runtime/persistent/watchdog.mjs +12 -27
- package/runtime/prewarm.mjs +35 -0
- package/runtime/rsc-worker.mjs +58 -8
- package/runtime/worker.mjs +29 -14
package/runtime/app-client.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deploymentHeaders, assertDeploymentResponse } from './deployment-client.mjs';
|
|
2
2
|
import React, { startTransition, useLayoutEffect } from 'react';
|
|
3
|
-
import { preload } from 'react-dom';
|
|
3
|
+
import { preload, flushSync } from 'react-dom';
|
|
4
4
|
import { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_CACHE_BYTES } from './app-prefetch-constants.mjs';
|
|
5
5
|
import { isDynamicBailout } from '../compat/dynamic-bailout.cjs';
|
|
6
6
|
import { createRoot, hydrateRoot } from 'react-dom/client';
|
|
@@ -21,6 +21,7 @@ import {recoverNavigation} from './navigation-recovery.mjs';
|
|
|
21
21
|
import { createActivityCache } from '../compat/activity-cache.cjs';
|
|
22
22
|
import { createFlightTransport, TYPE as FLIGHT_DELTA_TYPE } from '../compat/flight-delta.cjs';
|
|
23
23
|
import {decodeBase64} from '../compat/base64.cjs';
|
|
24
|
+
import { supersedableDevelopmentFlight, developmentFlightRoot } from './development-flight.mjs';
|
|
24
25
|
|
|
25
26
|
const PREFETCH_LIMIT = 8;
|
|
26
27
|
const PREFETCH_LIFETIME = 30_000;
|
|
@@ -258,6 +259,10 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
258
259
|
let historyRouting = currentRouting;
|
|
259
260
|
let currentModel = initialModel;
|
|
260
261
|
let committedView = { model: initialModel, routing: currentRouting, url: currentUrl.href, retentionEpoch: 0, authorizationEpoch: 0 };
|
|
262
|
+
// Development navigation can display a newer component generation before its
|
|
263
|
+
// last Flight row arrives. Keep that visible tree separately from the last
|
|
264
|
+
// genuinely complete response; superseding it must not restore older types.
|
|
265
|
+
let developmentView;
|
|
261
266
|
let pendingView, pendingNavigation;
|
|
262
267
|
const historyFocus = new Map();
|
|
263
268
|
let currentKey = window.history.state?.__prnext?.key || 0;
|
|
@@ -290,46 +295,57 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
290
295
|
// A committed loading shell can still contain rows owned by a live
|
|
291
296
|
// fetch. Keep the last fully received view available before aborting it.
|
|
292
297
|
// Authorized PPR previews likewise never become the canonical model.
|
|
293
|
-
if (canonical)
|
|
298
|
+
if (canonical) {
|
|
299
|
+
committedView = { model, routing: model.routing, url, element, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
|
|
300
|
+
developmentView = undefined;
|
|
301
|
+
}
|
|
294
302
|
};
|
|
295
303
|
if (flight) flight.committed = promote;
|
|
296
|
-
|
|
304
|
+
const element = appView({ model, controller, url, globalErrorComponent, globalErrorCss,
|
|
297
305
|
committed: () => {
|
|
298
306
|
if (token !== navigation || pendingView !== view) return;
|
|
299
307
|
view.committed = true;
|
|
308
|
+
if (canonical && flight?.development && kind !== 'action') developmentView = { model, routing: model.routing, url, element, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
|
|
300
309
|
promote();
|
|
301
310
|
committed?.();
|
|
302
311
|
},
|
|
303
|
-
})
|
|
312
|
+
});
|
|
313
|
+
view.element = element;
|
|
314
|
+
startTransition(() => root.render(element));
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
function abandonPendingView() {
|
|
307
318
|
if (!pendingView) return true;
|
|
308
319
|
const flight = navigationStreams.get(pendingView.model);
|
|
309
320
|
const strictAuthorization = controller._navigationCache?.authCookies !== undefined;
|
|
321
|
+
const retained = developmentView || committedView;
|
|
310
322
|
if (flight) flight.committed = undefined;
|
|
311
323
|
if (pendingView.kind === 'action' && committedView.authorizationEpoch === controller._authorizationEpoch &&
|
|
312
324
|
(!strictAuthorization || committedView.model.authorizationKey === currentModel.authorizationKey)) {
|
|
313
325
|
// An action's received result can invoke a returned action before React
|
|
314
326
|
// commits its UI. This response is never owned/aborted by activeRequest;
|
|
315
327
|
// adopt its model instead of rolling back the prior mutation's epoch.
|
|
328
|
+
committedView = { model: currentModel, routing: currentRouting, url: currentUrl.href, element: pendingView.element, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
|
|
316
329
|
pendingView = undefined;
|
|
317
|
-
|
|
330
|
+
developmentView = undefined;
|
|
318
331
|
return true;
|
|
319
332
|
}
|
|
320
333
|
// A new authorized shell or explicit invalidation can make the last full
|
|
321
334
|
// model private to an obsolete session. Reauthorize through a document
|
|
322
335
|
// request instead of briefly restoring that model while C is pending.
|
|
323
|
-
if ((strictAuthorization &&
|
|
324
|
-
|
|
325
|
-
|
|
336
|
+
if ((strictAuthorization && retained.model.authorizationKey !== currentModel.authorizationKey) ||
|
|
337
|
+
retained.retentionEpoch !== controller._retentionEpoch ||
|
|
338
|
+
retained.authorizationEpoch !== controller._authorizationEpoch ||
|
|
339
|
+
(retained.model.rootLayout && currentModel.rootLayout && retained.model.rootLayout !== currentModel.rootLayout)) return false;
|
|
326
340
|
pendingView = undefined;
|
|
327
|
-
currentModel =
|
|
328
|
-
currentRouting =
|
|
341
|
+
currentModel = retained.model;
|
|
342
|
+
currentRouting = retained.routing;
|
|
329
343
|
// React can already be reading a lazy row after createFromFetch resolves
|
|
330
344
|
// the root model. Replace that suspended render before abort rejects its
|
|
331
345
|
// remaining rows; it must not poison the retained layout or page cache.
|
|
332
|
-
|
|
346
|
+
// Reuse the exact submitted element. Before a replacement refresh commits,
|
|
347
|
+
// rebuilding the older Flight lazies after Refresh can itself remount them.
|
|
348
|
+
root.render(retained.element || appView({ model: retained.model, controller, url: retained.url, globalErrorComponent, globalErrorCss }));
|
|
333
349
|
return true;
|
|
334
350
|
}
|
|
335
351
|
|
|
@@ -371,7 +387,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
371
387
|
const actionReloadError = () => new Error('The session changed while this page was loading. Reload before repeating the action.');
|
|
372
388
|
|
|
373
389
|
const flightTransport = createFlightTransport();
|
|
374
|
-
async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false,fullFlight=false}={}) {
|
|
390
|
+
async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false,fullFlight=false,developmentSupersession}={}) {
|
|
375
391
|
if(process.env.NODE_ENV==='development'&&!routing?.refresh&&!skipEnsure)await globalThis.__PRNEXT_DEV__?.ensureRoute?.(url.href);
|
|
376
392
|
const exported = window.__PRNEXT_STATIC_EXPORT__ === true;
|
|
377
393
|
const base = !exported && !fullFlight ? flightTransport.snapshot() : undefined;
|
|
@@ -392,12 +408,17 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
392
408
|
}
|
|
393
409
|
await onPartial?.(response, destination);
|
|
394
410
|
let model;
|
|
395
|
-
const flight = { complete: false };
|
|
396
|
-
try {
|
|
411
|
+
const flight = { complete: false, development: process.env.NODE_ENV === 'development' && !!developmentSupersession };
|
|
412
|
+
try {
|
|
413
|
+
const received = await flightTransport.response(response,base,{onEnd(){flight.complete = true;flight.committed?.();}});
|
|
414
|
+
if (process.env.NODE_ENV === 'development' && developmentSupersession) {
|
|
415
|
+
model = assertModel(await developmentFlightRoot(createFromReadableStream(supersedableDevelopmentFlight(received.body, developmentSupersession), { ...flightOptions, unstable_allowPartialStream: true }), signal));
|
|
416
|
+
} else model = assertModel(await createFromFetch(Promise.resolve(received), flightOptions));
|
|
417
|
+
}
|
|
397
418
|
catch(error) {
|
|
398
419
|
if(!delta||fullFlight||signal?.aborted)throw error;
|
|
399
420
|
flightTransport.clear();
|
|
400
|
-
return requestFlight(url,signal,routing,onPartial,{skipEnsure:true,fullFlight:true});
|
|
421
|
+
return requestFlight(url,signal,routing,onPartial,{skipEnsure:true,fullFlight:true,developmentSupersession});
|
|
401
422
|
}
|
|
402
423
|
navigationStreams.set(model, flight);
|
|
403
424
|
model.router.basePath = basePath;
|
|
@@ -406,7 +427,11 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
406
427
|
return { model, url: destination };
|
|
407
428
|
}
|
|
408
429
|
|
|
409
|
-
|
|
430
|
+
// Only development navigation GETs may retain a DOM-committed partial tree.
|
|
431
|
+
// Direct prefetch, production navigation and Action POST decoders stay strict.
|
|
432
|
+
const navigateFlight=(url,signal,routing,onPartial,options={})=>recoverNavigation(recoverySignal=>requestFlight(url,recoverySignal,routing,onPartial,{
|
|
433
|
+
...options, ...(process.env.NODE_ENV === 'development' ? { developmentSupersession: signal } : {}),
|
|
434
|
+
}),{signal});
|
|
410
435
|
|
|
411
436
|
const requestPartial = (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) ? async function requestPartial(url, signal, full = false) {
|
|
412
437
|
const response = await fetch(url.href, { credentials: 'same-origin', signal,
|
|
@@ -507,10 +532,10 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
507
532
|
|
|
508
533
|
async function waitForDevelopmentUpdate() {
|
|
509
534
|
// A generation updates the Flight loader before React Refresh registers
|
|
510
|
-
// the replacement component families. An already-running
|
|
511
|
-
// in that interval; rendering its model immediately
|
|
512
|
-
// components and
|
|
513
|
-
// never replay
|
|
535
|
+
// the replacement component families. An already-running Flight request
|
|
536
|
+
// may finish in that interval; rendering its model immediately can remount
|
|
537
|
+
// those client components and lose layout state. Wait only on development
|
|
538
|
+
// bookkeeping, never replay a request or cancel a newer user navigation.
|
|
514
539
|
let development = globalThis.__PRNEXT_DEV__;
|
|
515
540
|
while (development?.applying && development.applied) {
|
|
516
541
|
await development.applied;
|
|
@@ -596,14 +621,19 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
596
621
|
return outcome();
|
|
597
622
|
}
|
|
598
623
|
|
|
599
|
-
async function navigate(href, { replace = false, scroll = true, pop = false, savedScroll, savedRouting, refresh = false } = {}) {
|
|
600
|
-
if (
|
|
624
|
+
async function navigate(href, { replace = false, scroll = true, pop = false, savedScroll, savedRouting, refresh = false, developmentSignal } = {}) {
|
|
625
|
+
if (developmentSignal?.aborted) return false;
|
|
626
|
+
if (pageHidden || documentFallback) return developmentSignal ? false : undefined;
|
|
601
627
|
const target = navigationUrl(href);
|
|
602
628
|
if (!isApplicationURL(target, window.location.origin, basePath) || isHtmlDocumentRoute(htmlOnlyRoutes, removeBasePath(target.pathname, basePath))) {
|
|
603
629
|
fallbackDocument(target.href, replace);
|
|
604
|
-
return;
|
|
630
|
+
return developmentSignal ? false : undefined;
|
|
605
631
|
}
|
|
606
632
|
const token = ++navigation;
|
|
633
|
+
// The compiler generation may remain valid when an ordinary navigation or
|
|
634
|
+
// Action replaces only its pending UI. This is distinct from a DOM commit,
|
|
635
|
+
// an owner cancellation, or a genuine failed request.
|
|
636
|
+
const superseded = () => developmentSignal && token !== navigation && !developmentSignal.aborted && !pageHidden && !documentFallback ? 'superseded' : false;
|
|
607
637
|
const intent = { token, url: target.href, replace: replace || pop || refresh };
|
|
608
638
|
pendingNavigation = intent;
|
|
609
639
|
finishCommit?.();
|
|
@@ -615,7 +645,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
615
645
|
activeRequest = undefined;
|
|
616
646
|
if (!restored) {
|
|
617
647
|
fallbackDocument(target.href, replace || pop || refresh);
|
|
618
|
-
return;
|
|
648
|
+
return developmentSignal ? false : undefined;
|
|
619
649
|
}
|
|
620
650
|
|
|
621
651
|
function updateHistory(destination) {
|
|
@@ -639,7 +669,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
639
669
|
updateHistory(target);
|
|
640
670
|
if (scroll) scrollToTarget(target, savedScroll);
|
|
641
671
|
if (pendingNavigation === intent) pendingNavigation = undefined;
|
|
642
|
-
return;
|
|
672
|
+
return developmentSignal ? false : undefined;
|
|
643
673
|
}
|
|
644
674
|
|
|
645
675
|
const cacheKey = `${target.origin}${target.pathname}${target.search}`;
|
|
@@ -669,6 +699,8 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
669
699
|
const abort = new AbortController();
|
|
670
700
|
activeRequest = abort;
|
|
671
701
|
result = await navigateFlight(target, abort.signal, currentRouting, async (response, destination) => {
|
|
702
|
+
if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
|
|
703
|
+
if (token !== navigation || abort.signal.aborted) return;
|
|
672
704
|
// A cached shell (including any cached authorization key) cannot
|
|
673
705
|
// establish this request's live session identity. Strict account
|
|
674
706
|
// isolation waits for the freshly authorized Flight model instead.
|
|
@@ -680,6 +712,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
680
712
|
if (destination.pathname !== preview.url.pathname || destination.search !== preview.url.search) return;
|
|
681
713
|
prepareLayoutCache(preview.model, currentModel);
|
|
682
714
|
await appStyleManager(document).prepare(preview.model.css || [], abort.signal);
|
|
715
|
+
if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
|
|
683
716
|
if (token !== navigation || abort.signal.aborted) return;
|
|
684
717
|
updateHistory(destination); historyUpdated = true;
|
|
685
718
|
appStyleManager(document).handoff(preview.model.css || []);
|
|
@@ -692,19 +725,26 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
692
725
|
// A refresh is already inside the compiler's apply transaction. Calling
|
|
693
726
|
// ensureRoute from here can wait for that same transaction forever when
|
|
694
727
|
// this tree has no routing state (the ordinary single-route case).
|
|
695
|
-
|
|
728
|
+
const signal = developmentSignal ? AbortSignal.any([abort.signal, developmentSignal]) : abort.signal;
|
|
729
|
+
result = await navigateFlight(target, signal, pop && savedRouting ? { ...savedRouting, restore: true } : refresh && currentRouting ? { ...currentRouting, refresh: true } : currentRouting, undefined, {skipEnsure:refresh,developmentSupersession:developmentSignal ? signal : undefined});
|
|
696
730
|
}
|
|
697
|
-
|
|
731
|
+
// Internal refreshes execute inside applyUpdate and must never wait on
|
|
732
|
+
// their own transaction. Ordinary navigations can already be in flight
|
|
733
|
+
// when an on-demand route publishes another client-module generation.
|
|
734
|
+
if (process.env.NODE_ENV === 'development' && !refresh) await waitForDevelopmentUpdate();
|
|
735
|
+
if (token !== navigation || activeRequest?.signal.aborted || developmentSignal?.aborted) return superseded();
|
|
698
736
|
if (!isApplicationURL(result.url, window.location.origin, basePath)) throw new Error('The navigation destination requires a document request');
|
|
699
737
|
if (result.model.rootLayout && currentModel.rootLayout && result.model.rootLayout !== currentModel.rootLayout) throw new Error('A different root layout requires a document request');
|
|
700
738
|
if (requiresAuthorizedDocument(result.model)) {
|
|
701
739
|
activeRequest?.abort();
|
|
702
740
|
fallbackDocument(result.url.href, replace || pop || refresh);
|
|
703
|
-
return;
|
|
741
|
+
return developmentSignal ? false : undefined;
|
|
704
742
|
}
|
|
705
743
|
prepareLayoutCache(result.model, currentModel);
|
|
706
|
-
|
|
707
|
-
|
|
744
|
+
const styleSignal = developmentSignal && activeRequest ? AbortSignal.any([activeRequest.signal, developmentSignal]) : activeRequest?.signal;
|
|
745
|
+
await appStyleManager(document).prepare(result.model.globalError ? [] : result.model.css || [], styleSignal);
|
|
746
|
+
if (process.env.NODE_ENV === 'development' && !refresh) await waitForDevelopmentUpdate();
|
|
747
|
+
if (token !== navigation || activeRequest?.signal.aborted || developmentSignal?.aborted) return superseded();
|
|
708
748
|
currentRouting = result.model.routing;
|
|
709
749
|
currentModel = result.model;
|
|
710
750
|
historyRouting = currentRouting;
|
|
@@ -718,21 +758,32 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
718
758
|
}
|
|
719
759
|
};
|
|
720
760
|
appStyleManager(document).handoff(result.model.globalError ? [] : result.model.css || []);
|
|
761
|
+
let didCommit = false;
|
|
721
762
|
if(process.env.NODE_ENV==='development'&&refresh)await new Promise(resolve=>{
|
|
722
|
-
const finish=()=>{if(finishCommit===finish)finishCommit=undefined;resolve();};
|
|
763
|
+
const finish=()=>{developmentSignal?.removeEventListener('abort', finish);if(finishCommit===finish)finishCommit=undefined;resolve();};
|
|
723
764
|
finishCommit=finish;
|
|
724
|
-
|
|
765
|
+
developmentSignal?.addEventListener('abort', finish, { once: true });
|
|
766
|
+
if (developmentSignal?.aborted) { finish(); return; }
|
|
767
|
+
renderView(result.model, result.url.href, { token, kind: developmentSignal ? 'development-refresh' : 'navigation', committed: () => { didCommit = true; committed(); finish(); } });
|
|
725
768
|
});
|
|
726
769
|
else renderView(result.model, result.url.href, { token, committed });
|
|
770
|
+
if (!developmentSignal) return;
|
|
771
|
+
if (developmentSignal.aborted || pageHidden || documentFallback) return false;
|
|
772
|
+
return didCommit || superseded();
|
|
727
773
|
} catch (error) {
|
|
728
|
-
if (
|
|
774
|
+
if (developmentSignal?.aborted) return false;
|
|
729
775
|
if(error?.code==='PRNEXT_NAVIGATION_NETWORK'){
|
|
776
|
+
// A deadline failure is never evidence that only the UI was replaced.
|
|
777
|
+
if (token !== navigation) return false;
|
|
730
778
|
window.dispatchEvent(new CustomEvent('prnext:navigation-error',{detail:{message:error.message}}));
|
|
731
|
-
return;
|
|
779
|
+
return developmentSignal ? false : undefined;
|
|
732
780
|
}
|
|
781
|
+
if (token !== navigation) return error?.name === 'AbortError' ? superseded() : false;
|
|
782
|
+
if (error?.name === 'AbortError') return false;
|
|
733
783
|
// Pages Router routes, public files, and unavailable Flight endpoints use
|
|
734
784
|
// a normal document navigation, including the browser's redirect handling.
|
|
735
785
|
fallbackDocument(target.href, replace || pop || refresh);
|
|
786
|
+
return developmentSignal ? false : undefined;
|
|
736
787
|
} finally {
|
|
737
788
|
if (pendingNavigation === intent) pendingNavigation = undefined;
|
|
738
789
|
if (finishNavigation === releaseNavigation) finishNavigation = undefined;
|
|
@@ -740,7 +791,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
740
791
|
}
|
|
741
792
|
}
|
|
742
793
|
|
|
743
|
-
function purgeRetainedPages() {
|
|
794
|
+
function purgeRetainedPages({ synchronous = false } = {}) {
|
|
744
795
|
if (pageHidden || documentFallback) return false;
|
|
745
796
|
saveScroll();
|
|
746
797
|
if (!abandonPendingView()) {
|
|
@@ -751,9 +802,14 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
751
802
|
controller._retentionEpoch++;
|
|
752
803
|
historyFocus.clear();
|
|
753
804
|
if (committedView.model === currentModel && committedView.authorizationEpoch === controller._authorizationEpoch) committedView = { ...committedView, retentionEpoch: controller._retentionEpoch };
|
|
805
|
+
if (developmentView?.model === currentModel && developmentView.authorizationEpoch === controller._authorizationEpoch) developmentView = { ...developmentView, retentionEpoch: controller._retentionEpoch };
|
|
754
806
|
// This only updates retention for the already committed model. It must
|
|
755
807
|
// not become a new pending Flight view or a development commit barrier.
|
|
756
|
-
|
|
808
|
+
const element = appView({ model: currentModel, controller, url: (developmentView || committedView).url, globalErrorComponent, globalErrorCss });
|
|
809
|
+
if (committedView.model === currentModel) committedView = { ...committedView, element };
|
|
810
|
+
if (developmentView?.model === currentModel) developmentView = { ...developmentView, element };
|
|
811
|
+
if (synchronous) flushSync(() => root.render(element));
|
|
812
|
+
else startTransition(() => root.render(element));
|
|
757
813
|
return true;
|
|
758
814
|
}
|
|
759
815
|
|
|
@@ -813,6 +869,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
813
869
|
let resolveDevReady;
|
|
814
870
|
const devReady=process.env.NODE_ENV==='development'?new Promise(resolve=>{resolveDevReady=resolve;}):undefined;
|
|
815
871
|
const initialView = appView({ model: initialModel, controller, globalErrorComponent, globalErrorCss, ...(devReady?{committed:resolveDevReady}:{}) });
|
|
872
|
+
committedView.element = initialView;
|
|
816
873
|
const rootOptions = {
|
|
817
874
|
formState: initialModel.formState ?? null,
|
|
818
875
|
onCaughtError(error) {
|
|
@@ -871,7 +928,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
871
928
|
return { root, router: controller, ...(process.env.NODE_ENV === 'development' ? {
|
|
872
929
|
devReady,
|
|
873
930
|
devStyles: routes => developmentStyles(currentModel, routes).css || [],
|
|
874
|
-
devPrepareStyles: routes => appStyleManager(document).prepare(developmentStyles(currentModel, routes).css || []),
|
|
931
|
+
devPrepareStyles: (routes, signal) => appStyleManager(document).prepare(developmentStyles(currentModel, routes).css || [], signal),
|
|
875
932
|
devStylesApplied: (routes, { commit = true } = {}) => {
|
|
876
933
|
for (const model of new Set([currentModel, committedView.model, pendingView?.model].filter(Boolean))) {
|
|
877
934
|
const next = developmentStyles(model, routes);
|
|
@@ -880,7 +937,18 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
|
|
|
880
937
|
if (commit) appStyleManager(document).commit(currentModel.globalError ? [] : currentModel.css || []);
|
|
881
938
|
},
|
|
882
939
|
devPrepare: options => moduleLoader.updateModules(options.clientModules),
|
|
883
|
-
|
|
940
|
+
devBeforeRefresh: ({ navigation: routeOpening } = {}) => {
|
|
941
|
+
if (!routeOpening) { invalidatePrefetches(); return purgeRetainedPages({ synchronous: true }); }
|
|
942
|
+
// A cold route's module publication must not invalidate retained pages.
|
|
943
|
+
// Quiesce the old visible tree under its old families before Refresh,
|
|
944
|
+
// without changing epochs, cancelling the click, or committing its URL.
|
|
945
|
+
saveScroll();
|
|
946
|
+
if (!abandonPendingView()) { activeRequest?.abort(); fallbackDocument(currentUrl.href); return false; }
|
|
947
|
+
const retained = developmentView || committedView;
|
|
948
|
+
flushSync(() => root.render(retained.element));
|
|
949
|
+
return true;
|
|
950
|
+
},
|
|
951
|
+
devCommit: (_options, { signal } = {}) => navigate(new URL(window.location.href), { refresh: true, developmentSignal: signal }),
|
|
884
952
|
} : {}) };
|
|
885
953
|
}
|
|
886
954
|
|
package/runtime/app-render.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { workerMetadata, freezeClientModules, isImmutableClientModules } from '.
|
|
|
15
15
|
import { workerArtifacts } from './worker-artifacts.mjs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_BYTES } from './app-prefetch.mjs';
|
|
18
|
-
import { pathToFileURL } from 'node:url';
|
|
18
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
import { AppRouterProvider } from '../compat/app-context.cjs';
|
|
21
21
|
import { appContent } from './app-content.mjs';
|
|
@@ -243,19 +243,44 @@ function getWorker(production) {
|
|
|
243
243
|
return current;
|
|
244
244
|
}
|
|
245
245
|
export async function prewarmAppRenderer(options){
|
|
246
|
+
if(!Number.isFinite(options?.timeoutMs)||options.timeoutMs<=0||options.timeoutMs>30000||!Number.isFinite(options?.maxRssMb)||options.maxRssMb<=0||options.maxRssMb>2048)throw new TypeError('Invalid renderer prewarm budget');
|
|
247
|
+
const deadline=performance.now()+options.timeoutMs;
|
|
248
|
+
const remaining=()=>{
|
|
249
|
+
options.signal?.throwIfAborted();
|
|
250
|
+
if(performance.now()>=deadline||process.memoryUsage().rss>=options.maxRssMb*1024*1024)throw new Error('Renderer prewarm budget exhausted');
|
|
251
|
+
return Math.max(1,deadline-performance.now());
|
|
252
|
+
};
|
|
253
|
+
remaining();
|
|
246
254
|
flightDecoder(true);
|
|
247
|
-
|
|
255
|
+
// The progressive HTML renderer is otherwise imported by the first document
|
|
256
|
+
// request. This prepares framework code only, never an application route.
|
|
257
|
+
htmlRuntime ??= await import('./app-stream-html.mjs');
|
|
258
|
+
const flight=await preloadAppRoutes([],{maxRssMb:options.maxRssMb,timeoutMs:remaining(),signal:options.signal,frameworkWarmup:true});
|
|
259
|
+
remaining();
|
|
260
|
+
if(!(flight instanceof ArrayBuffer)||flight.byteLength>65536)throw new Error('Invalid renderer prewarm Flight');
|
|
261
|
+
const body=new ReadableStream({start(controller){controller.enqueue(new Uint8Array(flight));controller.close();}});
|
|
262
|
+
let response,htmlBytes=0;
|
|
263
|
+
try{
|
|
264
|
+
response=await htmlRuntime.renderProgressiveAppHtml({result:{body,status:200},request:{production:true,method:'GET',clientModules:{},distDir:path.dirname(fileURLToPath(import.meta.url)),strictMode:true},route:{},responseHeaders:{},decodeFlight,signal:options.signal,timeoutMs:remaining()});
|
|
265
|
+
for await(const chunk of response.body){remaining();htmlBytes+=chunk.byteLength;if(htmlBytes>65536)throw new Error('Renderer prewarm HTML exceeds its byte budget');}
|
|
266
|
+
remaining();
|
|
267
|
+
return{flightBytes:flight.byteLength,htmlBytes,synthetic:true};
|
|
268
|
+
}finally{await response?.cancel();}
|
|
248
269
|
}
|
|
249
270
|
export async function preloadAppRoutes(files,options){
|
|
271
|
+
const {signal,...wireOptions}=options;
|
|
272
|
+
signal?.throwIfAborted();
|
|
250
273
|
const worker=getWorker(true),id=`preload-${++nextId}`;
|
|
251
274
|
worker.ref();
|
|
252
|
-
try{await new Promise((resolve,reject)=>{
|
|
253
|
-
const clean=()=>{clearTimeout(timer);worker.off('message',message);worker.off('error',error);worker.off('exit',exit);};
|
|
254
|
-
const message=value=>{if(value.type==='preloaded'&&value.id===id){clean();value.error?reject(new Error(value.error)):resolve();}};
|
|
275
|
+
try{return await new Promise((resolve,reject)=>{
|
|
276
|
+
const clean=()=>{clearTimeout(timer);worker.off('message',message);worker.off('error',error);worker.off('exit',exit);signal?.removeEventListener('abort',cancel);};
|
|
277
|
+
const message=value=>{if(value.type==='preloaded'&&value.id===id){clean();value.error?reject(new Error(value.error)):resolve(value.frameworkFlight);}};
|
|
255
278
|
const error=value=>{clean();reject(value);};const exit=()=>error(new Error('RSC worker exited while preloading'));
|
|
279
|
+
const cancel=()=>{try{worker.postMessage({type:'preload-cancel',id});}catch{}finally{error(signal.reason);}};
|
|
256
280
|
const timer=setTimeout(()=>{clean();void lifecycles.get(worker)?.terminate(new Error('RSC preload timed out'));reject(new Error('RSC preload timed out'));},options.timeoutMs);
|
|
257
281
|
worker.on('message',message);worker.once('error',error);worker.once('exit',exit);
|
|
258
|
-
|
|
282
|
+
signal?.addEventListener('abort',cancel,{once:true});
|
|
283
|
+
try{worker.postMessage({type:'preload',id,value:{files,options:wireOptions}});}catch(failure){error(failure);}
|
|
259
284
|
});}finally{if(!hasPending(worker)&&!lifecycles.get(worker)?.retiring)worker.unref();}
|
|
260
285
|
}
|
|
261
286
|
|
|
@@ -12,6 +12,8 @@ export function workerStreamChannel(port, id) {
|
|
|
12
12
|
let ended = false;
|
|
13
13
|
let previousSignal;
|
|
14
14
|
let combinedSignal;
|
|
15
|
+
let chunkMessage;
|
|
16
|
+
let transfer;
|
|
15
17
|
const wait = async signal => {
|
|
16
18
|
while (!credit) {
|
|
17
19
|
signal.throwIfAborted();
|
|
@@ -43,11 +45,22 @@ export function workerStreamChannel(port, id) {
|
|
|
43
45
|
if (!Buffer.isBuffer(bytes) && offset === 0 && length === bytes.byteLength && bytes.buffer.byteLength <= APP_STREAM_CHUNK_BYTES) chunk = bytes;
|
|
44
46
|
else { chunk = new Uint8Array(length); chunk.set(bytes.subarray(offset, offset + length)); }
|
|
45
47
|
const last = offset + length === bytes.byteLength;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
// MessagePort serializes synchronously. Reuse this request's ordinary
|
|
49
|
+
// chunk envelope and transfer list after the first frame rather than
|
|
50
|
+
// allocating both for every chunk. The one-frame case keeps its old
|
|
51
|
+
// allocation count; no envelope or payload is shared across requests.
|
|
52
|
+
const message = metadata ? { id, type: 'start', ...metadata }
|
|
53
|
+
: chunkMessage ??= { id, type: 'chunk' };
|
|
54
|
+
const complete = done && last;
|
|
55
|
+
message.chunk = chunk.buffer;
|
|
56
|
+
message.byteOffset = chunk.byteOffset;
|
|
57
|
+
message.byteLength = chunk.byteLength;
|
|
58
|
+
message.done = complete;
|
|
48
59
|
metadata = undefined;
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
(transfer ??= [undefined])[0] = chunk.buffer;
|
|
61
|
+
try { port.postMessage(message, transfer); }
|
|
62
|
+
finally { transfer[0] = message.chunk = undefined; }
|
|
63
|
+
if (complete) ended = true;
|
|
51
64
|
}
|
|
52
65
|
},
|
|
53
66
|
end() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
|
|
2
|
-
import { htmlEnvelope } from './html-envelope.mjs';
|
|
2
|
+
import { htmlEnvelope, flightBootstrap } from './html-envelope.mjs';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { fontPreloads } from './font-preload.mjs';
|
|
5
5
|
import { isDynamicBailout } from '../compat/dynamic-bailout.cjs';
|
|
@@ -16,11 +16,14 @@ import { streamSelector } from './stream-select.mjs';
|
|
|
16
16
|
import {nodeHtmlReader,useNodeHtml} from './node-html-stream.mjs';
|
|
17
17
|
|
|
18
18
|
import {flightTap} from './flight-tap.mjs';
|
|
19
|
+
import {createFlightMeasurement} from './flight-diagnostics.mjs';
|
|
19
20
|
|
|
20
21
|
export async function renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal, timeoutMs, partial }) {
|
|
21
22
|
timeoutMs ??= route?.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : 25_000;
|
|
22
23
|
const nonceAttribute = request.nonce ? ` nonce="${escapeHtml(request.nonce)}"` : '';
|
|
23
|
-
const
|
|
24
|
+
const measurement = createFlightMeasurement(request, 'ssr');
|
|
25
|
+
const tap = flightTap(result.body, measurement);
|
|
26
|
+
let status = result.status;
|
|
24
27
|
const abort = new AbortController();
|
|
25
28
|
const timeout = setTimeout(() => abort.abort(appTimeoutError('HTML stream', timeoutMs)), timeoutMs);
|
|
26
29
|
const combinedSignal = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
|
|
@@ -29,7 +32,11 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
29
32
|
let committed = Boolean(partial?.shellSent);
|
|
30
33
|
let control;
|
|
31
34
|
let model;
|
|
35
|
+
let bootstrapFlight;
|
|
36
|
+
const failed = error => measurement?.finish(error?.statusCode === 504 || combinedSignal.reason?.statusCode === 504
|
|
37
|
+
? 'timedOut' : combinedSignal.aborted ? 'cancelled' : 'failed', status);
|
|
32
38
|
const cancel = async reason => {
|
|
39
|
+
measurement?.finish(reason?.statusCode === 504 ? 'timedOut' : 'cancelled', status);
|
|
33
40
|
clearTimeout(timeout);
|
|
34
41
|
combinedSignal.removeEventListener('abort', onAbort);
|
|
35
42
|
if (!abort.signal.aborted) abort.abort(reason || new Error('App Router response was cancelled'));
|
|
@@ -39,6 +46,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
39
46
|
combinedSignal.addEventListener('abort', onAbort, { once: true });
|
|
40
47
|
try {
|
|
41
48
|
model = await decodeFlight(tap.body, request.clientModules, request.distDir, { production: request.production });
|
|
49
|
+
measurement?.mark('modelReady');
|
|
42
50
|
const renderOptions = {
|
|
43
51
|
signal: combinedSignal,
|
|
44
52
|
nonce: request.nonce,
|
|
@@ -81,15 +89,17 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
81
89
|
}
|
|
82
90
|
const styles = fontPreloads(route.fonts, request.nonce) + (model.css ?? route.css ?? []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
|
|
83
91
|
if (styles && !partial) prefix = prefix.replace(/<\/head>/i, `${styles}</head>`);
|
|
92
|
+
measurement?.mark('shellReady');
|
|
84
93
|
tap.startOutput();
|
|
85
94
|
committed = true;
|
|
86
95
|
const body = (async function* () {
|
|
87
96
|
let total = 0;
|
|
88
97
|
const envelope = htmlEnvelope({ resumed: Boolean(partial) });
|
|
89
98
|
const encode = value => {
|
|
90
|
-
const bytes = Buffer.from(value);
|
|
99
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
91
100
|
total += bytes.byteLength;
|
|
92
101
|
if (total > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
|
|
102
|
+
if (measurement) measurement.responseBytes = total;
|
|
93
103
|
return bytes;
|
|
94
104
|
};
|
|
95
105
|
let flightDone = false;
|
|
@@ -116,9 +126,8 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
116
126
|
} else {
|
|
117
127
|
if (next.done) { flightDone = true; }
|
|
118
128
|
else {
|
|
119
|
-
const encoded = Buffer.from(next.value.buffer, next.value.byteOffset, next.value.byteLength).toString('base64');
|
|
120
129
|
selection.watch('flight', tap.next());
|
|
121
|
-
yield encode(
|
|
130
|
+
yield encode((bootstrapFlight ??= flightBootstrap(nonceAttribute))(next.value));
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
if (control) {
|
|
@@ -128,6 +137,10 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
yield encode(`<script${nonceAttribute}>self.__PRNEXT_FLIGHT_STREAM__.push(null);document.currentScript.remove()</script>` + envelope.finish());
|
|
140
|
+
measurement?.finish('completed', status);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
failed(error);
|
|
143
|
+
throw error;
|
|
131
144
|
} finally {
|
|
132
145
|
selection.close();
|
|
133
146
|
combinedSignal.removeEventListener('abort', onAbort);
|
|
@@ -136,7 +149,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
136
149
|
})();
|
|
137
150
|
// An error encoded in Flight may be contained by a valid Suspense shell.
|
|
138
151
|
// Ordinary GET errors select HTTP 500 only when that shell also fails.
|
|
139
|
-
|
|
152
|
+
status = result.rscError && result.status === 500 && ['GET', 'HEAD'].includes(request.method) ? 200 : result.status;
|
|
140
153
|
return { status, headers: { ...responseHeaders, ...result.headers, 'content-type': 'text/html; charset=utf-8' }, body, cancel };
|
|
141
154
|
} catch (error) {
|
|
142
155
|
if (!committed && !combinedSignal.aborted && error?.statusCode !== 504 &&
|
|
@@ -151,13 +164,16 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
151
164
|
(route.client ? `<script type="module" async${nonceAttribute} src="${escapeHtml(route.client)}"></script>` : '');
|
|
152
165
|
const prefix = shell.slice(0, end) + bootstrap;
|
|
153
166
|
const closing = shell.slice(end);
|
|
167
|
+
status = route.parallel && navigationResponse(error)?.status === 404 ? 404 : 500;
|
|
168
|
+
measurement?.mark('shellReady');
|
|
154
169
|
tap.startOutput();
|
|
155
170
|
const body = (async function* () {
|
|
156
171
|
let total = 0;
|
|
157
172
|
const encode = value => {
|
|
158
|
-
const bytes = Buffer.from(value);
|
|
173
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
159
174
|
total += bytes.byteLength;
|
|
160
175
|
if (total > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
|
|
176
|
+
if (measurement) measurement.responseBytes = total;
|
|
161
177
|
return bytes;
|
|
162
178
|
};
|
|
163
179
|
try {
|
|
@@ -166,18 +182,21 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
166
182
|
combinedSignal.throwIfAborted();
|
|
167
183
|
const next = await tap.next();
|
|
168
184
|
if (next.done) break;
|
|
169
|
-
|
|
170
|
-
yield encode(`<script${nonceAttribute}>(self.__PRNEXT_FLIGHT_STREAM__||=[]).push("${encoded}");document.currentScript.remove()</script>`);
|
|
185
|
+
yield encode((bootstrapFlight ??= flightBootstrap(nonceAttribute))(next.value));
|
|
171
186
|
}
|
|
172
187
|
yield encode(`<script${nonceAttribute}>self.__PRNEXT_FLIGHT_STREAM__.push(null);document.currentScript.remove()</script>` + closing);
|
|
173
|
-
|
|
188
|
+
measurement?.finish('completed', status);
|
|
189
|
+
} catch (failure) { failed(failure); throw failure; }
|
|
190
|
+
finally { await cancel(); }
|
|
174
191
|
})();
|
|
175
|
-
return { status
|
|
192
|
+
return { status, headers: { ...responseHeaders, ...result.headers, 'content-type': 'text/html; charset=utf-8' }, body, cancel };
|
|
176
193
|
} catch (recoveryError) {
|
|
194
|
+
failed(recoveryError);
|
|
177
195
|
await cancel(recoveryError);
|
|
178
196
|
throw combinedSignal.reason?.statusCode === 504 ? combinedSignal.reason : recoveryError;
|
|
179
197
|
}
|
|
180
198
|
}
|
|
199
|
+
failed(error);
|
|
181
200
|
combinedSignal.removeEventListener('abort', onAbort);
|
|
182
201
|
await cancel(error);
|
|
183
202
|
if (combinedSignal.reason?.statusCode === 504) throw combinedSignal.reason;
|
|
@@ -4,6 +4,7 @@ import { actionManifest, registerCacheClientModule } from './action-server.mjs';
|
|
|
4
4
|
import { codecSymbol } from '../compat/use-cache.cjs';
|
|
5
5
|
import { encodeCacheArguments } from './cache-arguments.mjs';
|
|
6
6
|
import { flightBundler } from './worker-metadata.mjs';
|
|
7
|
+
import {primitiveCacheResult,flightCacheResult,isCacheResultPacket,readCacheResult,CACHE_RESULT_KEY_VERSION,CACHE_RESULT_LIMIT} from './cache-result.mjs';
|
|
7
8
|
const consumers = new WeakMap();
|
|
8
9
|
const emptyModules = Object.freeze({});
|
|
9
10
|
|
|
@@ -32,6 +33,7 @@ export function flightConsumer(context) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
globalThis[codecSymbol] = {
|
|
36
|
+
cacheKeyVersion:CACHE_RESULT_KEY_VERSION,
|
|
35
37
|
async arguments(values, context) {
|
|
36
38
|
const temporaryReferences = clientReferences();
|
|
37
39
|
const body = await encodeReply(values, { temporaryReferences, signal: context.signal });
|
|
@@ -41,27 +43,45 @@ globalThis[codecSymbol] = {
|
|
|
41
43
|
return { key, values: decoded, temporaryReferences, serverTemporaryReferences };
|
|
42
44
|
},
|
|
43
45
|
async render(callback, encoded, context) {
|
|
46
|
+
context.signal?.throwIfAborted();
|
|
44
47
|
const bundler = flightBundler(context.clientModules, false);
|
|
45
48
|
let failure;
|
|
46
49
|
// React calls then inside its rendering context, preserving React.cache isolation.
|
|
47
50
|
// React can subscribe more than once while unwrapping a thenable. Start
|
|
48
51
|
// lazily in its rendering context, but never execute the producer twice.
|
|
49
|
-
let execution;
|
|
50
|
-
const result = { then(resolve, reject) { (execution ||= Promise.resolve().then(callback)).then(
|
|
52
|
+
let execution,checked=false,primitive;
|
|
53
|
+
const result = { then(resolve, reject) { (execution ||= Promise.resolve().then(callback)).then(value=>{
|
|
54
|
+
try{
|
|
55
|
+
context.signal?.throwIfAborted();
|
|
56
|
+
if(!checked){primitive=primitiveCacheResult(value);checked=true;}
|
|
57
|
+
// The producer still runs inside the official renderer exactly once.
|
|
58
|
+
// Do not return the original object through another Promise: that would
|
|
59
|
+
// inspect a user-defined then getter again before React receives it.
|
|
60
|
+
resolve(primitive?null:value);
|
|
61
|
+
}catch(error){reject(error);}
|
|
62
|
+
},reject); } };
|
|
51
63
|
const stream = renderToReadableStream(result, bundler, { signal: context.signal, temporaryReferences: encoded.serverTemporaryReferences, onError(error) { failure ||= error; return 'PRNextCacheRenderError'; } });
|
|
52
64
|
const chunks = [];
|
|
53
65
|
let size = 0;
|
|
54
66
|
for await (const value of stream) {
|
|
55
67
|
size += value.byteLength;
|
|
56
68
|
if (size > 16 * 1024 * 1024) throw new Error('Cached React result exceeds the 16 MiB rendering limit');
|
|
57
|
-
|
|
69
|
+
// React exposes an official type:'bytes' Web stream: each delivered
|
|
70
|
+
// Uint8Array owns its storage. Keep it until the sole packet aggregate;
|
|
71
|
+
// copy Buffers/other representations defensively instead.
|
|
72
|
+
chunks.push(value instanceof Uint8Array&&!Buffer.isBuffer(value)?value:Buffer.from(value));
|
|
58
73
|
}
|
|
59
74
|
if (failure) throw failure;
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
return
|
|
75
|
+
context.signal?.throwIfAborted();
|
|
76
|
+
// The packet always owns a final aggregate; no producer storage escapes.
|
|
77
|
+
return primitive||flightCacheResult(chunks,size);
|
|
63
78
|
},
|
|
64
79
|
async decode(bytes, encoded, context) {
|
|
80
|
+
if(isCacheResultPacket(bytes)){
|
|
81
|
+
const result=readCacheResult(bytes);
|
|
82
|
+
if(!result.flight)return result.value;
|
|
83
|
+
bytes=result.flight;
|
|
84
|
+
}else if(bytes.length>CACHE_RESULT_LIMIT)throw new Error('Invalid RSC cache result envelope');
|
|
65
85
|
return createFromReadableStream(new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }), {
|
|
66
86
|
serverConsumerManifest: flightConsumer(context), temporaryReferences: encoded.temporaryReferences, replayConsoleLogs: false,
|
|
67
87
|
});
|