@thomas.f/prnext 0.1.19-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 +46 -78
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/build-timings.mjs +38 -0
- package/build/bundle-runtime.mjs +17 -16
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +5 -4
- package/build/config.mjs +23 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/flight-summary.mjs +64 -0
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/index.mjs +57 -36
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +7 -2
- package/build/native-discovery.mjs +18 -6
- package/build/native-ppr.mjs +32 -0
- package/build/npm-packages.mjs +2 -2
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +108 -5
- package/build/standalone-copy.mjs +48 -0
- package/build/standalone.mjs +2 -22
- package/build/static-adapter.mjs +7 -3
- package/build/transform-cache.mjs +17 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/cli.mjs +27 -5
- package/compat/app-layout-context.cjs +7 -3
- package/compat/diagnostics.cjs +5 -4
- package/compat/flight-delta.cjs +47 -12
- package/compat/index.d.ts +24 -1
- package/compat/navigation-authorization.cjs +8 -1
- package/compat/runtime-sample.cjs +56 -0
- package/compat/use-cache.cjs +5 -2
- package/docs/ai/compatibility.md +113 -10
- package/docs/ai/deployment.md +144 -0
- package/docs/ai/quickstart.md +5 -0
- package/docs/ai/troubleshooting.md +22 -0
- package/package.json +25 -22
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +246 -54
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +42 -13
- package/runtime/app-stream-channel.mjs +20 -6
- package/runtime/app-stream-html.mjs +30 -11
- package/runtime/app-styles.mjs +22 -4
- package/runtime/cache-components-rsc.mjs +26 -6
- package/runtime/cache-result.mjs +58 -0
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +32 -8
- package/runtime/dev-compiler-broker.mjs +20 -23
- package/runtime/dev-gateway.mjs +23 -6
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/development-flight.mjs +59 -0
- package/runtime/development-queue.mjs +33 -0
- package/runtime/development.mjs +37 -16
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +31 -5
- 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 +88 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +5 -3
- package/runtime/persistent/common.mjs +7 -18
- package/runtime/persistent/daemon.mjs +4 -12
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/persistent/native-proxy.mjs +1 -1
- package/runtime/persistent/watchdog.mjs +12 -27
- package/runtime/prewarm.mjs +35 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/rsc-worker.mjs +58 -8
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +37 -17
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "@vercel/og",
|
|
4
|
+
"inputs": {
|
|
5
|
+
"sourceSha256": "3269eb15ca135a0eb7e07ddf8af0b35711d6e75c56b84c6a6727c5626d59409c",
|
|
6
|
+
"packages": {
|
|
7
|
+
"@vercel/og": {
|
|
8
|
+
"version": "1.0.3",
|
|
9
|
+
"integrity": "sha512-/iYJ34PDu4lty1TXdL86qHEw8viveHTllG1PWLuJX8tSFfqKL7kyVJeUORJmQ250bHB0h+alcalXUlza3c/NSA=="
|
|
10
|
+
},
|
|
11
|
+
"fflate": {
|
|
12
|
+
"version": "0.7.5",
|
|
13
|
+
"integrity": "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g=="
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"components": [
|
|
17
|
+
{
|
|
18
|
+
"name": "@vercel/og",
|
|
19
|
+
"version": "1.0.3"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "@resvg/resvg-wasm",
|
|
23
|
+
"version": "2.4.1"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "@shuding/opentype.js",
|
|
27
|
+
"version": "1.4.0-beta.0"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "base64-js",
|
|
31
|
+
"version": "0.0.8"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "camelize",
|
|
35
|
+
"version": "1.0.1"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "color-name",
|
|
39
|
+
"version": "1.1.4"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "css-background-parser",
|
|
43
|
+
"version": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "css-box-shadow",
|
|
47
|
+
"version": "1.0.0-3"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "css-color-keywords",
|
|
51
|
+
"version": "1.0.0"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "css-gradient-parser",
|
|
55
|
+
"version": "0.0.17"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "css-to-react-native",
|
|
59
|
+
"version": "3.2.0"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "escape-html",
|
|
63
|
+
"version": "1.0.3"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "fflate",
|
|
67
|
+
"version": "0.7.5"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "harfbuzzjs",
|
|
71
|
+
"version": "0.10.0"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "hex-rgb",
|
|
75
|
+
"version": "4.3.0"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "linebreak",
|
|
79
|
+
"version": "1.1.0"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "parse-css-color",
|
|
83
|
+
"version": "0.2.1"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "postcss-value-parser",
|
|
87
|
+
"version": "4.2.0"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "satori",
|
|
91
|
+
"version": "0.33.5"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "tiny-inflate",
|
|
95
|
+
"version": "1.0.3"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "unicode-trie",
|
|
99
|
+
"version": "2.0.0"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
"files": {
|
|
104
|
+
"Geist-Regular.ttf": "bde046ddd9f20be35b0bd56cc79eb752b967fb6661a3fe76cb067bb09f871d76",
|
|
105
|
+
"hb.wasm": "d2ab975172d6103e7624e8f7f03fea2316eca6fe562e1b80fc7f7058b283a6d0",
|
|
106
|
+
"index.node.mjs": "4fae6c6a5e3b6514b6a303bb2f9f5c945114aed0549724191a8ef127238e075b",
|
|
107
|
+
"LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
|
|
108
|
+
"LICENSE.fflate": "62249119bb7df6e54300f37a37145c26a6d4420410aac8881a119c5b427cf751",
|
|
109
|
+
"LICENSE.Geist": "c683bfbcc7e087f5d37a54ef628f10387c451a83ddc459b151403a164ac46c90",
|
|
110
|
+
"LICENSE.harfbuzzjs": "c85596583d1b7e4cfb4f21165ce0988aabd72e80a8c3a2ab6aaeedc1965e8288",
|
|
111
|
+
"LICENSE.satori": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
|
|
112
|
+
"NOTICE.txt": "bcc9a8f06053c81502b72a618ca004794027ec85ae94d4bbe81890f7dcdb8ec8",
|
|
113
|
+
"resvg.wasm": "9c1f8f54b842fcda3b72b06eba94a20b907958c443d73178da32b21ccb57b9bb"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { initializeHome, rpc, delay, environment, validName, validHealthPath } from './common.mjs';
|
|
6
6
|
import { resolveNativeBinary } from '../../native/resolve.mjs';
|
|
7
|
+
import {nativeHealthPath} from './health.mjs';
|
|
7
8
|
|
|
8
9
|
export const persistentHelp = `Persistent apps (ordinary prn start stays in the foreground):
|
|
9
10
|
prn pstart [directory] [options] Start a built app in the background
|
|
@@ -18,7 +19,8 @@ export const persistentHelp = `Persistent apps (ordinary prn start stays in the
|
|
|
18
19
|
|
|
19
20
|
pstart options: --name NAME --port 3000 --hostname 127.0.0.1 --workers 1
|
|
20
21
|
--profile balanced|speed|memory|classic --inspect
|
|
21
|
-
--health-path /
|
|
22
|
+
--health-path /api/health (optional application probe after the native readiness check)
|
|
23
|
+
--health-timeout 15000 --drain-timeout 30000 (milliseconds)
|
|
22
24
|
--rollback-window 15000 (post-switch monitoring; 0 disables, maximum 120000 ms)
|
|
23
25
|
prestart/pstop/plogs default to the app in the current directory.
|
|
24
26
|
plogs options: --lines 100 --follow. App environment comes from its .env files.
|
|
@@ -62,7 +64,7 @@ async function ensure(connection) {
|
|
|
62
64
|
child.on('error', () => {}); child.unref();
|
|
63
65
|
}
|
|
64
66
|
for (let i = 0; i < 200; i++) {
|
|
65
|
-
try { const result = await rpc(connection, { command: 'ping' }, 1500); if (result.version !==
|
|
67
|
+
try { const result = await rpc(connection, { command: 'ping' }, 1500); if (result.version !== 2) throw new Error('Supervisor protocol mismatch. Stop the supervisor with its current PRNext version before upgrading.'); if (result.ready) return; }
|
|
66
68
|
catch (error) { if (!['ECONNREFUSED', 'ECONNRESET'].includes(error.code)) throw error; }
|
|
67
69
|
await delay(100);
|
|
68
70
|
}
|
|
@@ -157,7 +159,7 @@ export async function persistentCommand(command, argv) {
|
|
|
157
159
|
if (command === 'pstart') {
|
|
158
160
|
const root = await realpath(path.resolve(target || '.'));
|
|
159
161
|
const name = validName(options.name || path.basename(root).replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^[^a-zA-Z0-9]+/, '').slice(0, 64));
|
|
160
|
-
request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] ||
|
|
162
|
+
request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] || nativeHealthPath, healthTimeout: options['health-timeout'] || 15000, drainTimeout: options['drain-timeout'] || 30000, rollbackWindow: options['rollback-window'] ?? 15000 } };
|
|
161
163
|
} else request = { command: ({ prestart: 'restart', pstop: 'stop', pdelete: 'delete' })[command], name: target, all: !!options.all, root: target || options.all ? undefined : await realpath(process.cwd()) };
|
|
162
164
|
await ensure(connection);
|
|
163
165
|
const report = await rpc(connection, request);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile, realpath, rm } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
4
|
-
import net from 'node:net';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { rename } from '../fs.mjs';
|
|
6
|
+
import { requestLoopback } from '../loopback-rpc.mjs';
|
|
7
7
|
|
|
8
8
|
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
9
9
|
export const homeDirectory = () => path.resolve(process.env.PRNEXT_PM_HOME || path.join(homedir(), '.prnext', 'processes'));
|
|
@@ -47,21 +47,10 @@ export function validHealthPath(value) {
|
|
|
47
47
|
return value;
|
|
48
48
|
}
|
|
49
49
|
export async function rpc(connection, request, timeout = 120000) {
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
socket.on('error', error => finish(error));
|
|
57
|
-
socket.once('connect', () => socket.write(JSON.stringify({ ...request, token: connection.token }) + '\n'));
|
|
58
|
-
socket.on('data', chunk => {
|
|
59
|
-
buffer += chunk.toString();
|
|
60
|
-
if (buffer.length > 1024 * 1024) return finish(new Error('Supervisor response is too large'));
|
|
61
|
-
if (!buffer.includes('\n')) return;
|
|
62
|
-
try { const result = JSON.parse(buffer.slice(0, buffer.indexOf('\n'))); finish(result.ok ? null : new Error(result.error || 'Supervisor request failed'), result.value); }
|
|
63
|
-
catch (error) { finish(error); }
|
|
64
|
-
});
|
|
65
|
-
socket.on('end', () => { if (!finished) finish(new Error('Supervisor disconnected')); });
|
|
66
|
-
});
|
|
50
|
+
try { return await requestLoopback(connection, request, { context: 'persistent-supervisor', timeout }); }
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === 'ERR_PRNEXT_PEER_AUTH') error.message = 'Cannot authenticate the supervisor control listener. If an older PRNext supervisor is running, stop it with that version\'s prn pdown before upgrading; saved apps are preserved.';
|
|
53
|
+
else if (error.message === 'Loopback request timed out') error.message = 'Supervisor request timed out. Check prn plogs --supervisor.';
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
67
56
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { request } from 'node:http';
|
|
3
2
|
import { cp, mkdir, readFile, rm, realpath } from 'node:fs/promises';
|
|
4
3
|
import { randomUUID, createHash } from 'node:crypto';
|
|
5
4
|
import path from 'node:path';
|
|
@@ -7,6 +6,8 @@ import { readBuildDirectory } from '../build-directory.mjs';
|
|
|
7
6
|
import { atomicJSON, delay, environment, validName, validHealthPath } from './common.mjs';
|
|
8
7
|
import { RotatingLog } from './logger.mjs';
|
|
9
8
|
import {nativeGateway,stopNativeProxy} from './native-proxy.mjs';
|
|
9
|
+
import {health} from './health.mjs';
|
|
10
|
+
import {productionEnvironment} from '../env.mjs';
|
|
10
11
|
|
|
11
12
|
const home = process.argv[2];
|
|
12
13
|
const registry = path.join(home, 'apps.json');
|
|
@@ -75,18 +76,9 @@ async function snapshot(app) {
|
|
|
75
76
|
if (path.relative(root, container).startsWith('..') || path.isAbsolute(path.relative(root, container))) throw new Error('Managed builds must remain inside the project');
|
|
76
77
|
await cp(source, destination, { recursive: true, verbatimSymlinks: true, filter: file => file !== path.join(source, 'standalone') });
|
|
77
78
|
if (await readFile(path.join(source, 'manifest.json'), 'utf8') !== before || await readFile(path.join(destination, 'manifest.json'), 'utf8') !== before) throw new Error('Build changed while copying. Finish prn build, then retry prn prestart.');
|
|
78
|
-
return { relative, buildId: manifest.buildId || null };
|
|
79
|
+
return { relative, buildId: manifest.buildId || null, server: manifest.config?.server || {} };
|
|
79
80
|
} catch (error) { await removeSnapshot(app, relative); throw error; }
|
|
80
81
|
}
|
|
81
|
-
function health(generation, config, timeout) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
const req = request({ host: '127.0.0.1', port: generation.port, path: config.healthPath, method: 'GET', headers: { host: `${config.hostname.includes(':') ? '[' + config.hostname + ']' : config.hostname}:${config.port}` }, signal: AbortSignal.timeout(timeout) }, res => {
|
|
84
|
-
const ok = res.statusCode >= 200 && res.statusCode < 400;
|
|
85
|
-
res.destroy(); ok ? resolve() : reject(new Error(`Health check ${config.healthPath} returned HTTP ${res.statusCode}`));
|
|
86
|
-
});
|
|
87
|
-
req.once('error', reject); req.end();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
82
|
async function stopGeneration(app, generation, drain = true) {
|
|
91
83
|
if (generation.stopping) return generation.stopping;
|
|
92
84
|
generation.expected = true;
|
|
@@ -123,7 +115,7 @@ async function launch(app) {
|
|
|
123
115
|
const build = await snapshot(app);
|
|
124
116
|
const child = spawn(config.binary, ['start', config.root, '--build-dir', build.relative, '--hostname', '127.0.0.1', '--port', '0', '--node', config.node, '--workers', String(config.workers), '--profile', config.profile, '--shutdown-on-stdin-eof'], {
|
|
125
117
|
cwd: config.root, windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
|
-
env: { ...environment(),
|
|
118
|
+
env: { ...productionEnvironment(config.root,{env:{...environment(),NODE_ENV:'production'}}), ...(config.inspect ? { PRNEXT_INSPECT_DIR: path.join(config.root, '.prnext-cache/inspect') } : {}) },
|
|
127
119
|
});
|
|
128
120
|
child.stdin.on('error', () => {});
|
|
129
121
|
const generation = { child, ...build, active: 0, dead: false, expected: false, port: null, startedAt: Date.now() };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {request} from 'node:http';
|
|
2
|
+
|
|
3
|
+
export const nativeHealthPath = '/_prnext/health';
|
|
4
|
+
const nativeLimit = 4096, applicationLimit = 65536;
|
|
5
|
+
|
|
6
|
+
// Connect directly to the private generation. A redirect, partial response or
|
|
7
|
+
// open-ended body cannot certify that a process is ready to receive traffic.
|
|
8
|
+
function probe(generation, config, healthPath, timeout, native) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
let finished = false, response;
|
|
11
|
+
const finish = error => {
|
|
12
|
+
if (finished) return;
|
|
13
|
+
finished = true;
|
|
14
|
+
if (error) { response?.destroy(); req.destroy(); reject(error); }
|
|
15
|
+
else resolve();
|
|
16
|
+
};
|
|
17
|
+
const req = request({host:'127.0.0.1', port:generation.port, path:healthPath,
|
|
18
|
+
method:'GET', headers:{host:`${config.hostname.includes(':')?'['+config.hostname+']':config.hostname}:${config.port}`, accept:'application/json', 'accept-encoding':'identity', connection:'close'},
|
|
19
|
+
signal:AbortSignal.timeout(Math.max(1, timeout))}, res => {
|
|
20
|
+
response = res;
|
|
21
|
+
if (res.statusCode !== 200) return finish(new Error(`Health check ${healthPath} returned HTTP ${res.statusCode}`));
|
|
22
|
+
if (res.headers['content-encoding'] && res.headers['content-encoding'] !== 'identity') return finish(new Error(`Health check ${healthPath} returned an encoded body`));
|
|
23
|
+
const limit = native ? nativeLimit : applicationLimit;
|
|
24
|
+
const declared = res.headers['content-length'];
|
|
25
|
+
if (declared !== undefined && (!/^\d+$/.test(declared) || Number(declared) > limit)) return finish(new Error(`Health check ${healthPath} body exceeds ${limit} bytes`));
|
|
26
|
+
const chunks = []; let size = 0;
|
|
27
|
+
res.on('data', chunk => {
|
|
28
|
+
size += chunk.length;
|
|
29
|
+
if (size > limit) return finish(new Error(`Health check ${healthPath} body exceeds ${limit} bytes`));
|
|
30
|
+
if (native) chunks.push(chunk);
|
|
31
|
+
});
|
|
32
|
+
res.once('error', finish);
|
|
33
|
+
res.once('aborted', () => finish(new Error(`Health check ${healthPath} response was incomplete`)));
|
|
34
|
+
res.once('end', () => {
|
|
35
|
+
if (!res.complete) return finish(new Error(`Health check ${healthPath} response was incomplete`));
|
|
36
|
+
if (native) {
|
|
37
|
+
if (!/^application\/json(?:\s*;|$)/i.test(res.headers['content-type'] || '')) return finish(new Error(`Health check ${healthPath} must return application/json`));
|
|
38
|
+
let body;
|
|
39
|
+
try { body = JSON.parse(Buffer.concat(chunks, size).toString('utf8')); } catch { return finish(new Error(`Health check ${healthPath} returned invalid JSON`)); }
|
|
40
|
+
if (!body || Array.isArray(body) || body.schemaVersion !== 1 || body.status !== 'ready') return finish(new Error(`Health check ${healthPath} did not report ready`));
|
|
41
|
+
if (body.buildId !== undefined && generation.buildId && body.buildId !== generation.buildId) return finish(new Error(`Health check ${healthPath} returned a different build`));
|
|
42
|
+
}
|
|
43
|
+
finish();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
req.once('error', finish); req.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function health(generation, config, timeout) {
|
|
51
|
+
const deadline = Date.now() + timeout;
|
|
52
|
+
await probe(generation, config, nativeHealthPath, timeout, true);
|
|
53
|
+
if (config.healthPath !== nativeHealthPath) {
|
|
54
|
+
const remaining = deadline - Date.now();
|
|
55
|
+
if (remaining <= 0) throw new Error('Application health check exceeded its deadline');
|
|
56
|
+
await probe(generation, config, config.healthPath, remaining, false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -53,7 +53,7 @@ export async function nativeGateway(config,options){
|
|
|
53
53
|
manager??=new NativeProxy(config.binary,options);await manager.ready;
|
|
54
54
|
const app=config.name;await manager.request('listen',{app,hostname:config.hostname,port:config.port});
|
|
55
55
|
return {pid:manager.pid,
|
|
56
|
-
activate:g=>manager.request('activate',{app,generation:g.relative,port:g.port}),
|
|
56
|
+
activate:g=>manager.request('activate',{app,generation:g.relative,port:g.port,server:g.server}),
|
|
57
57
|
pause:g=>manager.request('pause',{app,...(g?{generation:g.relative}:{})}),
|
|
58
58
|
retire:(g,timeout)=>manager.request('retire',{app,generation:g.relative,timeout},timeout+5000),
|
|
59
59
|
counts:async()=> (await manager.request('status',{app})).generations,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
2
|
import { createServer } from 'node:net';
|
|
3
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { initializeHome, environment, delay } from './common.mjs';
|
|
7
6
|
import { RotatingLog } from './logger.mjs';
|
|
7
|
+
import { serveLoopback } from '../loopback-rpc.mjs';
|
|
8
8
|
|
|
9
9
|
const connection = await initializeHome(process.argv[2]);
|
|
10
10
|
const log = new RotatingLog(path.join(connection.home, 'logs', 'supervisor.log'));
|
|
@@ -12,35 +12,20 @@ const pending = new Map(), children = new Set(), sockets = new Set();
|
|
|
12
12
|
const shutdownReplies = [];
|
|
13
13
|
let daemon, ready = false, closing = false, counter = 0, heartbeat = Date.now(), failures = 0, restartTimer;
|
|
14
14
|
const server = createServer(socket => {
|
|
15
|
-
socket.setEncoding('utf8');
|
|
16
15
|
sockets.add(socket); socket.once('close', () => sockets.delete(socket));
|
|
17
|
-
socket.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
socket.on('data', chunk => {
|
|
21
|
-
if (received) return;
|
|
22
|
-
buffer += chunk.toString();
|
|
23
|
-
if (buffer.length > 65536) { socket.destroy(); return; }
|
|
24
|
-
if (!buffer.includes('\n')) return;
|
|
25
|
-
received = true;
|
|
26
|
-
let message;
|
|
27
|
-
const reply = result => socket.end(JSON.stringify(result) + '\n');
|
|
28
|
-
try {
|
|
29
|
-
message = JSON.parse(buffer.slice(0, buffer.indexOf('\n')));
|
|
30
|
-
const supplied = Buffer.from(typeof message.token === 'string' ? message.token : '');
|
|
31
|
-
const expected = Buffer.from(connection.token);
|
|
32
|
-
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new Error('Invalid supervisor credentials');
|
|
33
|
-
delete message.token;
|
|
34
|
-
if (message.command === 'ping') { reply({ ok: true, value: { version: 1, pid: process.pid, daemonPid: daemon?.pid || null, ready } }); return; }
|
|
35
|
-
if (message.command === 'shutdown') { shutdownReplies.push(reply); void shutdown(); return; }
|
|
16
|
+
serveLoopback(socket, { token: connection.token, context: 'persistent-supervisor', handle: async message => {
|
|
17
|
+
if (message.command === 'ping') return { version: 2, pid: process.pid, daemonPid: daemon?.pid || null, ready };
|
|
18
|
+
if (message.command === 'shutdown') return new Promise(resolve => { shutdownReplies.push(resolve); void shutdown(); });
|
|
36
19
|
if (!ready || !daemon?.connected) throw new Error('Supervisor is recovering. Retry shortly.');
|
|
37
20
|
if (pending.size >= 32) throw new Error('Supervisor is busy. Retry shortly.');
|
|
38
21
|
const id = ++counter;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const reply = result => result.ok ? resolve(result.value) : reject(Error(result.error || 'Supervisor request failed'));
|
|
24
|
+
pending.set(id, reply);
|
|
25
|
+
socket.once('close', () => { pending.delete(id); reject(Error('Supervisor client disconnected')); });
|
|
26
|
+
daemon.send({ type: 'request', id, request: message }, error => { if (error) { pending.delete(id); reject(error); } });
|
|
27
|
+
});
|
|
28
|
+
} });
|
|
44
29
|
});
|
|
45
30
|
server.maxConnections = 64;
|
|
46
31
|
try { await new Promise((resolve, reject) => { server.once('error', reject); server.listen(connection.port, '127.0.0.1', resolve); }); }
|
|
@@ -100,7 +85,7 @@ async function shutdown() {
|
|
|
100
85
|
log.message('Supervisor stopped.');
|
|
101
86
|
await new Promise(resolve => log.end(resolve));
|
|
102
87
|
server.close();
|
|
103
|
-
for (const reply of shutdownReplies) reply(
|
|
88
|
+
for (const reply of shutdownReplies) reply('Supervisor stopped; enabled apps will return on the next launch.');
|
|
104
89
|
await delay(50);
|
|
105
90
|
for (const socket of sockets) socket.destroy();
|
|
106
91
|
process.exit(0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Framework preparation only: never import a route, render an app component or call
|
|
2
|
+
// application code. Imports cannot be unloaded or interrupted inside V8; the
|
|
3
|
+
// native parent bounds bootstrap and owns cancellation of the entire child.
|
|
4
|
+
export async function prewarmFramework({ routes, options, load, signal }, {
|
|
5
|
+
now = () => performance.now(), rss = () => process.memoryUsage().rss,
|
|
6
|
+
} = {}) {
|
|
7
|
+
if (!Number.isFinite(options?.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 30000
|
|
8
|
+
|| !Number.isFinite(options?.maxRssMb) || options.maxRssMb <= 0 || options.maxRssMb > 2048) {
|
|
9
|
+
throw new TypeError('Invalid framework prewarm budget');
|
|
10
|
+
}
|
|
11
|
+
const started = now(), deadline = started + options.timeoutMs, loaded = [];
|
|
12
|
+
const available = () => signal?.aborted ? 'cancelled'
|
|
13
|
+
: now() >= deadline ? 'timeout'
|
|
14
|
+
: rss() >= options.maxRssMb * 1024 * 1024 ? 'rss' : undefined;
|
|
15
|
+
const result = reason => ({ ready: !reason, reason, loaded, durationMs: Math.max(0, now() - started) });
|
|
16
|
+
const nodeRoutes = routes.filter(route => (route.cacheConfig?.runtime ?? route.handlerConfig?.runtime) !== 'edge');
|
|
17
|
+
const modules = [];
|
|
18
|
+
if (nodeRoutes.some(route => route.router === 'app' && route.kind === 'page')) modules.push('app-render');
|
|
19
|
+
if (nodeRoutes.some(route => route.router !== 'app' && route.kind === 'page')) modules.push('render');
|
|
20
|
+
if (nodeRoutes.some(route => route.kind === 'api')) modules.push('api');
|
|
21
|
+
for (const name of modules) {
|
|
22
|
+
let reason = available();
|
|
23
|
+
if (reason) return result(reason);
|
|
24
|
+
const module = await load(name);
|
|
25
|
+
reason = available();
|
|
26
|
+
if (reason) return result(reason);
|
|
27
|
+
if (name === 'app-render') {
|
|
28
|
+
await module.prewarmAppRenderer({ ...options, timeoutMs: Math.max(1, deadline - now()), signal });
|
|
29
|
+
reason = available();
|
|
30
|
+
if (reason) return result(reason);
|
|
31
|
+
}
|
|
32
|
+
loaded.push(name);
|
|
33
|
+
}
|
|
34
|
+
return result(available());
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdir, writeFile, rename, rm, realpath, lstat, open } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const LOCK = '.prnext-build.lock';
|
|
7
|
+
const busy = message => Object.assign(new Error(message), { code: 'PRNEXT_BUILD_LOCKED' });
|
|
8
|
+
function alive(pid) {
|
|
9
|
+
try { process.kill(pid, 0); return true; }
|
|
10
|
+
catch (error) { return error.code !== 'ESRCH'; }
|
|
11
|
+
}
|
|
12
|
+
async function owner(directory) {
|
|
13
|
+
const info = await lstat(directory);
|
|
14
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
|
|
15
|
+
const file = await open(path.join(directory, 'owner.json'), 'r');
|
|
16
|
+
try {
|
|
17
|
+
if ((await file.stat()).size > 4096) throw new Error('oversized owner');
|
|
18
|
+
const value = JSON.parse(await file.readFile('utf8'));
|
|
19
|
+
if (!Number.isSafeInteger(value.pid) || value.pid < 1 || typeof value.host !== 'string' || !/^[a-f0-9-]{36}$/.test(value.token)) throw new Error('invalid owner');
|
|
20
|
+
return value;
|
|
21
|
+
} finally { await file.close(); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One publisher per canonical project, covering React and direct static imports. */
|
|
25
|
+
export async function withBuildLock(directory, action, {development = false} = {}) {
|
|
26
|
+
const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
|
|
27
|
+
const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
|
|
28
|
+
const prepared = path.join(root, `.prnext-lock-${identity.token}`);
|
|
29
|
+
await mkdir(prepared, { mode: 0o700 });
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(path.join(prepared, 'owner.json'), JSON.stringify(identity), { flag: 'wx', mode: 0o600 });
|
|
32
|
+
for (let attempt = 0; ; attempt++) {
|
|
33
|
+
try { await rename(prepared, lock); break; }
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
36
|
+
let previous;
|
|
37
|
+
try { previous = await owner(lock); }
|
|
38
|
+
catch (cause) {
|
|
39
|
+
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
40
|
+
throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
|
|
41
|
+
}
|
|
42
|
+
if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
|
|
43
|
+
// Exactly one contender can recover this generation. Never delete a lock
|
|
44
|
+
// merely because it is old: long builds and suspended processes are valid.
|
|
45
|
+
let claim;
|
|
46
|
+
try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
|
|
47
|
+
catch (cause) {
|
|
48
|
+
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
49
|
+
throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
|
|
50
|
+
}
|
|
51
|
+
const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
|
|
52
|
+
try {
|
|
53
|
+
await claim.writeFile(JSON.stringify(identity)); await claim.close(); claim = null;
|
|
54
|
+
const current = await owner(lock);
|
|
55
|
+
if (current.token !== previous.token) {
|
|
56
|
+
// Do not unlink through this path: the new owner can release and
|
|
57
|
+
// another generation can appear between our read and the unlink.
|
|
58
|
+
throw busy('The build lock changed during recovery. Retry the build.');
|
|
59
|
+
}
|
|
60
|
+
await rename(lock, retired);
|
|
61
|
+
await rm(retired, { recursive: true });
|
|
62
|
+
} finally { await claim?.close(); }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
try { return await action(root); }
|
|
66
|
+
finally {
|
|
67
|
+
// Ownership must still match before releasing a lock.
|
|
68
|
+
if ((await owner(lock)).token === identity.token) await rm(lock, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
} finally { await rm(prepared, { recursive: true, force: true }); }
|
|
71
|
+
}
|
|
@@ -1,41 +1,121 @@
|
|
|
1
1
|
const MAX_HEADER = 256 * 1024;
|
|
2
2
|
const MAX_BODY = 32 * 1024 * 1024;
|
|
3
|
+
export const REQUEST_BODY_CHUNK = 64 * 1024;
|
|
3
4
|
const EMPTY_BODY = Buffer.alloc(0);
|
|
5
|
+
export const requestBodyState = Symbol('PRNext request body state');
|
|
4
6
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
while (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
// One owner reads a lane. Streaming bodies borrow this reader until their
|
|
8
|
+
// invocation finishes; body bytes are never parsed as request metadata.
|
|
9
|
+
function laneReader(source) {
|
|
10
|
+
const iterator = source[Symbol.asyncIterator]?.() || source[Symbol.iterator]();
|
|
11
|
+
let chunk = EMPTY_BODY, offset = 0, ended = false;
|
|
12
|
+
async function available() {
|
|
13
|
+
while (offset === chunk.length && !ended) {
|
|
14
|
+
const next = await iterator.next(); ended = !!next.done;
|
|
15
|
+
chunk = ended ? EMPTY_BODY : next.value; offset = 0;
|
|
16
|
+
if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
|
|
17
|
+
}
|
|
18
|
+
return !ended;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
async line(limit) {
|
|
22
|
+
const parts = []; let size = 0;
|
|
23
|
+
while (await available()) {
|
|
24
|
+
const end = chunk.indexOf(10, offset), stop = end < 0 ? chunk.length : end;
|
|
25
|
+
const part = chunk.subarray(offset, stop); size += part.length;
|
|
26
|
+
if (size > limit) throw new Error('Request transport header exceeds limit');
|
|
27
|
+
parts.push(part); offset = end < 0 ? stop : end + 1;
|
|
28
|
+
if (end >= 0) return (parts.length === 1 ? parts[0] : Buffer.concat(parts, size)).toString();
|
|
29
|
+
}
|
|
30
|
+
if (size) throw new Error('Truncated request transport header');
|
|
31
|
+
return undefined;
|
|
32
|
+
},
|
|
33
|
+
async exact(length) {
|
|
34
|
+
if (length === 0) return EMPTY_BODY;
|
|
35
|
+
const result = Buffer.allocUnsafe(length); let written = 0;
|
|
36
|
+
while (written < length) {
|
|
37
|
+
if (!await available()) throw new Error('Truncated request transport body');
|
|
38
|
+
const count = Math.min(length - written, chunk.length - offset);
|
|
39
|
+
chunk.copy(result, written, offset, offset + count); written += count; offset += count;
|
|
20
40
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
return result;
|
|
42
|
+
},
|
|
43
|
+
destroy() { source.destroy?.(); },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function streamingBody(reader, input, credit) {
|
|
48
|
+
const descriptor = input.bodyStream;
|
|
49
|
+
if (!descriptor || descriptor.version !== 1 || Object.keys(descriptor).some(key => !['version', 'length', 'maxBytes'].includes(key)) ||
|
|
50
|
+
descriptor.maxBytes !== MAX_BODY || !(descriptor.length === null || Number.isSafeInteger(descriptor.length) && descriptor.length >= 0 && descriptor.length <= MAX_BODY) ||
|
|
51
|
+
input.bodyLength !== undefined || input.body !== undefined || !Number.isSafeInteger(input.id) || input.id < 1 || ['GET', 'HEAD'].includes(input.method)) {
|
|
52
|
+
throw new Error('Invalid streaming request descriptor');
|
|
53
|
+
}
|
|
54
|
+
let controller, complete = false, total = 0, cancelled = false;
|
|
55
|
+
const state = { get complete() { return complete; }, get bytes() { return total; }, cancel() {
|
|
56
|
+
if (complete || cancelled) return;
|
|
57
|
+
cancelled = true;
|
|
58
|
+
try { controller?.error(new Error('Request body transport cancelled')); } catch {}
|
|
59
|
+
reader.destroy();
|
|
60
|
+
} };
|
|
61
|
+
const body = new ReadableStream({
|
|
62
|
+
start(value) { controller = value; },
|
|
63
|
+
async pull(value) {
|
|
64
|
+
try {
|
|
65
|
+
// A single explicit credit authorizes one frame. No speculative read
|
|
66
|
+
// consumes an upload while the application is not reading.
|
|
67
|
+
await credit({ id: input.id, type: 'bodycredit', length: REQUEST_BODY_CHUNK });
|
|
68
|
+
const header = await reader.exact(4), length = header.readUInt32BE();
|
|
69
|
+
if (length === 0xffffffff) {
|
|
70
|
+
const code = (await reader.exact(1))[0];
|
|
71
|
+
const kinds = { 1: ['PRNEXT_BODY_LIMIT', 413], 2: ['PRNEXT_BODY_TIMEOUT', 408], 3: ['PRNEXT_BODY_READ', 400], 4: ['PRNEXT_BODY_LENGTH', 400] };
|
|
72
|
+
if (!kinds[code]) throw new Error('Invalid request body error frame');
|
|
73
|
+
throw Object.assign(new Error('Request body transport failed'), { code: kinds[code][0], statusCode: kinds[code][1] });
|
|
74
|
+
}
|
|
75
|
+
if (length === 0) {
|
|
76
|
+
if (descriptor.length !== null && total !== descriptor.length) throw new Error('Request body length mismatch');
|
|
77
|
+
complete = true; value.close(); return;
|
|
78
|
+
}
|
|
79
|
+
if (length > REQUEST_BODY_CHUNK || total + length > MAX_BODY || descriptor.length !== null && total + length > descriptor.length) throw new Error('Request body exceeds its transport budget');
|
|
80
|
+
const bytes = await reader.exact(length); total += bytes.length; value.enqueue(bytes);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (cancelled) return;
|
|
83
|
+
value.error(error);
|
|
84
|
+
if (!/^PRNEXT_BODY_(LIMIT|TIMEOUT|READ|LENGTH)$/.test(error.code || '')) reader.destroy();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async cancel() { cancelled = true; await credit({ id: input.id, type: 'bodycancel' }); },
|
|
88
|
+
}, { highWaterMark: 0 });
|
|
89
|
+
Object.defineProperty(input, requestBodyState, { value: state });
|
|
90
|
+
return body;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function* requestFrames(source, { authenticate, streamBodies = false, credit } = {}) {
|
|
94
|
+
const reader = laneReader(source);
|
|
95
|
+
if (authenticate) {
|
|
96
|
+
const token = await reader.line(256);
|
|
97
|
+
if (token === undefined) throw new Error('Truncated request authentication');
|
|
98
|
+
authenticate(token);
|
|
99
|
+
}
|
|
100
|
+
for (;;) {
|
|
101
|
+
const line = await reader.line(MAX_HEADER);
|
|
102
|
+
if (line === undefined) return;
|
|
103
|
+
if (!line.trim()) continue;
|
|
104
|
+
const input = JSON.parse(line);
|
|
105
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Invalid request frame');
|
|
106
|
+
if (input.bodyStream !== undefined) {
|
|
107
|
+
if (!authenticate || !streamBodies || typeof credit !== 'function') throw new Error('Streaming request protocol was not negotiated');
|
|
108
|
+
input.body = streamingBody(reader, input, credit);
|
|
109
|
+
try { yield input; }
|
|
110
|
+
finally { input[requestBodyState].cancel(); }
|
|
111
|
+
// Streaming upload lanes are single-use: an early response cannot turn
|
|
112
|
+
// unread upload bytes into another request. The Node host stays warm.
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (input.bodyLength !== undefined) {
|
|
35
116
|
if (!Number.isSafeInteger(input.bodyLength) || input.bodyLength < 0 || input.bodyLength > MAX_BODY) throw new Error('Invalid request body length');
|
|
36
|
-
|
|
37
|
-
else body = Buffer.allocUnsafe(input.bodyLength);
|
|
117
|
+
input.body = await reader.exact(input.bodyLength);
|
|
38
118
|
}
|
|
119
|
+
yield input;
|
|
39
120
|
}
|
|
40
|
-
if (size || body || authenticate) throw new Error('Truncated request transport frame');
|
|
41
121
|
}
|