@thomas.f/prnext 0.1.1-alpha → 0.1.12-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/compat/index.d.ts CHANGED
@@ -112,6 +112,10 @@ export interface Rewrite extends CustomRoute { destination: string }
112
112
  export type ConfigRedirect = CustomRoute & Redirect;
113
113
  /** Implemented project options. Unsupported behavior-changing options fail the build. */
114
114
  export interface NextConfig {
115
+ /** PRNext only. CLI > environment > build configuration > profile.
116
+ * 0 disables the corresponding retirement; keepWarm retains one lane per used host.
117
+ */
118
+ workerRetirement?: { idleSeconds?: number; laneIdleSeconds?: number; keepWarm?: boolean };
115
119
  /** Next dev page-retention hints; PRNext builds all routes without that eviction queue. */
116
120
  onDemandEntries?: { maxInactiveAge?: number; pagesBufferLength?: number };
117
121
  i18n?: { locales: string[]; defaultLocale: string; localeDetection?: boolean; domains?: Array<{domain: string; defaultLocale: string; locales?: string[]; http?: boolean}> };
@@ -120,6 +124,12 @@ export interface NextConfig {
120
124
  images?: import('./image.cjs').ImageConfig;
121
125
  pageExtensions?: string[];
122
126
  reactStrictMode?: boolean;
127
+ /** Enabled React Compiler transforms are not implemented. */
128
+ reactCompiler?: false;
129
+ /** Typed route generation and validation are not implemented. */
130
+ typedRoutes?: false;
131
+ /** Production checks use the project's installed TypeScript without emitting files. */
132
+ typescript?: { ignoreBuildErrors?: boolean; tsconfigPath?: string };
123
133
  cacheComponents?: boolean;
124
134
  cacheLife?: Record<string, import('./cache.cjs').CacheLife>;
125
135
  transpilePackages?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thomas.f/prnext",
3
- "version": "0.1.1-alpha",
3
+ "version": "0.1.12-alpha",
4
4
  "license": "MIT",
5
5
  "description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
6
6
  "homepage": "https://prnext.dev",
@@ -17,14 +17,30 @@
17
17
  "name": "Thomas",
18
18
  "url": "https://github.com/Lorkydey"
19
19
  },
20
- "keywords": ["prnext", "nextjs", "react", "rust", "framework", "ssr", "server-components"],
20
+ "keywords": [
21
+ "prnext",
22
+ "nextjs",
23
+ "react",
24
+ "rust",
25
+ "framework",
26
+ "ssr",
27
+ "server-components"
28
+ ],
21
29
  "type": "module",
22
30
  "publishConfig": {
23
31
  "access": "public",
24
32
  "tag": "alpha",
25
33
  "registry": "https://registry.npmjs.org/"
26
34
  },
27
- "files": ["cli.mjs", "index.mjs", "build/", "runtime/", "compat/", "native/", "release/"],
35
+ "files": [
36
+ "cli.mjs",
37
+ "index.mjs",
38
+ "build/",
39
+ "runtime/",
40
+ "compat/",
41
+ "native/",
42
+ "release/"
43
+ ],
28
44
  "scripts": {
29
45
  "prepublishOnly": "node release/verify.mjs"
30
46
  },
@@ -117,12 +133,12 @@
117
133
  "react-server-dom-webpack": "19.3.0"
118
134
  },
119
135
  "optionalDependencies": {
120
- "prnext-darwin-arm64": "0.1.1-alpha",
121
- "prnext-darwin-x64": "0.1.1-alpha",
122
- "prnext-linux-arm64-gnu": "0.1.1-alpha",
123
- "prnext-linux-x64-gnu": "0.1.1-alpha",
124
- "@thomas.f/prnext-win32-arm64-msvc": "0.1.1-alpha",
125
- "@thomas.f/prnext-win32-x64-msvc": "0.1.1-alpha"
136
+ "prnext-darwin-arm64": "0.1.12-alpha",
137
+ "prnext-darwin-x64": "0.1.12-alpha",
138
+ "prnext-linux-arm64-gnu": "0.1.12-alpha",
139
+ "prnext-linux-x64-gnu": "0.1.12-alpha",
140
+ "@thomas.f/prnext-win32-x64-msvc": "0.1.12-alpha",
141
+ "@thomas.f/prnext-win32-arm64-msvc": "0.1.12-alpha"
126
142
  },
