@thomas.f/prnext 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/build/action-exports.mjs +107 -0
- package/build/actions.mjs +214 -0
- package/build/app-config.mjs +138 -0
- package/build/app-routing.mjs +119 -0
- package/build/app-static.mjs +158 -0
- package/build/app.mjs +536 -0
- package/build/cache-components.mjs +91 -0
- package/build/cache-handlers.mjs +54 -0
- package/build/check.mjs +40 -0
- package/build/compiler.mjs +222 -0
- package/build/config.mjs +193 -0
- package/build/css-modules.mjs +114 -0
- package/build/custom-routes.mjs +363 -0
- package/build/dev-worker.mjs +18 -0
- package/build/dev.mjs +76 -0
- package/build/directory-cache.mjs +29 -0
- package/build/dynamic.mjs +125 -0
- package/build/edge.mjs +141 -0
- package/build/font-data.LICENSE.txt +25 -0
- package/build/font-data.json +1 -0
- package/build/font-files.mjs +221 -0
- package/build/fonts.mjs +131 -0
- package/build/framework-imports.mjs +17 -0
- package/build/i18n.mjs +39 -0
- package/build/images-config.mjs +62 -0
- package/build/images.mjs +86 -0
- package/build/incremental.mjs +76 -0
- package/build/index.mjs +356 -0
- package/build/loader-cache.mjs +69 -0
- package/build/metadata-images.mjs +72 -0
- package/build/metadata-routes.mjs +77 -0
- package/build/middleware.mjs +231 -0
- package/build/migrate.mjs +289 -0
- package/build/module-resolution.mjs +78 -0
- package/build/native-discovery.mjs +134 -0
- package/build/native-packages.mjs +105 -0
- package/build/npm-packages.mjs +110 -0
- package/build/og.mjs +14 -0
- package/build/page-extensions.mjs +12 -0
- package/build/passthrough-loader.cjs +4 -0
- package/build/precompress.mjs +122 -0
- package/build/scan.mjs +178 -0
- package/build/script-workers.mjs +25 -0
- package/build/server-actions-config.mjs +27 -0
- package/build/source-maps.mjs +34 -0
- package/build/standalone-config.mjs +19 -0
- package/build/standalone-runner.mjs +21 -0
- package/build/standalone-worker.mjs +5 -0
- package/build/standalone.mjs +240 -0
- package/build/static-export.mjs +93 -0
- package/build/styles.mjs +174 -0
- package/build/transform-cache.mjs +50 -0
- package/build/transform.mjs +155 -0
- package/build/turbopack-conditions.mjs +59 -0
- package/build/webpack-bridge.cjs +3 -0
- package/build/webpack-graph.mjs +256 -0
- package/build/webpack-loader.cjs +9 -0
- package/cli.mjs +210 -0
- package/compat/app-client-page.cjs +32 -0
- package/compat/app-context.cjs +40 -0
- package/compat/app-context.d.cts +20 -0
- package/compat/app-dynamic.cjs +34 -0
- package/compat/app-error-boundary.cjs +53 -0
- package/compat/app-global-error-boundary.cjs +34 -0
- package/compat/app-layout-context.cjs +27 -0
- package/compat/app-navigation-boundary.cjs +47 -0
- package/compat/app.cjs +33 -0
- package/compat/app.d.cts +9 -0
- package/compat/cache-handlers.cjs +223 -0
- package/compat/cache-life.cjs +34 -0
- package/compat/cache.cjs +99 -0
- package/compat/cache.d.cts +36 -0
- package/compat/compat/router.d.cts +2 -0
- package/compat/compat-router.cjs +7 -0
- package/compat/constants.cjs +8 -0
- package/compat/constants.d.cts +7 -0
- package/compat/cookies.cjs +129 -0
- package/compat/data-cache.cjs +389 -0
- package/compat/dist/shared/lib/app-router-context.shared-runtime.d.cts +3 -0
- package/compat/dist/shared/lib/router-context.d.cts +1 -0
- package/compat/dist/shared/lib/router-context.shared-runtime.d.cts +3 -0
- package/compat/document.cjs +73 -0
- package/compat/document.d.cts +41 -0
- package/compat/draft.cjs +28 -0
- package/compat/dynamic-bailout.cjs +13 -0
- package/compat/dynamic.cjs +119 -0
- package/compat/dynamic.d.cts +32 -0
- package/compat/error.cjs +36 -0
- package/compat/error.d.cts +15 -0
- package/compat/fetch.d.cts +10 -0
- package/compat/font/google.d.cts +1 -0
- package/compat/font/local.d.cts +2 -0
- package/compat/font-google.cjs +2 -0
- package/compat/font-google.d.cts +1944 -0
- package/compat/font-local.cjs +5 -0
- package/compat/font-local.d.cts +3 -0
- package/compat/font-types.d.cts +25 -0
- package/compat/head.cjs +180 -0
- package/compat/head.d.cts +3 -0
- package/compat/headers.cjs +89 -0
- package/compat/headers.d.cts +26 -0
- package/compat/image-config.cjs +3 -0
- package/compat/image-imports.d.ts +12 -0
- package/compat/image-shared.cjs +62 -0
- package/compat/image.cjs +64 -0
- package/compat/image.d.cts +35 -0
- package/compat/incremental-cache.cjs +186 -0
- package/compat/index.d.ts +159 -0
- package/compat/instant-samples.cjs +39 -0
- package/compat/instant.cjs +13 -0
- package/compat/link.cjs +68 -0
- package/compat/link.d.cts +17 -0
- package/compat/locale.cjs +30 -0
- package/compat/metadata.d.cts +77 -0
- package/compat/navigation-controls.cjs +27 -0
- package/compat/navigation-server.cjs +25 -0
- package/compat/navigation.cjs +52 -0
- package/compat/navigation.d.cts +28 -0
- package/compat/next-app-router-context.cjs +6 -0
- package/compat/next-router-context.cjs +3 -0
- package/compat/og.cjs +32 -0
- package/compat/og.d.cts +1 -0
- package/compat/paths.cjs +33 -0
- package/compat/preview.cjs +41 -0
- package/compat/rewrite.cjs +41 -0
- package/compat/route-request.cjs +64 -0
- package/compat/router.cjs +137 -0
- package/compat/router.d.cts +58 -0
- package/compat/script-context.cjs +5 -0
- package/compat/script-loader.cjs +135 -0
- package/compat/script.cjs +69 -0
- package/compat/script.d.cts +17 -0
- package/compat/server-primitives.cjs +109 -0
- package/compat/server.cjs +13 -0
- package/compat/server.d.cts +37 -0
- package/compat/static-generation.cjs +108 -0
- package/compat/use-cache.cjs +129 -0
- package/index.mjs +1 -0
- package/native/platforms.json +6 -0
- package/native/resolve.mjs +81 -0
- package/package.json +127 -0
- package/release/verify.mjs +59 -0
- package/runtime/action-client.mjs +19 -0
- package/runtime/action-crypto.mjs +138 -0
- package/runtime/action-reference.mjs +2 -0
- package/runtime/action-server.mjs +161 -0
- package/runtime/action-ssr.mjs +5 -0
- package/runtime/api.mjs +326 -0
- package/runtime/app-client.mjs +539 -0
- package/runtime/app-content.mjs +8 -0
- package/runtime/app-errors.mjs +12 -0
- package/runtime/app-instant.mjs +30 -0
- package/runtime/app-metadata.mjs +410 -0
- package/runtime/app-partial-model.mjs +228 -0
- package/runtime/app-partial.mjs +130 -0
- package/runtime/app-prefetch.mjs +34 -0
- package/runtime/app-recovery.mjs +87 -0
- package/runtime/app-render.mjs +544 -0
- package/runtime/app-routing-access.mjs +143 -0
- package/runtime/app-routing-context.mjs +35 -0
- package/runtime/app-routing-state.mjs +61 -0
- package/runtime/app-routing.mjs +323 -0
- package/runtime/app-segments.mjs +22 -0
- package/runtime/app-static-params.mjs +120 -0
- package/runtime/app-static.mjs +86 -0
- package/runtime/app-stream-channel.mjs +92 -0
- package/runtime/app-stream-html.mjs +279 -0
- package/runtime/build-directory.mjs +48 -0
- package/runtime/cache-arguments.mjs +33 -0
- package/runtime/cache-components-rsc.mjs +64 -0
- package/runtime/cache-request.mjs +23 -0
- package/runtime/client-navigation.mjs +40 -0
- package/runtime/dev-client.mjs +129 -0
- package/runtime/dev-file-changes.mjs +110 -0
- package/runtime/document.mjs +96 -0
- package/runtime/edge.mjs +105 -0
- package/runtime/env.mjs +68 -0
- package/runtime/fetch-cache.mjs +379 -0
- package/runtime/font-preload.mjs +4 -0
- package/runtime/http.mjs +190 -0
- package/runtime/incremental-cache.mjs +83 -0
- package/runtime/metadata-route.mjs +83 -0
- package/runtime/middleware.mjs +186 -0
- package/runtime/module-loader.mjs +21 -0
- package/runtime/navigation.mjs +16 -0
- package/runtime/og-loader.mjs +17 -0
- package/runtime/pages-client.mjs +197 -0
- package/runtime/pages-error-props.mjs +6 -0
- package/runtime/pages-errors.mjs +85 -0
- package/runtime/pages-initial-props.mjs +43 -0
- package/runtime/pages-navigation.mjs +565 -0
- package/runtime/pages-paths.mjs +80 -0
- package/runtime/pages-revalidate.mjs +33 -0
- package/runtime/partial-artifact-cache.mjs +37 -0
- package/runtime/partial-template.mjs +174 -0
- package/runtime/profile.mjs +9 -0
- package/runtime/profiles.json +47 -0
- package/runtime/render.mjs +304 -0
- package/runtime/request-transport.mjs +41 -0
- package/runtime/request-work.mjs +7 -0
- package/runtime/route-static.mjs +117 -0
- package/runtime/rsc-worker.mjs +426 -0
- package/runtime/script-html.mjs +61 -0
- package/runtime/stream-select.mjs +25 -0
- package/runtime/stream-utils.mjs +127 -0
- package/runtime/transport.mjs +108 -0
- package/runtime/watch-project.mjs +130 -0
- package/runtime/worker-artifacts.mjs +22 -0
- package/runtime/worker-metadata.mjs +31 -0
- package/runtime/worker.mjs +197 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { flightBundler } from './worker-metadata.mjs';
|
|
3
|
+
import { removeBasePath, normalizeTrailingSlash } from '../compat/paths.cjs';
|
|
4
|
+
import { renderToReadableStream } from 'react-server-dom-webpack/server.node';
|
|
5
|
+
import { createFromReadableStream } from 'react-server-dom-webpack/client.edge';
|
|
6
|
+
import { parentPort } from 'node:worker_threads';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { runRequestContext, currentRequest } from '../compat/headers.cjs';
|
|
10
|
+
import { Metadata } from './app-metadata.mjs';
|
|
11
|
+
import { appTimeoutError } from './app-errors.mjs';
|
|
12
|
+
import { paramsBySegment, selectedLayoutSegments } from './app-segments.mjs';
|
|
13
|
+
import { instantEnabled } from '../compat/instant.cjs';
|
|
14
|
+
import { createAdvancedTree, loadAdvancedRouting } from './app-routing.mjs';
|
|
15
|
+
import { authorizeAdvancedRouting } from './app-routing-access.mjs';
|
|
16
|
+
import { routingBranchRenderer } from './app-routing-context.mjs';
|
|
17
|
+
import { runAction } from './action-server.mjs';
|
|
18
|
+
import { workerStreamChannel, APP_STREAM_CHUNK_BYTES } from './app-stream-channel.mjs';
|
|
19
|
+
import { flushCacheWork, getCachePaths } from '../compat/data-cache.cjs';
|
|
20
|
+
import { staticMetadata, staticSearchParams, staticParams, trackStaticDependency } from '../compat/static-generation.cjs';
|
|
21
|
+
import { collectStaticParams, validateRuntimeStaticConfig } from './app-static-params.mjs';
|
|
22
|
+
import { navigationResponse } from './navigation.mjs';
|
|
23
|
+
import { installFetchCache } from './fetch-cache.mjs';
|
|
24
|
+
import { flightConsumer } from './cache-components-rsc.mjs';
|
|
25
|
+
import { partialModel, isPartialHole, boundedPartialFlight } from './app-partial-model.mjs';
|
|
26
|
+
import { PartialTemplates, canResumePartialDirectly, partialTemplateReuse } from './partial-template.mjs';
|
|
27
|
+
|
|
28
|
+
installFetchCache();
|
|
29
|
+
|
|
30
|
+
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
31
|
+
const routeEntries = new Map();
|
|
32
|
+
const partialTemplates = new PartialTemplates();
|
|
33
|
+
async function routeEntry(request) {
|
|
34
|
+
const key = request.modulePath;
|
|
35
|
+
if (request.production && routeEntries.has(key)) {
|
|
36
|
+
const entry = routeEntries.get(key);
|
|
37
|
+
routeEntries.delete(key); routeEntries.set(key, entry);
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
const entry = await import(pathToFileURL(key).href);
|
|
41
|
+
if (request.production) {
|
|
42
|
+
if (routeEntries.size >= 64) routeEntries.delete(routeEntries.keys().next().value);
|
|
43
|
+
routeEntries.set(key, entry);
|
|
44
|
+
}
|
|
45
|
+
return entry;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function component(module, name) {
|
|
49
|
+
const value = module?.default;
|
|
50
|
+
if (typeof value !== 'function' && (typeof value !== 'object' || value === null)) {
|
|
51
|
+
throw new Error(`App Router ${name} must export a React component as default`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function queryObject(url) {
|
|
57
|
+
const values = Object.create(null);
|
|
58
|
+
for (const [name, value] of url.searchParams) {
|
|
59
|
+
if (name === '_rsc') continue;
|
|
60
|
+
if (!Object.hasOwn(values, name)) values[name] = value;
|
|
61
|
+
else values[name] = [...(Array.isArray(values[name]) ? values[name] : [values[name]]), value];
|
|
62
|
+
}
|
|
63
|
+
// Client pages receive this promise through Flight, which requires ordinary
|
|
64
|
+
// plain objects. Spread preserves an own __proto__ query without invoking it.
|
|
65
|
+
return { ...values };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createTree(entry, request, fallback = null) {
|
|
69
|
+
const params = staticParams(request.params || {});
|
|
70
|
+
const searchParams = staticSearchParams(queryObject(new URL(request.url)), currentRequest(), entry.page?.default?.$$typeof === Symbol.for('react.client.reference'));
|
|
71
|
+
if (entry.routing && !fallback) return createAdvancedTree(entry, request, searchParams);
|
|
72
|
+
const segments = entry.segments || [];
|
|
73
|
+
const page = fallback?.kind === 'not-found' ? segments[fallback.index].notFound : entry.page;
|
|
74
|
+
const pageKey = `${request.routePattern || new URL(request.url).pathname}:${JSON.stringify(request.params || {})}`;
|
|
75
|
+
const keyScopes = [];
|
|
76
|
+
const instantTrees = [];
|
|
77
|
+
const instantDisabled = [entry.pageConfig, ...segments.map(segment => segment.staticConfig)].some(config => config?.instant && typeof config.instant === 'object' && (config.instant.unstable_disableValidation || (request.production ? config.instant.unstable_disableBuildValidation : config.instant.unstable_disableDevValidation)));
|
|
78
|
+
function keyScope(prefix, values, liveProps) {
|
|
79
|
+
const key = prefix + JSON.stringify(values);
|
|
80
|
+
keyScopes.push({ key, prefix, names: Object.keys(values), ...(liveProps ? { liveProps } : {}) });
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
if (request.partialParams?.length) keyScope(`${request.routePattern || new URL(request.url).pathname}:`, request.params || {});
|
|
84
|
+
const metadata = React.createElement(RouteMetadata, { key: 'metadata' });
|
|
85
|
+
const head = request.cacheComponents ? React.createElement(React.Suspense, { fallback: null, key: 'metadata-boundary' }, metadata) : metadata;
|
|
86
|
+
const rootLayout = segments.find(segment => segment.layout);
|
|
87
|
+
let tree = null;
|
|
88
|
+
if (fallback?.kind !== 'error') {
|
|
89
|
+
const Page = component(page, fallback ? 'not-found' : 'page');
|
|
90
|
+
tree = !fallback && entry.ClientPageRoot && Page.$$typeof === Symbol.for('react.client.reference')
|
|
91
|
+
? React.createElement(entry.ClientPageRoot, { Component: Page, params, searchParams, key: pageKey })
|
|
92
|
+
: React.createElement(Page, { params, searchParams, key: pageKey });
|
|
93
|
+
}
|
|
94
|
+
const scopes = new Map();
|
|
95
|
+
const scopedParams = paramsBySegment(segments, request.params || {});
|
|
96
|
+
for (const segment of segments) {
|
|
97
|
+
scopes.set(segment, `${segment.path || ''}:${JSON.stringify(scopedParams.get(segment))}`);
|
|
98
|
+
if (request.partialParams?.length) for (const prefix of ['', 'error:', 'navigation:', 'layout:']) keyScope(`${prefix}${segment.path || ''}:`, scopedParams.get(segment), prefix === 'layout:' ? ['segments'] : undefined);
|
|
99
|
+
}
|
|
100
|
+
let instantConfig = entry.pageConfig?.instant;
|
|
101
|
+
for (const segment of [...(fallback ? segments.slice(0, fallback.index + 1) : segments)].reverse()) {
|
|
102
|
+
if (segment.loading) tree = React.createElement(React.Suspense, {
|
|
103
|
+
fallback: React.createElement(component(segment.loading, 'loading')),
|
|
104
|
+
}, tree);
|
|
105
|
+
if (request.instantValidation && !instantDisabled && segment.layout) {
|
|
106
|
+
if (instantEnabled(instantConfig, request.production)) instantTrees.push({ path: segment.path || '/', tree: React.createElement(React.Fragment, null, tree) });
|
|
107
|
+
if (segment.staticConfig?.instant !== undefined) instantConfig = segment.staticConfig.instant;
|
|
108
|
+
}
|
|
109
|
+
if (segment.error && entry.ErrorBoundary) tree = React.createElement(entry.ErrorBoundary, {
|
|
110
|
+
errorComponent: component(segment.error, 'error'),
|
|
111
|
+
initialError: fallback?.kind === 'error' && fallback.index === segments.indexOf(segment) ? fallback.error : null,
|
|
112
|
+
resetKey: request.renderKey,
|
|
113
|
+
key: `error:${scopes.get(segment)}`,
|
|
114
|
+
}, tree);
|
|
115
|
+
if (entry.NavigationBoundary && (segment.notFound || segment === segments[0])) tree = React.createElement(entry.NavigationBoundary, {
|
|
116
|
+
notFound: segment.notFound ? React.createElement(component(segment.notFound, 'not-found')) : React.createElement('h1', null, '404: This page could not be found.'),
|
|
117
|
+
resetKey: request.renderKey,
|
|
118
|
+
key: `navigation:${scopes.get(segment)}`,
|
|
119
|
+
}, tree);
|
|
120
|
+
if (segment.template) {
|
|
121
|
+
const child = segments[segments.indexOf(segment) + 1];
|
|
122
|
+
tree = React.createElement(component(segment.template, 'template'), { key: child ? scopes.get(child) : pageKey }, tree);
|
|
123
|
+
}
|
|
124
|
+
if (segment.layout) {
|
|
125
|
+
if (request.cacheComponents && segment === rootLayout) tree = React.createElement(React.Fragment, null, head, tree);
|
|
126
|
+
tree = React.createElement(component(segment.layout, 'layout'), { params: staticParams(scopedParams.get(segment)), key: scopes.get(segment) }, tree);
|
|
127
|
+
if (entry.LayoutProvider) tree = React.createElement(entry.LayoutProvider, { key: `layout:${scopes.get(segment)}`,
|
|
128
|
+
segments: { children: selectedLayoutSegments(segments.slice(segments.indexOf(segment) + 1), request.params) } }, tree);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Keep module namespaces out of development owner props: they can contain
|
|
132
|
+
// large application exports which need not be serialized as Flight debug data.
|
|
133
|
+
async function RouteMetadata() {
|
|
134
|
+
const pathname = request.originalUrl
|
|
135
|
+
? removeBasePath(new URL(request.originalUrl).pathname, request.basePath)
|
|
136
|
+
: new URL(request.url).pathname;
|
|
137
|
+
return runRequestContext({ ...currentRequest(), metadataRendering: true }, () =>
|
|
138
|
+
Metadata({ entry, params, searchParams, pathname, notFoundIndex: fallback?.index ?? -1 }));
|
|
139
|
+
}
|
|
140
|
+
return { tree: request.cacheComponents ? tree : React.createElement(React.Fragment, null, head, tree), head, rootLayout: entry.rootLayout,
|
|
141
|
+
...(keyScopes.length ? { keyScopes } : {}), ...(request.instantValidation ? { instantTrees } : {}) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ordinaryRenderError(error) {
|
|
145
|
+
return error && error.code !== 'PRNEXT_DYNAMIC_SERVER_USAGE' && !isPartialHole(error) && error.statusCode !== 504 && !navigationResponse(error);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function publicError(error, production) {
|
|
149
|
+
const digest = typeof error?.digest === 'string' ? error.digest : createHash('sha256').update(String(error?.stack || error?.message || error)).digest('hex').slice(0, 16);
|
|
150
|
+
return { message: production ? 'An error occurred while rendering this page. See the server logs for details.' : error?.message || String(error), digest };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function errorBoundary(entry, error, production, before = Infinity) {
|
|
154
|
+
const index = entry.ErrorBoundary ? (entry.segments || []).findLastIndex((segment, index) => index < before && segment.error) : -1;
|
|
155
|
+
return index < 0 ? null : { kind: 'error', index, error: publicError(error, production) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function renderFlight(request, fallback = null, channel = null) {
|
|
159
|
+
return runRequestContext({ ...request, phase: 'render' }, async () => {
|
|
160
|
+
const cacheContext = currentRequest();
|
|
161
|
+
try {
|
|
162
|
+
if (cacheContext.staticState) trackStaticDependency({ paths: getCachePaths(cacheContext) }, cacheContext);
|
|
163
|
+
const allowedRouting = request.controlModel || await authorizeAdvancedRouting(request, channel?.signal);
|
|
164
|
+
const module = await routeEntry(request);
|
|
165
|
+
const entry = allowedRouting ? await loadAdvancedRouting(module, request) : module;
|
|
166
|
+
if (!allowedRouting) request.responseStatus = 400;
|
|
167
|
+
if (cacheContext.staticState) validateRuntimeStaticConfig(entry);
|
|
168
|
+
if (request.actionNotFound && !fallback) {
|
|
169
|
+
const index = (entry.segments || []).findLastIndex(segment => segment.notFound);
|
|
170
|
+
if (index < 0) throw Object.assign(new Error('Not Found'), { digest: 'NEXT_HTTP_ERROR_FALLBACK;404' });
|
|
171
|
+
fallback = { kind: 'not-found', index };
|
|
172
|
+
}
|
|
173
|
+
if (request.fallbackError && !fallback) {
|
|
174
|
+
fallback = errorBoundary(entry, request.fallbackError, request.production, request.beforeErrorBoundary);
|
|
175
|
+
if (!fallback) throw Object.assign(new Error(request.fallbackError.message), request.fallbackError);
|
|
176
|
+
}
|
|
177
|
+
const url = new URL(request.url);
|
|
178
|
+
url.searchParams.delete('_rsc');
|
|
179
|
+
const visible = request.originalUrl ? new URL(request.originalUrl) : url;
|
|
180
|
+
visible.searchParams.delete('_rsc');
|
|
181
|
+
const router = { pathname: request.originalUrl ? removeBasePath(visible.pathname, request.basePath) : normalizeTrailingSlash(visible.pathname, request), basePath: request.basePath || '', trailingSlash: request.trailingSlash || false, skipTrailingSlashRedirect: request.skipTrailingSlashRedirect || false, search: request.cacheConfig?.dynamic === 'force-static' ? '' : visible.search, params: request.params || {},
|
|
182
|
+
...(request.originalUrl ? { pageSearch: url.search } : {}),
|
|
183
|
+
...(request.cacheConfig?.dynamic === 'force-static' ? { forceStatic: true } : {}) };
|
|
184
|
+
const bundlerConfig = flightBundler(request.clientModules);
|
|
185
|
+
const abort = new AbortController();
|
|
186
|
+
const renderSignal = channel ? AbortSignal.any([abort.signal, channel.signal]) : abort.signal;
|
|
187
|
+
const timeoutMs = request.renderTimeoutMs ?? 25_000;
|
|
188
|
+
const timeout = setTimeout(() => abort.abort(appTimeoutError('RSC render', timeoutMs)), timeoutMs);
|
|
189
|
+
let renderError;
|
|
190
|
+
try {
|
|
191
|
+
if (request.routingContexts?.size) request.renderRoutingBranch = routingBranchRenderer(cacheContext, bundlerConfig,
|
|
192
|
+
renderSignal, error => publicError(error, request.production).digest);
|
|
193
|
+
let model = request.controlModel || { ...(allowedRouting ? createTree(entry, request, fallback) : { tree: null, routingInvalid: true }), router, css: request.css || [], fonts: request.fonts || [], ...(request.instantDiagnostics?.length ? { instantDiagnostics: request.instantDiagnostics } : {}), ...request.actionModel };
|
|
194
|
+
if (model.routing?.params) model.router.params = model.routing.params;
|
|
195
|
+
if (request.partialFlight) {
|
|
196
|
+
const consumer = { serverConsumerManifest: flightConsumer(request), replayConsoleLogs: false };
|
|
197
|
+
const decode = () => {
|
|
198
|
+
const bytes = Buffer.from(request.partialFlight, 'base64');
|
|
199
|
+
return createFromReadableStream(new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }), consumer);
|
|
200
|
+
};
|
|
201
|
+
const stored = request.production
|
|
202
|
+
? await partialTemplates.get(request.partialFlight, consumer.serverConsumerManifest, decode, renderSignal)
|
|
203
|
+
: await decode();
|
|
204
|
+
const live = !model.routing && canResumePartialDirectly(stored) ? model : await createFromReadableStream(boundedPartialFlight(renderToReadableStream(model, bundlerConfig, {
|
|
205
|
+
signal: renderSignal,
|
|
206
|
+
onError(error) { return publicError(error, request.production).digest; },
|
|
207
|
+
})), consumer);
|
|
208
|
+
const keyMap = request.partialKeys ? new Map(request.partialKeys) : undefined;
|
|
209
|
+
model = partialModel(stored, { live, keyMap, reuse: partialTemplateReuse(stored, keyMap) });
|
|
210
|
+
}
|
|
211
|
+
const stream = renderToReadableStream(model, bundlerConfig, {
|
|
212
|
+
signal: renderSignal,
|
|
213
|
+
temporaryReferences: request.temporaryReferences,
|
|
214
|
+
onError(error) {
|
|
215
|
+
const failure = error && typeof error === 'object' ? error : new Error(String(error));
|
|
216
|
+
if (cacheContext.staticState?.partial && isPartialHole(failure)) return 'PRNEXT_PPR_DYNAMIC';
|
|
217
|
+
renderError ??= failure;
|
|
218
|
+
const digest = publicError(failure, request.production).digest;
|
|
219
|
+
if (!abort.signal.aborted && !channel?.signal.aborted && ordinaryRenderError(failure)) console.error(`[prnext] ${digest}`, failure?.stack || failure);
|
|
220
|
+
return digest;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const reader = stream.getReader();
|
|
224
|
+
const chunks = [];
|
|
225
|
+
let length = 0;
|
|
226
|
+
let lookahead;
|
|
227
|
+
try {
|
|
228
|
+
for (;;) {
|
|
229
|
+
channel?.signal.throwIfAborted();
|
|
230
|
+
const nextRead = lookahead || reader.read();
|
|
231
|
+
lookahead = undefined;
|
|
232
|
+
let { value, done } = await nextRead;
|
|
233
|
+
if (done) break;
|
|
234
|
+
length += value.byteLength;
|
|
235
|
+
if (length > MAX_RESPONSE_BYTES) {
|
|
236
|
+
const error = new Error('Flight response exceeds the 16 MiB PRNext limit');
|
|
237
|
+
abort.abort(error);
|
|
238
|
+
await reader.cancel(error);
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
if (channel) {
|
|
242
|
+
// Flight frequently emits several small rows in one render turn.
|
|
243
|
+
// Coalesce only reads that are already resolved; an async child
|
|
244
|
+
// must never hold back the shell while this batch is assembled.
|
|
245
|
+
const batch = [value];
|
|
246
|
+
let batchLength = value.byteLength;
|
|
247
|
+
function prefetch() {
|
|
248
|
+
const state = {};
|
|
249
|
+
lookahead = reader.read().then(result => { state.result = result; return result; });
|
|
250
|
+
lookahead.catch(() => {});
|
|
251
|
+
return state;
|
|
252
|
+
}
|
|
253
|
+
let next = prefetch();
|
|
254
|
+
if (!channel.started) await new Promise(resolve => setImmediate(resolve));
|
|
255
|
+
else await Promise.resolve();
|
|
256
|
+
while (next.result && !next.result.done && batch.length < 64 && batchLength + next.result.value.byteLength <= APP_STREAM_CHUNK_BYTES) {
|
|
257
|
+
batch.push(next.result.value);
|
|
258
|
+
batchLength += next.result.value.byteLength;
|
|
259
|
+
length += next.result.value.byteLength;
|
|
260
|
+
if (length > MAX_RESPONSE_BYTES) {
|
|
261
|
+
const error = new Error('Flight response exceeds the 16 MiB PRNext limit');
|
|
262
|
+
abort.abort(error);
|
|
263
|
+
await reader.cancel(error);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
next = prefetch();
|
|
267
|
+
await Promise.resolve();
|
|
268
|
+
}
|
|
269
|
+
// Errors discovered while constructing the first Flight chunk can
|
|
270
|
+
// still select an HTTP status or render the nearest fallback.
|
|
271
|
+
if (!channel.started && !request.partialFlight && renderError && !ordinaryRenderError(renderError) && !(entry.routing && renderError.digest === 'NEXT_HTTP_ERROR_FALLBACK;404')) { await reader.cancel(renderError); break; }
|
|
272
|
+
if (batch.length > 1) {
|
|
273
|
+
value = new Uint8Array(batchLength);
|
|
274
|
+
let offset = 0;
|
|
275
|
+
for (const item of batch) { value.set(item, offset); offset += item.byteLength; }
|
|
276
|
+
}
|
|
277
|
+
if (!channel.started) channel.start({ status: request.partialFlight ? 200 : fallback?.kind === 'not-found' || (entry.routing && renderError?.digest === 'NEXT_HTTP_ERROR_FALLBACK;404') ? 404 : renderError || fallback?.kind === 'error' ? 500 : request.responseStatus || 200,
|
|
278
|
+
rscError: Boolean(ordinaryRenderError(renderError)), boundaryIndex: fallback?.index, headers: request.responseHeaders });
|
|
279
|
+
await channel.write(value, abort.signal, Boolean(next.result?.done));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// The byte stream hands ownership of each Uint8Array to its reader.
|
|
283
|
+
// Retain those chunks until assembly instead of copying them again.
|
|
284
|
+
chunks.push(value);
|
|
285
|
+
}
|
|
286
|
+
} finally { reader.releaseLock(); }
|
|
287
|
+
if (abort.signal.aborted) throw abort.signal.reason;
|
|
288
|
+
if (cacheContext.staticState?.error) throw cacheContext.staticState.error;
|
|
289
|
+
let staticNavigation;
|
|
290
|
+
if (renderError) {
|
|
291
|
+
if (renderError.code === 'PRNEXT_DYNAMIC_SERVER_USAGE') throw renderError;
|
|
292
|
+
// After the shell is sent, React's encoded error/digest and the client
|
|
293
|
+
// navigation/error boundaries complete the response in-band.
|
|
294
|
+
if (channel?.started && renderError.statusCode !== 504) { channel.end(); return; }
|
|
295
|
+
if (renderError.digest === 'NEXT_HTTP_ERROR_FALLBACK;404' && !entry.routing) {
|
|
296
|
+
const boundary = (entry.segments || []).findLastIndex((segment, index) => index < (fallback?.index ?? Infinity) && segment.notFound);
|
|
297
|
+
if (boundary >= 0) return renderFlight({ ...request, cacheState: cacheContext.cacheState, staticState: cacheContext.staticState }, { kind: 'not-found', index: boundary }, channel);
|
|
298
|
+
} else if (ordinaryRenderError(renderError)) {
|
|
299
|
+
if (cacheContext.staticState) throw renderError;
|
|
300
|
+
// Preserve the original errored Flight tree. React's browser
|
|
301
|
+
// boundaries know the exact failing subtree; replaying the route
|
|
302
|
+
// here both repeats user effects and can select a child boundary
|
|
303
|
+
// for an exception actually thrown by its parent layout.
|
|
304
|
+
}
|
|
305
|
+
if (!ordinaryRenderError(renderError) && !(entry.routing && renderError.digest === 'NEXT_HTTP_ERROR_FALLBACK;404')) {
|
|
306
|
+
staticNavigation = cacheContext.staticState && navigationResponse(renderError);
|
|
307
|
+
if (!staticNavigation) throw renderError;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (channel) {
|
|
311
|
+
if (!channel.started) channel.start({ status: request.responseStatus || 200, headers: request.responseHeaders });
|
|
312
|
+
channel.end();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// A dedicated ArrayBuffer is transferable even for small responses; a
|
|
316
|
+
// pooled Node Buffer could otherwise clone its entire allocation pool.
|
|
317
|
+
const body = new Uint8Array(length);
|
|
318
|
+
let offset = 0;
|
|
319
|
+
for (const chunk of chunks) {
|
|
320
|
+
body.set(chunk, offset);
|
|
321
|
+
offset += chunk.byteLength;
|
|
322
|
+
}
|
|
323
|
+
return { body: body.buffer, status: staticNavigation?.status || (fallback?.kind === 'not-found' || (entry.routing && renderError?.digest === 'NEXT_HTTP_ERROR_FALLBACK;404') ? 404 : renderError || fallback?.kind === 'error' ? 500 : request.responseStatus || 200),
|
|
324
|
+
rscError: Boolean(ordinaryRenderError(renderError)), boundaryIndex: fallback?.index, headers: { ...request.responseHeaders, ...staticNavigation?.headers },
|
|
325
|
+
...(staticNavigation ? { navigation: { status: staticNavigation.status, body: String(staticNavigation.body || '') } } : {}),
|
|
326
|
+
...(cacheContext.staticState ? { staticMetadata: staticMetadata(cacheContext) } : {}) };
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (abort.signal.aborted && abort.signal.reason?.statusCode === 504) throw abort.signal.reason;
|
|
329
|
+
throw error;
|
|
330
|
+
} finally {
|
|
331
|
+
clearTimeout(timeout);
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
await flushCacheWork(cacheContext);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function handleRequest(request, channel = null) {
|
|
340
|
+
if (request.operation === 'static-params') {
|
|
341
|
+
return runRequestContext({ ...request, phase: 'render' }, async () => {
|
|
342
|
+
const context = currentRequest();
|
|
343
|
+
try { return { staticParams: await collectStaticParams(await import(pathToFileURL(request.modulePath).href), request.routePattern) }; }
|
|
344
|
+
finally { await flushCacheWork(context); }
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (!request.action) return renderFlight(request, null, channel);
|
|
348
|
+
const outcome = await runAction(request);
|
|
349
|
+
const responseHeaders = outcome.setCookies.length ? { 'set-cookie': outcome.setCookies } : {};
|
|
350
|
+
if (outcome.native && outcome.actionRedirect) {
|
|
351
|
+
return { body: new ArrayBuffer(0), status: 303, headers: { ...responseHeaders, location: outcome.actionRedirect.url } };
|
|
352
|
+
}
|
|
353
|
+
const { action, ...renderRequest } = request;
|
|
354
|
+
const actionModel = { actionResult: outcome.actionResult, actionError: outcome.actionError,
|
|
355
|
+
formState: outcome.formState, actionRedirect: outcome.actionRedirect };
|
|
356
|
+
if (renderRequest.routingRequestHeaders && outcome.setCookies.length) {
|
|
357
|
+
const original = new Headers(renderRequest.routingRequestHeaders);
|
|
358
|
+
const parsed = value => new Map((value || '').split(';').map(part => { const split = part.indexOf('='); return [part.slice(0, split).trim(), part.slice(split + 1).trim()]; }).filter(([name]) => name));
|
|
359
|
+
const cookies = parsed(original.get('cookie')), updated = parsed(new Headers(outcome.headers).get('cookie'));
|
|
360
|
+
for (const line of outcome.setCookies) {
|
|
361
|
+
const name = line.slice(0, line.indexOf('=')).trim();
|
|
362
|
+
if (updated.has(name)) cookies.set(name, updated.get(name)); else cookies.delete(name);
|
|
363
|
+
}
|
|
364
|
+
if (cookies.size) original.set('cookie', [...cookies].map(([name, value]) => `${name}=${value}`).join('; ')); else original.delete('cookie');
|
|
365
|
+
renderRequest.routingRequestHeaders = Object.fromEntries(original);
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
return await renderFlight({ ...renderRequest, mutableCookies: false, headers: outcome.headers,
|
|
369
|
+
temporaryReferences: outcome.temporaryReferences, responseHeaders,
|
|
370
|
+
responseStatus: outcome.failure ? 500 : 200,
|
|
371
|
+
actionNotFound: outcome.actionNotFound,
|
|
372
|
+
...(outcome.native && outcome.failure ? { fallbackError: outcome.failure } : {}),
|
|
373
|
+
...(outcome.actionRedirect ? { controlModel: actionModel } : { actionModel }),
|
|
374
|
+
}, null, channel);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
error.responseHeaders = responseHeaders;
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const streamChannels = new Map();
|
|
382
|
+
const activeRequests = new Set();
|
|
383
|
+
const cancelledRequests = new Set();
|
|
384
|
+
const manifests = new Map();
|
|
385
|
+
const artifacts = new Map();
|
|
386
|
+
parentPort.on('message', async ({ id, request, type, value }) => {
|
|
387
|
+
if (type === 'metadata') { manifests.set(id, value); return; }
|
|
388
|
+
if (type === 'metadata-drop') { manifests.delete(id); return; }
|
|
389
|
+
if (type === 'artifact') { artifacts.set(id, value); return; }
|
|
390
|
+
if (type === 'artifact-drop') { artifacts.delete(id); return; }
|
|
391
|
+
if (type === 'credit') { streamChannels.get(id)?.credit(); return; }
|
|
392
|
+
if (type === 'cancel') {
|
|
393
|
+
if (activeRequests.has(id)) cancelledRequests.add(id);
|
|
394
|
+
else parentPort.postMessage({ id, type: 'settled' });
|
|
395
|
+
streamChannels.get(id)?.cancel(); return;
|
|
396
|
+
}
|
|
397
|
+
for (const field of ['clientModules', 'actions']) {
|
|
398
|
+
if (request[field + 'Id'] !== undefined) request[field] = manifests.get(request[field + 'Id']);
|
|
399
|
+
}
|
|
400
|
+
const channel = request.stream && (!request.action || request.action.id) ? workerStreamChannel(parentPort, id) : null;
|
|
401
|
+
activeRequests.add(id);
|
|
402
|
+
if (channel) streamChannels.set(id, channel);
|
|
403
|
+
try {
|
|
404
|
+
if (request.partialFlightId !== undefined) {
|
|
405
|
+
// Resolve synchronously, before the next message can evict this ID.
|
|
406
|
+
if (!artifacts.has(request.partialFlightId)) throw new Error('Unknown partial Flight artefact');
|
|
407
|
+
request.partialFlight = artifacts.get(request.partialFlightId);
|
|
408
|
+
delete request.partialFlightId;
|
|
409
|
+
}
|
|
410
|
+
if (channel) {
|
|
411
|
+
await handleRequest({ ...request, renderKey: id }, channel);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const result = await handleRequest({ ...request, renderKey: id });
|
|
415
|
+
parentPort.postMessage({ id, ...result }, result.body instanceof ArrayBuffer ? [result.body] : []);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
parentPort.postMessage({ id, error: {
|
|
418
|
+
message: error?.message || String(error), stack: error?.stack, digest: error?.digest, statusCode: error?.statusCode,
|
|
419
|
+
retireWorker: error?.retireWorker, responseHeaders: error?.responseHeaders,
|
|
420
|
+
code: error?.code, staticMode: error?.staticMode, dynamicReason: error?.dynamicReason,
|
|
421
|
+
} });
|
|
422
|
+
} finally {
|
|
423
|
+
streamChannels.delete(id); activeRequests.delete(id);
|
|
424
|
+
if (cancelledRequests.delete(id)) parentPort.postMessage({ id, type: 'settled' });
|
|
425
|
+
}
|
|
426
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const ignored = new Set(['strategy', 'onLoad', 'onReady', 'onError', 'stylesheets', 'children', 'dangerouslySetInnerHTML']);
|
|
4
|
+
export function scriptJSON(value) {
|
|
5
|
+
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, char => ({ '<': '\\u003c', '>': '\\u003e', '&': '\\u0026', '\u2028': '\\u2028', '\u2029': '\\u2029' })[char]);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function scriptNonce(headers = {}) {
|
|
9
|
+
const get = name => Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
10
|
+
const policy = get('content-security-policy') || get('content-security-policy-report-only');
|
|
11
|
+
if (typeof policy !== 'string') return undefined;
|
|
12
|
+
const directives = policy.split(';').map(value => value.trim());
|
|
13
|
+
const selected = directives.find(value => value.startsWith('script-src')) || directives.find(value => value.startsWith('default-src'));
|
|
14
|
+
for (const value of selected?.split(/\s+/).slice(1) || []) {
|
|
15
|
+
const match = /^'nonce-([A-Za-z0-9+/_-]+={0,2})'$/.exec(value);
|
|
16
|
+
if (match) return match[1];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scriptElement(props, { nonce, crossOrigin, worker = false } = {}) {
|
|
21
|
+
const attributes = Object.fromEntries(Object.entries(props).filter(([key, value]) => !ignored.has(key) && typeof value !== 'function'));
|
|
22
|
+
const source = props.dangerouslySetInnerHTML ? props.dangerouslySetInnerHTML.__html || ''
|
|
23
|
+
: typeof props.children === 'string' ? props.children : Array.isArray(props.children) ? props.children.join('') : '';
|
|
24
|
+
return React.createElement('script', { ...attributes,
|
|
25
|
+
...(props.src ? { defer: worker ? undefined : props.defer ?? true } : { dangerouslySetInnerHTML: { __html: source } }),
|
|
26
|
+
nonce: worker || !props.src ? nonce : props.nonce || nonce, crossOrigin,
|
|
27
|
+
...(worker ? { type: 'text/partytown' } : {}), 'data-nscript': worker ? 'worker' : 'beforeInteractive',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Finalize after one Document render so scripts below NextScript are included. */
|
|
32
|
+
export function completePageScripts(markup, { pageScripts, documentScripts, worker, nonce, crossOrigin }, render) {
|
|
33
|
+
const all = [...pageScripts, ...documentScripts];
|
|
34
|
+
const before = all.filter(props => props.strategy === 'beforeInteractive');
|
|
35
|
+
const inline = before.filter(props => !props.src);
|
|
36
|
+
const external = before.filter(props => props.src);
|
|
37
|
+
const elements = [];
|
|
38
|
+
for (const props of external) elements.push(React.createElement('link', { rel: 'preload', as: 'script', href: props.src,
|
|
39
|
+
nonce, crossOrigin }));
|
|
40
|
+
elements.push(...inline.map(props => scriptElement(props, { nonce, crossOrigin })));
|
|
41
|
+
if (worker) {
|
|
42
|
+
if (!/\bdata-partytown-config(?:[\s=>])/i.test(markup)) elements.push(React.createElement('script', { nonce, 'data-partytown-config': '',
|
|
43
|
+
dangerouslySetInnerHTML: { __html: `partytown={lib:${scriptJSON(worker.lib)}};` } }));
|
|
44
|
+
elements.push(React.createElement('script', { nonce, 'data-partytown': '', dangerouslySetInnerHTML: { __html: worker.snippet } }));
|
|
45
|
+
elements.push(...pageScripts.filter(props => props.strategy === 'worker').map(props => scriptElement(props, { nonce, crossOrigin, worker: true })));
|
|
46
|
+
}
|
|
47
|
+
elements.push(...external.map(props => scriptElement(props, { nonce, crossOrigin })));
|
|
48
|
+
const html = elements.length ? render(React.createElement(React.Fragment, null, ...elements)) : '';
|
|
49
|
+
const target = '<prnext-document-script-target></prnext-document-script-target>';
|
|
50
|
+
// Head's children must run first: <base> affects relative script URLs, and a
|
|
51
|
+
// user Partytown configuration must be set before its bootstrap executes.
|
|
52
|
+
markup = markup.includes(target) ? markup.replace(target, () => html).replaceAll(target, '')
|
|
53
|
+
: markup.replace(/<\/head>/i, closing => html + closing);
|
|
54
|
+
const items = documentScripts.filter(props => props.strategy !== 'beforeInteractive');
|
|
55
|
+
if (items.length) {
|
|
56
|
+
const payload = render(React.createElement('script', { id: '__PRNEXT_SCRIPT_LOADER__', type: 'application/json', nonce,
|
|
57
|
+
dangerouslySetInnerHTML: { __html: scriptJSON(items) } }));
|
|
58
|
+
markup = markup.replace(/<\/body>/i, closing => payload + closing);
|
|
59
|
+
}
|
|
60
|
+
return markup;
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A read gets one settlement listener. Repeated Promise.race against a slow
|
|
2
|
+
// source otherwise accumulates one listener per chunk from the faster source.
|
|
3
|
+
// Callers keep at most one outstanding read per source and retain backpressure.
|
|
4
|
+
export function streamSelector() {
|
|
5
|
+
const ready = [];
|
|
6
|
+
let wake, closed = false;
|
|
7
|
+
function settle(item) {
|
|
8
|
+
if (closed) return;
|
|
9
|
+
ready.push(item);
|
|
10
|
+
wake?.(); wake = undefined;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
watch(type, promise) {
|
|
14
|
+
Promise.resolve(promise).then(value => settle({ type, ...value }), error => settle({ error }));
|
|
15
|
+
},
|
|
16
|
+
async next() {
|
|
17
|
+
while (!ready.length && !closed) await new Promise(resolve => { wake = resolve; });
|
|
18
|
+
if (closed) throw new Error('Stream selector closed');
|
|
19
|
+
const item = ready.shift();
|
|
20
|
+
if (Object.hasOwn(item, 'error')) throw item.error;
|
|
21
|
+
return item;
|
|
22
|
+
},
|
|
23
|
+
close() { closed = true; ready.length = 0; wake?.(); wake = undefined; },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export const STREAM_CHUNK_BYTES = 64 * 1024;
|
|
2
|
+
|
|
3
|
+
export function abortError(reason = 'Response body was canceled') {
|
|
4
|
+
if (reason instanceof Error) return reason;
|
|
5
|
+
const error = new Error(String(reason));
|
|
6
|
+
error.name = 'AbortError';
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function apiTimeoutError(phase, milliseconds) {
|
|
11
|
+
const error = new Error(`API ${phase} timed out after ${milliseconds / 1000} seconds`);
|
|
12
|
+
error.statusCode = 504;
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Await user code without leaving an abort listener attached after it settles. */
|
|
17
|
+
export function withSignal(promise, signal) {
|
|
18
|
+
if (!signal) return Promise.resolve(promise);
|
|
19
|
+
if (signal.aborted) {
|
|
20
|
+
Promise.resolve(promise).catch(() => {});
|
|
21
|
+
return Promise.reject(abortError(signal.reason));
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const aborted = () => reject(abortError(signal.reason));
|
|
25
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
26
|
+
Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener('abort', aborted));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pull one source chunk at a time, splitting it without copying its bytes.
|
|
32
|
+
* Explicit iterator methods also make return() cancel before the first next().
|
|
33
|
+
*/
|
|
34
|
+
export function streamingBody(source, { signal, onCancel, onComplete, timeoutMs = 25_000, runInContext = callback => callback() } = {}) {
|
|
35
|
+
const reader = source?.getReader?.();
|
|
36
|
+
const iterator = reader ? {
|
|
37
|
+
next: () => reader.read(),
|
|
38
|
+
return: reason => reader.cancel(reason),
|
|
39
|
+
} : source?.[Symbol.asyncIterator]?.();
|
|
40
|
+
if (!iterator) throw new TypeError('A streaming response requires a ReadableStream or async iterable');
|
|
41
|
+
let stopped = false;
|
|
42
|
+
let failure;
|
|
43
|
+
let chunk;
|
|
44
|
+
let offset = 0;
|
|
45
|
+
let timer;
|
|
46
|
+
let nextInProgress = false;
|
|
47
|
+
let rejectPending;
|
|
48
|
+
|
|
49
|
+
function clean() {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
signal?.removeEventListener('abort', aborted);
|
|
52
|
+
chunk = undefined;
|
|
53
|
+
if (reader) {
|
|
54
|
+
try { reader.releaseLock(); } catch { /* Cancellation may still be settling a pending read. */ }
|
|
55
|
+
}
|
|
56
|
+
onComplete?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stop(reason, error = false) {
|
|
60
|
+
if (stopped) return;
|
|
61
|
+
stopped = true;
|
|
62
|
+
if (error) failure = abortError(reason);
|
|
63
|
+
rejectPending?.(failure || abortError(reason));
|
|
64
|
+
// A user stream may never settle its cancel callback. Release our request
|
|
65
|
+
// immediately, while observing that callback's eventual rejection.
|
|
66
|
+
try { Promise.resolve(runInContext(() => iterator.return?.(reason))).catch(() => {}); } catch {}
|
|
67
|
+
try { onCancel?.(abortError(reason)); }
|
|
68
|
+
catch (error) { failure ||= error; }
|
|
69
|
+
finally { clean(); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function touch() {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
if (timeoutMs > 0) {
|
|
75
|
+
timer = setTimeout(() => stop(apiTimeoutError('response idle', timeoutMs), true), timeoutMs);
|
|
76
|
+
timer.unref?.();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function aborted() { stop(signal.reason, true); }
|
|
80
|
+
if (signal?.aborted) aborted();
|
|
81
|
+
else { signal?.addEventListener('abort', aborted, { once: true }); touch(); }
|
|
82
|
+
|
|
83
|
+
const body = {
|
|
84
|
+
[Symbol.asyncIterator]() { return this; },
|
|
85
|
+
async next() {
|
|
86
|
+
if (failure) throw failure;
|
|
87
|
+
if (stopped) return { done: true, value: undefined };
|
|
88
|
+
if (nextInProgress) throw new Error('Response body reads must be sequential');
|
|
89
|
+
nextInProgress = true;
|
|
90
|
+
try {
|
|
91
|
+
while (!chunk || offset >= chunk.byteLength) {
|
|
92
|
+
chunk = undefined;
|
|
93
|
+
const canceled = new Promise((_, reject) => { rejectPending = reject; });
|
|
94
|
+
const next = await Promise.race([runInContext(() => iterator.next()), canceled]);
|
|
95
|
+
rejectPending = undefined;
|
|
96
|
+
if (stopped) {
|
|
97
|
+
if (failure) throw failure;
|
|
98
|
+
return { done: true, value: undefined };
|
|
99
|
+
}
|
|
100
|
+
if (next.done) {
|
|
101
|
+
stopped = true;
|
|
102
|
+
clean();
|
|
103
|
+
return { done: true, value: undefined };
|
|
104
|
+
}
|
|
105
|
+
if (!(next.value instanceof Uint8Array)) throw new TypeError('Response body chunks must be Uint8Array values');
|
|
106
|
+
chunk = next.value;
|
|
107
|
+
offset = 0;
|
|
108
|
+
}
|
|
109
|
+
const value = chunk.subarray(offset, Math.min(offset + STREAM_CHUNK_BYTES, chunk.byteLength));
|
|
110
|
+
offset += value.byteLength;
|
|
111
|
+
touch();
|
|
112
|
+
return { done: false, value };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (stopped && !failure) return { done: true, value: undefined };
|
|
115
|
+
stop(error, true);
|
|
116
|
+
throw failure || error;
|
|
117
|
+
} finally { nextInProgress = false; rejectPending = undefined; }
|
|
118
|
+
},
|
|
119
|
+
async return(reason) {
|
|
120
|
+
stop(reason || abortError());
|
|
121
|
+
return { done: true, value: undefined };
|
|
122
|
+
},
|
|
123
|
+
async throw(error) { stop(error, true); throw error; },
|
|
124
|
+
cancel(reason) { return body.return(reason); },
|
|
125
|
+
};
|
|
126
|
+
return body;
|
|
127
|
+
}
|