127
143
  "engines": {
128
144
  "node": ">=22"
@@ -26,6 +26,15 @@ export async function verifyPackage() {
26
26
  if (source) {
27
27
  const cargo = await readFile(path.join(source, 'crates/prnext/Cargo.toml'), 'utf8');
28
28
  if (!cargo.includes(`version = "${pkg.version}"`)) throw new Error('Cargo and npm versions differ. Update crates/prnext/Cargo.toml and Cargo.lock before building release binaries.');
29
+ const top = JSON.parse(await readFile(path.join(source, 'package.json'), 'utf8'));
30
+ const lock = JSON.parse(await readFile(path.join(source, 'package-lock.json'), 'utf8'));
31
+ const cargoLock = (await readFile(path.join(source, 'Cargo.lock'), 'utf8')).replaceAll('\r\n', '\n');
32
+ if ([top.version, lock.version, lock.packages[''].version, lock.packages['packages/prnext'].version].some(version => version !== pkg.version) ||
33
+ !cargoLock.includes(`name = "prnext"\nversion = "${pkg.version}"`)) throw new Error('Workspace and lockfile versions differ; run scripts/release-version.mjs before building.');
34
+ for (const target of platforms) {
35
+ const native = lock.packages[`node_modules/${target.package}`];
36
+ if (native?.version !== pkg.version || native.optional !== true || lock.packages['packages/prnext'].optionalDependencies[target.package] !== pkg.version) throw new Error(`Missing or stale native lock entry: ${target.package}`);
37
+ }
29
38
  }
30
39
  }
31
40
 
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import diagnostics from '../compat/diagnostics.cjs';
2
3
  import { runtimeProfile } from './profile.mjs';
3
4
  import { fontPreloads } from './font-preload.mjs';
4
5
  import { advancedRoutingSources } from './app-routing-state.mjs';
@@ -25,10 +26,14 @@ import { partialArtifact, partialShell } from './app-partial.mjs';
25
26
  import { partialKeyMap } from './app-partial-model.mjs';
26
27
  import { partialIdentity } from './partial-artifact-cache.mjs';
27
28
  import { isDraftRequest } from '../compat/draft.cjs';
29
+ import { createRscLifecycle } from './rsc-lifecycle.mjs';
30
+ import { trackRequestWork } from './request-work.mjs';
31
+ import { createRscWorkWatchdog } from './rsc-work-watchdog.mjs';
28
32
 
29
33
  installFetchCache();
30
34
 
31
35
  const workers = new Map();
36
+ const lifecycles = new WeakMap();
32
37
  const metadata = new WeakMap();
33
38
  const artifacts = new WeakMap();
34
39
  const edgeManifests = new WeakMap();
@@ -97,6 +102,7 @@ globalThis.__webpack_chunk_load__ = id => {
97
102
 
98
103
  function getWorker(production) {
99
104
  if (workers.has(production)) return workers.get(production);
105
+ const created = diagnostics.enabled ? performance.now() : 0;
100
106
  const current = new Worker(new URL('./rsc-worker.mjs', import.meta.url), {
101
107
  execArgv: ['--conditions=react-server'], stdout: true, stderr: true,
102
108
  // Production nursery policy; an explicit Node semi-space flag takes
@@ -105,37 +111,68 @@ function getWorker(production) {
105
111
  env: { ...process.env, NODE_ENV: production ? 'production' : 'development' },
106
112
  });
107
113
  workers.set(production, current);
114
+ if (diagnostics.enabled) {
115
+ current.once('online', () => diagnostics.record('wake', {phase:'rsc-online', durationMs:performance.now()-created}));
116
+ const ready = message => {
117
+ if (message.type !== 'rsc-ready') return;
118
+ diagnostics.record('wake', {phase:'rsc-ready', durationMs:performance.now()-created});
119
+ current.off('message', ready);
120
+ };
121
+ current.on('message', ready);
122
+ }
123
+ let workWatchdog;
124
+ const lifecycle = createRscLifecycle(() => { workWatchdog?.close(); return current.terminate(); });
125
+ workWatchdog = createRscWorkWatchdog({
126
+ probe: token => current.postMessage({ type: 'work-probe', token }),
127
+ retire: error => {
128
+ console.error('[prnext] RSC background worker stopped responding:', error);
129
+ return lifecycle.terminate(error);
130
+ },
131
+ });
132
+ lifecycles.set(current, lifecycle);
108
133
  metadata.set(current, workerMetadata(current));
109
134
  artifacts.set(current, workerArtifacts(current));
110
135
  // The HTTP worker's stdout belongs to its Rust transport. User code and React
111
136
  // diagnostics in the separate RSC environment must never corrupt that stream.
112
137
  current.stdout.pipe(process.stderr);
113
138
  current.stderr.pipe(process.stderr);
114
- function fail(error) {
115
- if (workers.get(production) === current) workers.delete(production);
139
+ function fail(error, exited = false) {
140
+ // Keep a retiring isolate registered until its actual exit; callers may
141
+ // wait for replacement but cannot start an unbounded series of retirees.
142
+ if (exited && workers.get(production) === current) workers.delete(production);
143
+ lifecycle.close(error);
144
+ workWatchdog.close();
116
145
  for (const [id, request] of pending) {
117
146
  if (request.worker !== current) continue;
118
147
  clearTimeout(request.timeout);
119
148
  pending.delete(id);
149
+ request.lease.release();
150
+ request.settle();
151
+ request.releaseWorkWatch?.();
120
152
  request.detach?.();
121
153
  request.channel?.error(error);
122
154
  request.reject(error);
123
155
  }
124
156
  }
125
- current.on('message', ({ id, type, chunk, byteOffset, byteLength, done, body, status, rscError, boundaryIndex, headers, error, staticParams, staticMetadata, navigation }) => {
157
+ current.on('message', ({ id, type, token, chunk, byteOffset, byteLength, done, body, status, rscError, boundaryIndex, headers, error, staticParams, staticMetadata, navigation }) => {
158
+ if (type === 'work-responsive') { workWatchdog.ack(token); return; }
126
159
  const request = pending.get(id);
127
160
  if (!request || request.worker !== current) return;
128
161
  // A timed-out mutation can still be running after its HTTP caller leaves.
129
162
  // Its retirement marker must be honored before discarding cancelled output.
130
163
  if (error?.retireWorker) {
131
- if (workers.get(production) === current) workers.delete(production);
132
- void current.terminate();
164
+ void lifecycle.terminate(new Error(error.message)).catch(error => console.error('[prnext] RSC worker termination failed:', error));
133
165
  }
134
166
  if (request.cancelled && type !== 'settled') return;
135
167
  if (type === 'settled') {
136
- if (!request.cancelled) return;
137
168
  pending.delete(id); clearTimeout(request.timeout);
138
- if (!hasPending(current)) current.unref();
169
+ request.lease.release();
170
+ request.detach?.();
171
+ request.settle();
172
+ request.releaseWorkWatch?.();
173
+ // terminate() keeps its exit event referenced. A late settlement must
174
+ // not undo that reference while build publication awaits shutdown.
175
+ if (!hasPending(current) && !lifecycle.retiring) current.unref();
139
176
  return;
140
177
  }
141
178
  if (type === 'start') {
@@ -145,11 +182,19 @@ function getWorker(production) {
145
182
  }
146
183
  if (type === 'chunk') {
147
184
  try { request.channel.chunk(chunk, byteOffset, byteLength); }
148
- catch (error) { request.channel.error(error); current.postMessage({ id, type: 'cancel' }); }
185
+ catch (error) { request.channel.error(error); current.postMessage({ id, type: 'cancel' }); request.cancel(error); }
149
186
  if (!done) return;
150
187
  }
151
- pending.delete(id);
188
+ if (request.completed) {
189
+ if (error) console.error('[prnext] RSC background work failed:', error.stack || error.message);
190
+ return;
191
+ }
192
+ request.completed = true;
193
+ // The response deadline ends with its successful terminal frame. Cache
194
+ // work can legitimately continue afterwards and retains its own bounds;
195
+ // supervise isolate responsiveness, not total background-task duration.
152
196
  clearTimeout(request.timeout);
197
+ request.releaseWorkWatch = workWatchdog.add();
153
198
  request.detach?.();
154
199
  if (type === 'end' || done) request.channel.end();
155
200
  else if (error) {
@@ -171,51 +216,71 @@ function getWorker(production) {
171
216
  // ArrayBuffer overload: a view of the transferred allocation, no copy.
172
217
  request.resolve({ body: Buffer.from(body), status: status || 200, rscError, boundaryIndex, headers, staticMetadata, navigation });
173
218
  }
174
- if (!hasPending(current)) current.unref();
219
+ // Keep only settlement bookkeeping after sending the body. The
220
+ // isolate can still be flushing cache writes after its terminal frame.
221
+ request.channel = undefined;
222
+ request.resolve = request.reject = () => {};
175
223
  });
176
224
  current.on('error', fail);
177
- current.on('exit', code => fail(new Error(`App Router worker exited (${code})`)));
225
+ current.on('exit', code => fail(new Error(`App Router worker exited (${code})`), true));
178
226
  current.unref();
179
227
  return current;
180
228
  }
181
229
 
182
230
  export function closeAppRuntime() {
183
231
  const current = [...workers.values()];
184
- workers.clear();
185
- return Promise.all(current.map(worker => worker.terminate()));
232
+ return Promise.all(current.map(worker => lifecycles.get(worker).terminate()));
186
233
  }
187
234
 
188
- export function renderFlight(request, { softTimeoutMs = 25_000, hardTimeoutMs = 28_000, signal } = {}) {
235
+ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeoutMs = 28_000, signal } = {}) {
236
+ const deadline = performance.now() + hardTimeoutMs;
237
+ let current, lifecycle, lease;
238
+ for (;;) {
239
+ signal?.throwIfAborted();
240
+ current = getWorker(Boolean(request.production));
241
+ lifecycle = lifecycles.get(current);
242
+ if (lifecycle.retiring) {
243
+ await lifecycle.waitForRetirement({ signal, timeoutMs: deadline - performance.now() });
244
+ continue;
245
+ }
246
+ try { lease = await lifecycle.acquire({ signal, timeoutMs: deadline - performance.now() }); }
247
+ catch (error) { if (lifecycle.retiring) continue; throw error; }
248
+ if (lifecycle.closed) { lease.release(); continue; }
249
+ break;
250
+ }
189
251
  return new Promise((resolve, reject) => {
190
- const current = getWorker(Boolean(request.production));
252
+ if (signal?.aborted) { lease.release(); reject(signal.reason); return; }
253
+ if (performance.now() >= deadline) { lease.release(); reject(appTimeoutError('RSC admission', hardTimeoutMs)); return; }
191
254
  current.ref();
192
255
  const id = ++nextId;
256
+ let settle;
257
+ trackRequestWork(new Promise(resolve => { settle = resolve; }), signal, { deadline, supervised: true });
193
258
  const timeout = setTimeout(() => {
194
259
  // A blocking npm dependency cannot honor an AbortSignal. Retire the
195
260
  // isolate at the hard deadline so subsequent requests can recover.
196
- if (workers.get(Boolean(request.production)) === current) workers.delete(Boolean(request.production));
197
- void current.terminate();
261
+ void lifecycle.terminate(appTimeoutError('RSC worker', hardTimeoutMs)).finally(settle).catch(error => console.error('[prnext] RSC worker termination failed:', error));
198
262
  pending.delete(id);
263
+ lease.release();
199
264
  const error = appTimeoutError('RSC worker', hardTimeoutMs);
265
+ if (entry.completed) console.error('[prnext] RSC background work failed:', error);
200
266
  entry.channel?.error(error);
201
267
  entry.detach?.();
202
268
  reject(error);
203
- }, hardTimeoutMs);
204
- const entry = { worker: current, resolve, reject, timeout, operation: request.operation };
269
+ }, Math.max(0, deadline - performance.now()));
270
+ const entry = { worker: current, resolve, reject, timeout, operation: request.operation, lease, settle };
205
271
  function cancel(reason) {
272
+ if (entry.cancelled) return;
206
273
  // Keep the independent deadline until the isolate confirms settlement.
207
274
  // A disconnected client must not disable recovery of blocked npm code.
208
275
  entry.cancelled = true;
276
+ lease.cancel();
209
277
  entry.detach?.();
210
- // HTTP callers can leave faster than arbitrary npm work can unwind.
211
- // Bound abandoned work independently of the admission of live requests.
212
- if (hasPending(current, 16, true)) {
213
- if (workers.get(Boolean(request.production)) === current) workers.delete(Boolean(request.production));
214
- void current.terminate();
215
- }
216
- if (!hasPending(current)) current.unref();
278
+ // Pause new work at 16 abandoned requests while their acknowledgements
279
+ // settle. Existing mutation/cache work keeps its original hard deadline.
280
+ if (!hasPending(current) && !lifecycle.retiring) current.unref();
217
281
  reject(reason instanceof Error ? reason : new Error('App Router response was cancelled'));
218
282
  }
283
+ entry.cancel = cancel;
219
284
  if (request.stream) entry.channel = parentStreamChannel(current, id, cancel);
220
285
  if (signal) {
221
286
  const onAbort = () => {
@@ -225,11 +290,10 @@ export function renderFlight(request, { softTimeoutMs = 25_000, hardTimeoutMs =
225
290
  };
226
291
  entry.detach = () => signal.removeEventListener('abort', onAbort);
227
292
  signal.addEventListener('abort', onAbort, { once: true });
228
- if (signal.aborted) { clearTimeout(timeout); onAbort(); return; }
229
293
  }
230
294
  pending.set(id, entry);
231
295
  try {
232
- const wire = { ...request, renderTimeoutMs: softTimeoutMs };
296
+ const wire = { ...request, renderTimeoutMs: Math.min(softTimeoutMs, Math.max(1, deadline - performance.now())) };
233
297
  if (request.production) {
234
298
  const artifactId = artifacts.get(current)(wire.partialFlight);
235
299
  if (artifactId !== undefined) { wire.partialFlightId = artifactId; delete wire.partialFlight; }
@@ -243,9 +307,11 @@ export function renderFlight(request, { softTimeoutMs = 25_000, hardTimeoutMs =
243
307
  catch (error) {
244
308
  clearTimeout(timeout);
245
309
  pending.delete(id);
310
+ lease.release();
311
+ settle();
246
312
  entry.detach?.();
247
313
  reject(error);
248
- if (!hasPending(current)) current.unref();
314
+ if (!hasPending(current) && !lifecycle.retiring) current.unref();
249
315
  }
250
316
  });
251
317
  }
@@ -1,3 +1,4 @@
1
+ import { htmlEnvelope } from './html-envelope.mjs';
1
2
  import React from 'react';
2
3
  import { fontPreloads } from './font-preload.mjs';
3
4
  import { isDynamicBailout } from '../compat/dynamic-bailout.cjs';
@@ -12,58 +13,6 @@ import { renderAppRecoveryShell } from './app-recovery.mjs';
12
13
  import { partialModel } from './app-partial-model.mjs';
13
14
  import { streamSelector } from './stream-select.mjs';
14
15
 
15
- // Do not insert bootstrap scripts inside a split HTML tag or raw-text element.
16
- // React may split a large inline script across arbitrary byte chunks.
17
- function htmlEnvelope({ resumed = false } = {}) {
18
- let pending = '';
19
- let closing = '';
20
- let received = 0;
21
- return {
22
- append(text) {
23
- received += Buffer.byteLength(text);
24
- if (received > MAX_RESPONSE_BYTES) throw new Error('HTML response exceeds the 16 MiB PRNext limit');
25
- pending += text;
26
- let ready = '';
27
- while (pending) {
28
- const start = pending.indexOf('<');
29
- if (start < 0) { ready += pending; pending = ''; break; }
30
- if (start) { ready += pending.slice(0, start); pending = pending.slice(start); }
31
- let end = -1;
32
- if (pending.startsWith('<!--')) {
33
- const comment = pending.indexOf('-->');
34
- if (comment >= 0) end = comment + 2;
35
- } else {
36
- let quote;
37
- for (let index = 1; index < pending.length; index++) {
38
- const char = pending[index];
39
- if (quote) { if (char === quote) quote = undefined; }
40
- else if (char === '"' || char === "'") quote = char;
41
- else if (char === '>') { end = index; break; }
42
- }
43
- }
44
- if (end < 0) break;
45
- const tag = pending.slice(0, end + 1);
46
- const raw = /^<(script|style|textarea|title|svg|math)(?:\s|>)/i.exec(tag);
47
- if (raw && !tag.endsWith('/>')) {
48
- const match = new RegExp(`</${raw[1]}\\s*>`, 'i').exec(pending.slice(end + 1));
49
- if (!match) break;
50
- end += 1 + match.index + match[0].length;
51
- ready += pending.slice(0, end); pending = pending.slice(end);
52
- } else {
53
- if (/^<\/(body|html)\s*>$/i.test(tag)) closing += tag;
54
- else ready += tag;
55
- pending = pending.slice(end + 1);
56
- }
57
- }
58
- return ready;
59
- },
60
- finish() {
61
- if (pending || !(resumed ? /^(?:<\/body><\/html>){1,2}$/i : /^<\/body><\/html>$/i).test(closing)) throw new Error('App Router root layout did not close its body and html elements');
62
- return resumed ? '</body></html>' : closing;
63
- },
64
- };
65
- }
66
-
67
16
  // The SSR decoder and HTML bootstrap share each Flight allocation. Unlike
68
17
  // ReadableStream.tee(), this tap stops reading when the HTML consumer stalls.
69
18
  // Before a document shell exists, at most the overall 16 MiB response limit can
@@ -0,0 +1,73 @@
1
+ import { MAX_RESPONSE_BYTES } from './http.mjs';
2
+
3
+ // Do not insert Flight/bootstrap scripts inside a split tag or raw-text element.
4
+ // Scan by index: ordinary tags stay in one contiguous slice, instead of creating
5
+ // a new tail and output string for every cell of a rendered table.
6
+ export function htmlEnvelope({ resumed = false } = {}) {
7
+ let pending = '';
8
+ let closing = '';
9
+ let received = 0;
10
+ const rawStart = /<(script|style|textarea|title|svg|math)(?:\s|>)/iy;
11
+ const rawEnds = new Map();
12
+ const documentEnd = /<\/(body|html)\s*>/iy;
13
+ return {
14
+ append(text) {
15
+ received += Buffer.byteLength(text);
16
+ if (received > MAX_RESPONSE_BYTES) throw new Error('HTML response exceeds the 16 MiB PRNext limit');
17
+ pending += text;
18
+ let cursor = 0, output = 0, ready = '';
19
+ while (cursor < pending.length) {
20
+ const start = pending.indexOf('<', cursor);
21
+ if (start < 0) { cursor = pending.length; break; }
22
+ cursor = start;
23
+ let end = -1;
24
+ if (pending.startsWith('<!--', start)) {
25
+ const comment = pending.indexOf('-->', start);
26
+ if (comment >= 0) end = comment + 2;
27
+ } else {
28
+ let quote;
29
+ for (let index = start + 1; index < pending.length; index++) {
30
+ const char = pending[index];
31
+ if (quote) { if (char === quote) quote = undefined; }
32
+ else if (char === '"' || char === "'") quote = char;
33
+ else if (char === '>') { end = index; break; }
34
+ }
35
+ }
36
+ if (end < 0) break;
37
+ // Most tags need no special handling. Reject their ASCII prefixes
38
+ // before running the raw-element and document-closing expressions;
39
+ // the expressions still validate every candidate in full.
40
+ const first = pending.charCodeAt(start + 1) | 32;
41
+ const second = pending.charCodeAt(start + 2) | 32;
42
+ const rawCandidate = first === 115 && (second === 99 || second === 116 || second === 118) ||
43
+ first === 116 && (second === 101 || second === 105) || first === 109 && second === 97;
44
+ rawStart.lastIndex = start;
45
+ const raw = rawCandidate && rawStart.exec(pending);
46
+ if (raw && pending[end - 1] !== '/') {
47
+ const name = raw[1].toLowerCase();
48
+ let pattern = rawEnds.get(name);
49
+ if (!pattern) { pattern = new RegExp(`</${name}\\s*>`, 'ig'); rawEnds.set(name, pattern); }
50
+ pattern.lastIndex = end + 1;
51
+ const match = pattern.exec(pending);
52
+ if (!match) break;
53
+ cursor = pattern.lastIndex;
54
+ } else {
55
+ documentEnd.lastIndex = start;
56
+ if (first === 47 && (second === 98 || second === 104) && documentEnd.test(pending) && documentEnd.lastIndex === end + 1) {
57
+ ready += pending.slice(output, start);
58
+ closing += pending.slice(start, end + 1);
59
+ output = end + 1;
60
+ }
61
+ cursor = end + 1;
62
+ }
63
+ }
64
+ ready += pending.slice(output, cursor);
65
+ pending = pending.slice(cursor);
66
+ return ready;
67
+ },
68
+ finish() {
69
+ if (pending || !(resumed ? /^(?:<\/body><\/html>){1,2}$/i : /^<\/body><\/html>$/i).test(closing)) throw new Error('App Router root layout did not close its body and html elements');
70
+ return resumed ? '</body></html>' : closing;
71
+ },
72
+ };
73
+ }
package/runtime/http.mjs CHANGED
@@ -184,7 +184,7 @@ export function plainResponse(status, message) {
184
184
 
185
185
  export function errorResponse(error, production = process.env.NODE_ENV === 'production') {
186
186
  console.error('[prnext]', error?.stack || error);
187
- const status = [400, 504].includes(error?.statusCode) ? error.statusCode : 500;
188
- const message = status === 400 ? 'Bad Request' : status === 504 ? 'Gateway Timeout' : 'Internal Server Error';
187
+ const status = [400, 503, 504].includes(error?.statusCode) ? error.statusCode : 500;
188
+ const message = status === 400 ? 'Bad Request' : status === 503 ? 'Service Unavailable' : status === 504 ? 'Gateway Timeout' : 'Internal Server Error';
189
189
  return plainResponse(status, production || status === 400 ? message : String(error?.stack || error));
190
190
  }
@@ -6,7 +6,8 @@
6
6
  "renderStarts": 16,
7
7
  "liveResponses": 32,
8
8
  "apiConcurrency": 512,
9
- "responseBufferMiB": 8
9
+ "responseBufferMiB": 8,
10
+ "renderIdleSeconds": null
10
11
  },
11
12
  "compact": {
12
13
  "optimizeForSize": true,
@@ -15,16 +16,18 @@
15
16
  "renderStarts": 16,
16
17
  "liveResponses": 32,
17
18
  "apiConcurrency": 512,
18
- "responseBufferMiB": 8
19
+ "responseBufferMiB": 8,
20
+ "renderIdleSeconds": 30
19
21
  },
20
22
  "balanced": {
21
- "optimizeForSize": true,
22
- "semiSpaceMiB": 8,
23
+ "optimizeForSize": false,
24
+ "semiSpaceMiB": 4,
23
25
  "youngGenerationMiB": 16,
24
26
  "renderStarts": 16,
25
27
  "liveResponses": 32,
26
28
  "apiConcurrency": 256,
27
- "responseBufferMiB": 4
29
+ "responseBufferMiB": 4,
30
+ "renderIdleSeconds": 300
28
31
  },
29
32
  "speed": {
30
33
  "optimizeForSize": false,
@@ -33,15 +36,17 @@
33
36
  "renderStarts": 32,
34
37
  "liveResponses": 128,
35
38
  "apiConcurrency": 512,
36
- "responseBufferMiB": 16
39
+ "responseBufferMiB": 16,
40
+ "renderIdleSeconds": null
37
41
  },
38
42
  "memory": {
39
43
  "optimizeForSize": true,
40
44
  "semiSpaceMiB": 2,
41
45
  "youngGenerationMiB": 4,
42
46
  "renderStarts": 8,
43
- "liveResponses": 16,
47
+ "liveResponses": 28,
44
48
  "apiConcurrency": 64,
45
- "responseBufferMiB": 1
49
+ "responseBufferMiB": 1,
50
+ "renderIdleSeconds": 30
46
51
  }
47
52
  }
@@ -1,7 +1,7 @@
1
1
  // Track the actual application promise as well as the HTTP wrapper. A wrapper
2
2
  // can reject on abort while npm code keeps running and retaining its context.
3
3
  export const requestWork = Symbol.for('prnext.request-work');
4
- export function trackRequestWork(promise, signal) {
5
- signal?.[requestWork]?.(promise);
4
+ export function trackRequestWork(promise, signal, options) {
5
+ signal?.[requestWork]?.(promise, options);
6
6
  return promise;
7
7
  }