@thomas.f/prnext 0.1.16-alpha → 0.1.18-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.
Files changed (215) hide show
  1. package/AI.md +63 -0
  2. package/README.md +35 -503
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +6 -1
  5. package/build/analyze.mjs +340 -0
  6. package/build/app-config.mjs +20 -8
  7. package/build/app-static.mjs +41 -14
  8. package/build/app-styles.mjs +79 -0
  9. package/build/app.mjs +99 -31
  10. package/build/build-lock.mjs +6 -6
  11. package/build/bundle-runtime.mjs +39 -11
  12. package/build/cache-components.mjs +1 -1
  13. package/build/cache-handlers.mjs +1 -1
  14. package/build/check.mjs +56 -11
  15. package/build/compact-css.mjs +53 -0
  16. package/build/compilation-cache.mjs +269 -0
  17. package/build/compilation-store.mjs +142 -0
  18. package/build/compiler.mjs +51 -4
  19. package/build/concurrent.mjs +12 -0
  20. package/build/config.mjs +50 -13
  21. package/build/create.mjs +239 -0
  22. package/build/css-modules.mjs +65 -10
  23. package/build/deployment-assets.mjs +89 -0
  24. package/build/deployment-config.mjs +18 -0
  25. package/build/dev-compiler-health.mjs +11 -0
  26. package/build/dev-css-update.mjs +85 -0
  27. package/build/dev-dependencies.mjs +44 -0
  28. package/build/dev-discovery.mjs +28 -0
  29. package/build/dev-worker.mjs +14 -5
  30. package/build/dev.mjs +64 -6
  31. package/build/dynamic.mjs +1 -1
  32. package/build/edge.mjs +5 -4
  33. package/build/fonts.mjs +1 -1
  34. package/build/framework-imports.mjs +50 -0
  35. package/build/html-only-config.mjs +53 -0
  36. package/build/html-only-graph.mjs +67 -0
  37. package/build/incremental.mjs +8 -4
  38. package/build/index.mjs +140 -112
  39. package/build/inspect.mjs +3 -0
  40. package/build/inspection-summary.mjs +37 -4
  41. package/build/instrumentation.mjs +30 -0
  42. package/build/loader-cache.mjs +53 -26
  43. package/build/metadata-routes.mjs +3 -4
  44. package/build/migrate.mjs +60 -57
  45. package/build/migration-audit.mjs +1 -1
  46. package/build/native-type-config.mjs +37 -0
  47. package/build/npm-packages.mjs +3 -18
  48. package/build/package-manager.mjs +308 -0
  49. package/build/persistent-cache.mjs +112 -0
  50. package/build/precompress.mjs +62 -30
  51. package/build/prerender-cache.mjs +70 -0
  52. package/build/prerender-config.mjs +29 -0
  53. package/build/prerender-pages.mjs +100 -0
  54. package/build/prerender-pool.mjs +165 -0
  55. package/build/prerender-process-worker.mjs +58 -0
  56. package/build/prerender-worker.mjs +10 -0
  57. package/build/production-builder.mjs +129 -0
  58. package/build/react-compiler-report.mjs +225 -0
  59. package/build/react-compiler.mjs +139 -0
  60. package/build/runtime-contract.mjs +2 -2
  61. package/build/scan.mjs +4 -4
  62. package/build/source-analysis.mjs +42 -0
  63. package/build/static-adapter.mjs +13 -5
  64. package/build/static-export.mjs +4 -4
  65. package/build/static-input.mjs +48 -0
  66. package/build/styles.mjs +17 -3
  67. package/build/transform-cache.mjs +37 -4
  68. package/build/transform.mjs +1 -1
  69. package/build/type-command.mjs +41 -0
  70. package/build/typed-routes.mjs +95 -0
  71. package/build/typescript-dev-worker.mjs +18 -0
  72. package/build/typescript-dev.mjs +49 -0
  73. package/build/typescript-native-worker.mjs +55 -0
  74. package/build/typescript-native.mjs +18 -0
  75. package/build/typescript-toolchain.mjs +19 -0
  76. package/build/typescript-watch.mjs +22 -0
  77. package/build/typescript.mjs +11 -8
  78. package/build/webpack-graph.mjs +170 -30
  79. package/build/worker-memory.mjs +9 -0
  80. package/build/worker-preload.mjs +9 -0
  81. package/cli.mjs +162 -122
  82. package/compat/activity-cache.cjs +26 -0
  83. package/compat/after.cjs +6 -4
  84. package/compat/app-layout-context.cjs +52 -2
  85. package/compat/base64.cjs +10 -0
  86. package/compat/cache-handlers.cjs +12 -2
  87. package/compat/cache-wire.cjs +28 -0
  88. package/compat/cache.cjs +2 -1
  89. package/compat/cache.d.cts +4 -0
  90. package/compat/cookies.cjs +21 -5
  91. package/compat/data-cache.cjs +12 -8
  92. package/compat/flight-delta.cjs +150 -0
  93. package/compat/headers.cjs +4 -2
  94. package/compat/incremental-cache.cjs +20 -7
  95. package/compat/index.d.ts +89 -10
  96. package/compat/link.cjs +12 -9
  97. package/compat/link.d.cts +4 -3
  98. package/compat/navigation-authorization.cjs +9 -0
  99. package/compat/navigation-stages.cjs +19 -0
  100. package/compat/navigation.d.cts +4 -3
  101. package/compat/offline-store.cjs +11 -0
  102. package/compat/offline.cjs +5 -0
  103. package/compat/offline.d.cts +2 -0
  104. package/compat/redis-cache.cjs +201 -0
  105. package/compat/redis-cache.d.cts +37 -0
  106. package/compat/router.d.cts +4 -3
  107. package/compat/server-primitives.cjs +31 -11
  108. package/compat/server.d.cts +15 -2
  109. package/compat/static-generation.cjs +7 -0
  110. package/compat/typed-route.d.cts +17 -0
  111. package/compat/use-cache.cjs +3 -0
  112. package/docs/ai/compatibility.md +275 -0
  113. package/docs/ai/data.md +288 -0
  114. package/docs/ai/deployment.md +269 -0
  115. package/docs/ai/manifest.json +11 -0
  116. package/docs/ai/quickstart.md +171 -0
  117. package/docs/ai/routing.md +214 -0
  118. package/docs/ai/troubleshooting.md +155 -0
  119. package/index.mjs +2 -0
  120. package/native/diagnostics.mjs +34 -0
  121. package/native/resolve.mjs +7 -2
  122. package/package.json +44 -7
  123. package/release/ai-docs.mjs +48 -0
  124. package/release/verify.mjs +2 -0
  125. package/runtime/action-client.mjs +10 -4
  126. package/runtime/action-server.mjs +2 -0
  127. package/runtime/ai-docs.mjs +202 -0
  128. package/runtime/app-client.mjs +226 -68
  129. package/runtime/app-content.mjs +4 -3
  130. package/runtime/app-partial-client.mjs +3 -0
  131. package/runtime/app-partial.mjs +1 -1
  132. package/runtime/app-prefetch-constants.mjs +4 -0
  133. package/runtime/app-prefetch.mjs +2 -3
  134. package/runtime/app-render-key.mjs +5 -0
  135. package/runtime/app-render.mjs +100 -28
  136. package/runtime/app-routing.mjs +22 -9
  137. package/runtime/app-static.mjs +18 -8
  138. package/runtime/app-stream-html.mjs +10 -53
  139. package/runtime/app-style-selection.mjs +34 -0
  140. package/runtime/app-styles.mjs +154 -0
  141. package/runtime/cache-components-rsc.mjs +4 -1
  142. package/runtime/cache-request.mjs +2 -1
  143. package/runtime/cloud/archive.mjs +5 -6
  144. package/runtime/compile-cache.mjs +8 -0
  145. package/runtime/deployment-client.mjs +19 -0
  146. package/runtime/dev-ai-brands.mjs +26 -0
  147. package/runtime/dev-ai-style.mjs +99 -0
  148. package/runtime/dev-build-reasons.mjs +25 -0
  149. package/runtime/dev-cache-control.mjs +53 -0
  150. package/runtime/dev-client.mjs +245 -46
  151. package/runtime/dev-compiler-broker.mjs +42 -0
  152. package/runtime/dev-console-copy.mjs +19 -0
  153. package/runtime/dev-console.mjs +106 -0
  154. package/runtime/dev-dependency-graph.mjs +40 -0
  155. package/runtime/dev-element-picker-style.mjs +27 -0
  156. package/runtime/dev-element-picker.mjs +213 -0
  157. package/runtime/dev-error-overlay-style.mjs +41 -0
  158. package/runtime/dev-error-overlay.mjs +167 -0
  159. package/runtime/dev-error-report.mjs +121 -0
  160. package/runtime/dev-file-changes.mjs +1 -1
  161. package/runtime/dev-gateway.mjs +44 -0
  162. package/runtime/dev-host-relay.mjs +66 -0
  163. package/runtime/dev-indicator.mjs +191 -0
  164. package/runtime/dev-issue-motion.mjs +214 -0
  165. package/runtime/dev-lazy-tool.mjs +34 -0
  166. package/runtime/dev-logo.mjs +2 -0
  167. package/runtime/dev-notification.mjs +81 -0
  168. package/runtime/dev-popout.mjs +34 -0
  169. package/runtime/dev-position.mjs +107 -0
  170. package/runtime/dev-preview-touch.mjs +122 -0
  171. package/runtime/dev-runtime-pool.mjs +45 -0
  172. package/runtime/dev-runtime-spare.mjs +57 -0
  173. package/runtime/dev-tools-client.mjs +155 -0
  174. package/runtime/dev-tools-mcp.mjs +126 -0
  175. package/runtime/dev-tools-metrics.mjs +31 -0
  176. package/runtime/dev-tools-model.mjs +42 -0
  177. package/runtime/dev-tools-panel.mjs +341 -0
  178. package/runtime/dev-tools-result.mjs +37 -0
  179. package/runtime/dev-tools-server.mjs +256 -0
  180. package/runtime/dev-tools-source.mjs +104 -0
  181. package/runtime/dev-tooltip.mjs +94 -0
  182. package/runtime/dev-ui-icons.mjs +8 -0
  183. package/runtime/dev-workbench-style.mjs +106 -0
  184. package/runtime/dev-workbench.mjs +428 -0
  185. package/runtime/dev-worker-logs.mjs +31 -0
  186. package/runtime/development.mjs +324 -0
  187. package/runtime/document.mjs +1 -1
  188. package/runtime/edge.mjs +6 -5
  189. package/runtime/env.mjs +2 -1
  190. package/runtime/fetch-cache.mjs +10 -8
  191. package/runtime/flight-delta.mjs +50 -0
  192. package/runtime/flight-tap.mjs +28 -0
  193. package/runtime/html-only.mjs +27 -0
  194. package/runtime/http.mjs +2 -0
  195. package/runtime/incremental-cache.mjs +25 -4
  196. package/runtime/instrumentation.mjs +49 -0
  197. package/runtime/navigation-recovery.mjs +36 -0
  198. package/runtime/navigation.mjs +7 -0
  199. package/runtime/node-html-stream.mjs +47 -0
  200. package/runtime/pages-client.mjs +24 -8
  201. package/runtime/pages-navigation.mjs +54 -64
  202. package/runtime/persistent/client.mjs +6 -5
  203. package/runtime/persistent/daemon.mjs +66 -56
  204. package/runtime/persistent/native-proxy.mjs +63 -0
  205. package/runtime/prefetch-scheduler.mjs +110 -0
  206. package/runtime/preload.mjs +12 -0
  207. package/runtime/profile.mjs +14 -0
  208. package/runtime/profiles.json +2 -2
  209. package/runtime/render.mjs +3 -0
  210. package/runtime/route-pattern.mjs +44 -0
  211. package/runtime/rsc-worker.mjs +41 -12
  212. package/runtime/trace.mjs +78 -0
  213. package/runtime/typed-route.mjs +19 -0
  214. package/runtime/watch-project.mjs +29 -18
  215. package/runtime/worker.mjs +102 -24
@@ -1,11 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { Agent, createServer, request } from 'node:http';
2
+ import { request } from 'node:http';
3
3
  import { cp, mkdir, readFile, rm, realpath } from 'node:fs/promises';
4
4
  import { randomUUID, createHash } from 'node:crypto';
5
5
  import path from 'node:path';
6
6
  import { readBuildDirectory } from '../build-directory.mjs';
7
7
  import { atomicJSON, delay, environment, validName, validHealthPath } from './common.mjs';
8
8
  import { RotatingLog } from './logger.mjs';
9
+ import {nativeGateway,stopNativeProxy} from './native-proxy.mjs';
9
10
 
10
11
  const home = process.argv[2];
11
12
  const registry = path.join(home, 'apps.json');
@@ -16,6 +17,7 @@ const send = message => { if (process.connected) process.send(message); };
16
17
 
17
18
  function validate(config) {
18
19
  validName(config.name); validHealthPath(config.healthPath);
20
+ if(config.rollbackWindow!==undefined&&(!Number.isInteger(config.rollbackWindow)||config.rollbackWindow<0||config.rollbackWindow>120000))throw new Error('rollbackWindow must be between 0 and 120000 milliseconds');
19
21
  if (!path.isAbsolute(config.root) || !path.isAbsolute(config.binary) || !path.isAbsolute(config.node)) throw new Error('Application, Node and native executable paths must be absolute');
20
22
  if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) throw new Error('--port must be between 1 and 65535');
21
23
  if (typeof config.hostname !== 'string' || !config.hostname || /[\s\x00-\x1f]/.test(config.hostname)) throw new Error('Choose a valid --hostname');
@@ -27,7 +29,8 @@ async function persist() {
27
29
  await atomicJSON(registry, { version: 1, apps: [...apps.values()].map(app => ({ config: app.config, enabled: app.enabled, snapshots: [...app.snapshots] })) });
28
30
  }
29
31
  function newApp(config, enabled = false) {
30
- return { config, enabled, snapshots: new Set(), active: null, generations: new Set(), gateway: null, sockets: new Set(), state: 'stopped', error: null, starts: 0, failures: [], retry: null,
32
+ config={rollbackWindow:15000,...config};
33
+ return { config, enabled, snapshots: new Set(), active: null, generations: new Set(), gateway: null, state: 'stopped', error: null, starts: 0, failures: [], retry: null, probation:null,
31
34
  log: new RotatingLog(path.join(home, 'logs', `app-${config.name}.log`)) };
32
35
  }
33
36
  function summary(app) {
@@ -36,44 +39,13 @@ function summary(app) {
36
39
  activeRequests: [...app.generations].reduce((sum, generation) => sum + generation.active, 0),
37
40
  draining: [...app.generations].filter(generation => generation !== app.active).length,
38
41
  buildId: app.active?.buildId || null, startedAt: app.active?.startedAt || null, error: app.error,
42
+ proxyPid:app.gateway?.pid||null,
43
+ rollback:app.probation?{previousBuildId:app.probation.previous.buildId,until:app.probation.deadline}:null,
39
44
  log: path.join(home, 'logs', `app-${app.config.name}.log`) };
40
45
  }
41
- function hopHeaders(headers) {
42
- const result = { ...headers };
43
- const nominated = String(headers.connection || '').split(',').map(value => value.trim().toLowerCase());
44
- for (const key of ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ...nominated]) delete result[key];
45
- return result;
46
- }
47
46
  async function listen(app) {
48
47
  if (app.gateway) return;
49
- const server = createServer({ requestTimeout: 300000, headersTimeout: 30000 }, (incoming, outgoing) => {
50
- const generation = app.active;
51
- if (!incoming.url.startsWith('/')) { outgoing.writeHead(400).end('Use a local request path.'); return; }
52
- if (!generation || generation.dead) { outgoing.writeHead(503, { 'retry-after': '1' }).end('Application is restarting.'); incoming.resume(); return; }
53
- if (generation.active >= 512) { outgoing.writeHead(503, { 'retry-after': '1' }).end('Application is busy.'); incoming.resume(); return; }
54
- generation.active++;
55
- const headers = hopHeaders(incoming.headers);
56
- // Preserve original Host and trusted reverse-proxy headers. Never invent
57
- // retry/replay behavior for POSTs or requests that have already streamed.
58
- const upstream = request({ host: '127.0.0.1', port: generation.port, method: incoming.method, path: incoming.url, headers, agent: generation.agent }, response => {
59
- outgoing.writeHead(response.statusCode, hopHeaders(response.headers));
60
- response.on('error', () => outgoing.destroy()); response.pipe(outgoing);
61
- });
62
- generation.requests.add(upstream);
63
- let released = false;
64
- const release = () => { if (released) return; released = true; generation.active--; generation.requests.delete(upstream); upstream.destroy(); };
65
- outgoing.once('finish', release); outgoing.once('close', release);
66
- incoming.once('aborted', () => outgoing.destroy());
67
- incoming.once('error', () => outgoing.destroy());
68
- upstream.once('error', () => { if (!outgoing.headersSent) outgoing.writeHead(502).end('Application connection closed.'); else outgoing.destroy(); });
69
- incoming.pipe(upstream);
70
- });
71
- server.on('upgrade', (_request, socket) => socket.end('HTTP/1.1 501 Not Implemented\r\nConnection: close\r\nContent-Length: 0\r\n\r\n'));
72
- server.on('connection', socket => { app.sockets.add(socket); socket.once('close', () => app.sockets.delete(socket)); });
73
- server.maxConnections = 1024;
74
- await new Promise((resolve, reject) => { server.once('error', reject); server.listen(app.config.port, app.config.hostname, resolve); });
75
- server.on('error', error => { app.error = error.message; app.log.message(error.message); });
76
- app.gateway = server;
48
+ app.gateway=await nativeGateway(app.config,{home,notify:send,onFatal:error=>{console.error(error.message);if(!closing)process.exit(1);}});
77
49
  }
78
50
  async function removeSnapshot(app, relative) {
79
51
  // Only delete a directory generated by this manager inside this exact root.
@@ -120,11 +92,9 @@ async function stopGeneration(app, generation, drain = true) {
120
92
  generation.expected = true;
121
93
  generation.stopping = (async () => {
122
94
  const deadline = Date.now() + (drain ? app.config.drainTimeout : 0);
123
- while (generation.active && Date.now() < deadline && !generation.dead) await delay(25);
124
- for (const req of generation.requests) req.destroy();
125
- generation.agent.destroy();
95
+ await app.gateway?.retire(generation,drain?Math.max(0,deadline-Date.now()):0).catch(error=>app.log.message(`Proxy retirement: ${error.message}`));
126
96
  generation.child.stdin?.end();
127
- await Promise.race([generation.closed, delay(3000)]);
97
+ await Promise.race([generation.closed, delay(drain?Math.max(1000,deadline-Date.now()):3000)]);
128
98
  if (!generation.dead) generation.child.kill('SIGKILL');
129
99
  if (process.platform !== 'win32' && generation.child.pid) { try { process.kill(-generation.child.pid, 'SIGKILL'); } catch {} }
130
100
  await generation.closed;
@@ -156,7 +126,7 @@ async function launch(app) {
156
126
  env: { ...environment(), NODE_ENV: 'production', ...(config.inspect ? { PRNEXT_INSPECT_DIR: path.join(config.root, '.prnext-cache/inspect') } : {}) },
157
127
  });
158
128
  child.stdin.on('error', () => {});
159
- const generation = { child, ...build, active: 0, requests: new Set(), agent: new Agent({ keepAlive: true, maxSockets: 512, maxFreeSockets: 16, timeout: 30000 }), dead: false, expected: false, port: null, startedAt: Date.now() };
129
+ const generation = { child, ...build, active: 0, dead: false, expected: false, port: null, startedAt: Date.now() };
160
130
  app.generations.add(generation);
161
131
  if (child.pid) send({ type: 'child', pid: child.pid, running: true });
162
132
  let output = '', failure;
@@ -173,12 +143,12 @@ async function launch(app) {
173
143
  generation.closed = new Promise(resolve => child.once('close', (code, signal) => {
174
144
  generation.dead = true;
175
145
  if (child.pid) send({ type: 'child', pid: child.pid, running: false });
176
- if (app.active === generation) app.active = null;
146
+ if (app.active === generation) {app.active = null;void app.gateway?.pause(generation).catch(error=>app.log.message(error.message));}
177
147
  if (!generation.expected) {
178
148
  app.log.message(`Process ${child.pid || 'unknown'} exited (${signal || code}).`);
179
149
  if (generation.ready) {
180
150
  // Serialize recovery and cleanup with explicit lifecycle commands.
181
- sequence = sequence.then(async () => { await stopGeneration(app, generation, false); await persist(); retry(app, new Error(`Application exited (${signal || code})`)); }).catch(error => app.log.message(error.stack));
151
+ sequence = sequence.then(async () => { if(app.probation?.next===generation&&await rollback(app,new Error(`Replacement exited (${signal||code})`)))return;await stopGeneration(app, generation, false); await persist(); if(!app.active)retry(app, new Error(`Application exited (${signal || code})`)); }).catch(error => app.log.message(error.stack));
182
152
  }
183
153
  }
184
154
  resolve();
@@ -197,7 +167,42 @@ async function launch(app) {
197
167
  } catch (error) { await stopGeneration(app, generation, false); throw error; }
198
168
  finally { child.stdout.off('data', started); }
199
169
  }
170
+ async function finishProbation(app){
171
+ const probation=app.probation;if(!probation)return;
172
+ clearTimeout(probation.timer);app.probation=null;
173
+ await stopGeneration(app,probation.previous).catch(error=>app.log.message(`Previous build cleanup: ${error.message}`));
174
+ await persist();
175
+ }
176
+ async function rollback(app,error){
177
+ const probation=app.probation;if(!probation)return false;
178
+ clearTimeout(probation.timer);app.probation=null;
179
+ if(!app.enabled||closing||probation.previous.dead||probation.previous.expected){
180
+ await stopGeneration(app,probation.previous,false);return false;
181
+ }
182
+ // A live previous process still has to pass its health check. Never roll
183
+ // traffic back to an instance whose dependencies have also become unhealthy.
184
+ try{await health(probation.previous,app.config,Math.min(2000,app.config.healthTimeout));await app.gateway.activate(probation.previous);}
185
+ catch(restore){app.log.message(`Rollback unavailable: ${restore.message}`);await stopGeneration(app,probation.previous,false);return false;}
186
+ app.active=probation.previous;app.state='online';app.error=`Rolled back: ${error.message}`;
187
+ app.log.message(`Rolled back to build ${app.active.buildId}: ${error.message}`);
188
+ await stopGeneration(app,probation.next);await persist();return true;
189
+ }
190
+ function beginProbation(app,next,previous){
191
+ const probation=app.probation={next,previous,deadline:Date.now()+app.config.rollbackWindow,failures:0};
192
+ const schedule=()=>{probation.timer=setTimeout(()=>{
193
+ sequence=sequence.then(async()=>{
194
+ if(app.probation!==probation||!app.enabled||closing)return;
195
+ if(next.dead){await rollback(app,new Error('Replacement exited during monitoring'));return;}
196
+ if(Date.now()>=probation.deadline){await finishProbation(app);return;}
197
+ try{await health(next,app.config,Math.min(2000,app.config.healthTimeout));probation.failures=0;}
198
+ catch(error){if(++probation.failures>=3){if(!await rollback(app,error)){await stopGeneration(app,next,false);retry(app,error);}return;}}
199
+ if(app.probation===probation)schedule();
200
+ }).catch(error=>app.log.message(`Replacement monitoring: ${error.stack||error}`));
201
+ },Math.min(1000,Math.max(1,probation.deadline-Date.now())));};
202
+ schedule();
203
+ }
200
204
  async function activate(app) {
205
+ await finishProbation(app);
201
206
  await listen(app);
202
207
  const previous = app.active;
203
208
  const wasEnabled = app.enabled;
@@ -208,9 +213,13 @@ async function activate(app) {
208
213
  app.enabled = true;
209
214
  await persist();
210
215
  if (next.dead) throw new Error('Application exited before traffic could switch');
216
+ await app.gateway.activate(next);
217
+ if (next.dead) throw new Error('Application exited while traffic switched');
211
218
  app.active = next; app.state = 'online'; app.error = null;
212
219
  app.log.message(`Ready: pid ${next.child.pid}, build ${next.buildId || 'unknown'}, ${app.config.hostname}:${app.config.port}`);
213
220
  } catch (error) {
221
+ if (previous && !previous.dead) await app.gateway.activate(previous).catch(restore=>app.log.message(restore.message));
222
+ else await app.gateway?.pause().catch(restore=>app.log.message(restore.message));
214
223
  if (next) await stopGeneration(app, next, false).catch(cleanup => app.log.message(cleanup.message));
215
224
  app.enabled = wasEnabled;
216
225
  app.state = previous && !previous.dead ? 'online' : 'errored'; app.error = error.message;
@@ -218,21 +227,17 @@ async function activate(app) {
218
227
  app.log.message(`Start failed; ${previous && !previous.dead ? 'previous generation remains active' : 'no healthy generation'}: ${error.message}`);
219
228
  throw error;
220
229
  }
221
- if (previous) {
222
- await stopGeneration(app, previous).catch(error => app.log.message(`Previous build cleanup: ${error.message}`));
223
- await persist();
224
- }
230
+ if(previous && app.config.rollbackWindow>0)beginProbation(app,next,previous);
231
+ else if(previous){await stopGeneration(app,previous);await persist();}
225
232
  }
226
233
  async function stopApp(app) {
234
+ if(app.probation)clearTimeout(app.probation.timer);app.probation=null;
227
235
  app.enabled = false; clearTimeout(app.retry); app.retry = null;
228
236
  await persist();
229
237
  app.state = 'stopping'; app.active = null;
230
- const gateway = app.gateway; app.gateway = null;
231
- const closed = gateway ? new Promise(resolve => gateway.close(resolve)) : Promise.resolve();
232
- gateway?.closeIdleConnections();
238
+ await app.gateway?.pause();
233
239
  await Promise.all([...app.generations].map(generation => stopGeneration(app, generation)));
234
- for (const socket of app.sockets) socket.destroy();
235
- await closed; app.state = 'stopped'; app.error = null; await persist();
240
+ await app.gateway?.close();app.gateway=null;app.state = 'stopped'; app.error = null; await persist();
236
241
  app.log.message('Stopped.');
237
242
  }
238
243
  function select(message) {
@@ -242,8 +247,12 @@ function select(message) {
242
247
  return selected;
243
248
  }
244
249
  async function command(message) {
245
- if (message.command === 'status') return { daemonPid: process.pid, apps: [...apps.values()].map(summary) };
250
+ if (message.command === 'status') {
251
+ for(const app of apps.values())if(app.gateway){const counts=await app.gateway.counts();for(const g of app.generations)g.active=counts[g.relative]||0;}
252
+ return { daemonPid: process.pid, apps: [...apps.values()].map(summary) };
253
+ }
246
254
  if (message.command === 'start') {
255
+ message.config={rollbackWindow:15000,...message.config};
247
256
  validate(message.config);
248
257
  message.config.root = await realpath(message.config.root);
249
258
  let app = apps.get(message.config.name);
@@ -274,12 +283,13 @@ async function shutdown() {
274
283
  await sequence;
275
284
  // A supervisor shutdown preserves each enabled flag for the next launch.
276
285
  await Promise.all([...apps.values()].map(async app => {
277
- clearTimeout(app.retry); app.active = null; app.gateway?.close();
286
+ if(app.probation)clearTimeout(app.probation.timer);app.probation=null;
287
+ clearTimeout(app.retry); app.active = null;await app.gateway?.pause();
278
288
  await Promise.all([...app.generations].map(generation => stopGeneration(app, generation)));
279
- for (const socket of app.sockets) socket.destroy();
289
+ await app.gateway?.close();app.gateway=null;
280
290
  app.log.end();
281
291
  }));
282
- await persist(); process.exit(0);
292
+ await stopNativeProxy();await persist(); process.exit(0);
283
293
  }
284
294
  process.on('disconnect', () => void shutdown());
285
295
  process.on('SIGTERM', () => void shutdown());
@@ -0,0 +1,63 @@
1
+ // Lifecycle RPC only: no public HTTP bytes, headers or response bodies enter JS.
2
+ import {spawn} from 'node:child_process';
3
+ import {environment,delay} from './common.mjs';
4
+ let manager;
5
+ class NativeProxy {
6
+ constructor(binary,{home,notify,onFatal}){
7
+ this.pending=new Map();this.next=0;this.stopping=false;
8
+ this.child=spawn(binary,['persistent-proxy'],{cwd:home,windowsHide:true,detached:process.platform!=='win32',stdio:['pipe','pipe','inherit'],env:{...environment(),TOKIO_WORKER_THREADS:'1'}});
9
+ this.pid=this.child.pid;if(this.pid)notify({type:'child',pid:this.pid,running:true});
10
+ this.closed=new Promise(resolve=>this.child.once('close',()=>{if(this.pid)notify({type:'child',pid:this.pid,running:false});resolve();}));
11
+ this.ready=new Promise((resolve,reject)=>{
12
+ const timer=setTimeout(()=>{reject(new Error('Native persistent proxy did not initialize'));this.child.kill();},10000);timer.unref();
13
+ let buffer='';
14
+ this.child.stdout.setEncoding('utf8');
15
+ this.child.stdout.on('data',chunk=>{
16
+ buffer+=chunk;
17
+ if(buffer.length>1024*1024){this.child.kill();return;}
18
+ for(let end;(end=buffer.indexOf('\n'))>=0;){
19
+ const line=buffer.slice(0,end);buffer=buffer.slice(end+1);let message;
20
+ try{message=JSON.parse(line);}catch{this.child.kill();return;}
21
+ if(message.type==='ready'&&message.protocol===1){clearTimeout(timer);resolve();continue;}
22
+ const request=this.pending.get(message.id);if(!request)continue;
23
+ this.pending.delete(message.id);clearTimeout(request.timer);message.ok?request.resolve(message.value):request.reject(new Error(message.error||'Native proxy request failed'));
24
+ }
25
+ });
26
+ const failed=error=>{
27
+ clearTimeout(timer);reject(error);
28
+ for(const request of this.pending.values()){clearTimeout(request.timer);request.reject(error);}this.pending.clear();
29
+ if(!this.stopping)onFatal(error);
30
+ };
31
+ this.child.once('error',failed);this.child.once('exit',(code,signal)=>failed(new Error(`Native persistent proxy exited (${signal||code})`)));
32
+ });
33
+ this.child.stdin.on('error',()=>{});
34
+ }
35
+ async request(command,values={},timeout=10000){
36
+ await this.ready;
37
+ if(this.stopping||this.child.exitCode!==null||this.pending.size>=32)throw new Error('Native persistent proxy unavailable');
38
+ const id=++this.next;
39
+ return new Promise((resolve,reject)=>{
40
+ const timer=setTimeout(()=>{
41
+ this.pending.delete(id);reject(new Error('Native proxy control request timed out'));
42
+ // A timed-out switch/listen might still mutate native state later.
43
+ // End that uncertain control plane; the watchdog restores saved apps.
44
+ this.child.kill('SIGKILL');
45
+ },timeout);timer.unref();
46
+ this.pending.set(id,{resolve,reject,timer});
47
+ this.child.stdin.write(JSON.stringify({id,command,...values})+'\n',error=>{if(error){const request=this.pending.get(id);if(request){clearTimeout(request.timer);this.pending.delete(id);request.reject(error);}}});
48
+ });
49
+ }
50
+ async stop(){this.stopping=true;this.child.stdin.end();await Promise.race([this.closed,delay(3000)]);if(this.child.exitCode===null)this.child.kill('SIGKILL');await this.closed;}
51
+ }
52
+ export async function nativeGateway(config,options){
53
+ manager??=new NativeProxy(config.binary,options);await manager.ready;
54
+ const app=config.name;await manager.request('listen',{app,hostname:config.hostname,port:config.port});
55
+ return {pid:manager.pid,
56
+ activate:g=>manager.request('activate',{app,generation:g.relative,port:g.port}),
57
+ pause:g=>manager.request('pause',{app,...(g?{generation:g.relative}:{})}),
58
+ retire:(g,timeout)=>manager.request('retire',{app,generation:g.relative,timeout},timeout+5000),
59
+ counts:async()=> (await manager.request('status',{app})).generations,
60
+ close:()=>manager.request('close',{app}),
61
+ };
62
+ }
63
+ export async function stopNativeProxy(){if(manager){const active=manager;manager=undefined;await active.stop();}}
@@ -0,0 +1,110 @@
1
+ const INTENT = 1;
2
+ const abortError = () => Object.assign(new Error('Speculative navigation cancelled'), { name: 'AbortError' });
3
+
4
+ // The completed-result cache obeys the same priority as the producer queue.
5
+ // Otherwise a viewport scan could evict the hovered route immediately after
6
+ // the scheduler had correctly protected its request.
7
+ export function prefetchCacheVictim(cache, priority) {
8
+ let candidate;
9
+ for (const [key, entry] of cache) {
10
+ if (entry.abort.signal.aborted) return key;
11
+ if (!candidate || entry.priority < candidate[1].priority) candidate = [key, entry];
12
+ }
13
+ return candidate && candidate[1].priority <= priority ? candidate[0] : undefined;
14
+ }
15
+
16
+ // Speculative work never competes with a committed navigation. The same policy
17
+ // is used by both routers; data and authorization remain in their own caches.
18
+ export function createPrefetchScheduler({ window: win = globalThis.window, document: doc = globalThis.document,
19
+ concurrency = 2, limit = 8 } = {}) {
20
+ const tasks = new Set();
21
+ let serial = 0, active = 0, navigating = 0, disposed = false, scheduled = false;
22
+ const connection = win?.navigator?.connection;
23
+ const priorityOf = value => value === 'viewport' ? 0 : INTENT;
24
+ function policy(priority) {
25
+ const network = win?.navigator?.connection;
26
+ if (doc?.visibilityState === 'hidden' || win?.navigator?.onLine === false || network?.saveData || /^(slow-)?2g$/.test(network?.effectiveType || '')) return 0;
27
+ const slow = network?.effectiveType === '3g' || (network?.downlink > 0 && network.downlink < 1.5);
28
+ return slow ? (priority < INTENT ? 0 : 1) : concurrency;
29
+ }
30
+ function finish(task, error, value) {
31
+ if (!tasks.delete(task)) return;
32
+ if (task.running) active--;
33
+ task.abort.signal.removeEventListener('abort', task.cancelled);
34
+ const { resolve, reject } = task;
35
+ task.run = task.resolve = task.reject = task.cancelled = undefined;
36
+ error ? reject(error) : resolve(value);
37
+ wake();
38
+ }
39
+ function start(task) {
40
+ task.running = true; active++;
41
+ let result;
42
+ try { result = task.run(task.abort.signal); } catch (error) { finish(task, error); return; }
43
+ Promise.resolve(result).then(value => finish(task, null, value), error => finish(task, error));
44
+ }
45
+ function pump() {
46
+ scheduled = false;
47
+ if (disposed) return;
48
+ const queued = [...tasks].filter(task => !task.running).sort((a, b) => b.priority - a.priority || a.order - b.order);
49
+ for (const task of queued) {
50
+ if (!tasks.has(task)) continue;
51
+ if (task.priority === 2) { start(task); continue; }
52
+ if (navigating) break;
53
+ const slots = policy(task.priority);
54
+ if (!slots) { task.abort.abort(abortError()); continue; }
55
+ if (active >= slots) continue;
56
+ start(task);
57
+ }
58
+ }
59
+ function wake() {
60
+ if (scheduled || disposed) return;
61
+ scheduled = true; queueMicrotask(pump);
62
+ }
63
+ function changed() {
64
+ for (const task of tasks) if (task.priority !== 2 && !policy(task.priority)) task.abort.abort(abortError());
65
+ wake();
66
+ }
67
+ doc?.addEventListener?.('visibilitychange', changed);
68
+ win?.addEventListener?.('online', changed);
69
+ win?.addEventListener?.('offline', changed);
70
+ connection?.addEventListener?.('change', changed);
71
+ return {
72
+ schedule(run, { priority, abort = new AbortController() } = {}) {
73
+ const rank = priorityOf(priority);
74
+ if (disposed || abort.signal.aborted || !policy(rank)) return undefined;
75
+ if (tasks.size >= limit) {
76
+ // Keep intent ahead of broad viewport scans. Never evict demand work.
77
+ const oldest = [...tasks].filter(task => task.priority < 2).sort((a, b) => a.priority - b.priority || a.order - b.order)[0];
78
+ if (!oldest || oldest.priority > rank) return undefined;
79
+ oldest.abort.abort(abortError());
80
+ }
81
+ const task = { run, abort, priority: rank, order: serial++ };
82
+ const promise = new Promise((resolve, reject) => { task.resolve = resolve; task.reject = reject; });
83
+ // Cancellation also settles a task whose producer ignores AbortSignal.
84
+ // A late result cannot regain a queue slot or resurrect an invalid cache.
85
+ task.cancelled = () => finish(task, abort.signal.reason || abortError());
86
+ tasks.add(task); abort.signal.addEventListener('abort', task.cancelled, { once: true });
87
+ wake();
88
+ return { abort, promise,
89
+ promote(priority = 'intent') { if (tasks.has(task)) { task.priority = Math.max(task.priority, priorityOf(priority)); wake(); } },
90
+ demand() { if (tasks.has(task)) { task.priority = 2; wake(); } },
91
+ };
92
+ },
93
+ navigation(target) {
94
+ navigating++;
95
+ target?.demand();
96
+ for (const task of tasks) if (task.abort !== target?.abort && task.priority !== 2) task.abort.abort(abortError());
97
+ let released = false;
98
+ return () => { if (!released) { released = true; navigating--; wake(); } };
99
+ },
100
+ dispose() {
101
+ if (disposed) return;
102
+ disposed = true;
103
+ doc?.removeEventListener?.('visibilitychange', changed);
104
+ win?.removeEventListener?.('online', changed);
105
+ win?.removeEventListener?.('offline', changed);
106
+ connection?.removeEventListener?.('change', changed);
107
+ for (const task of tasks) task.abort.abort(abortError());
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,12 @@
1
+ import path from 'node:path';
2
+ import {pathToFileURL} from 'node:url';
3
+ // This is an import budget, not a total RSS limit. An individual module and its
4
+ // transitive imports can exceed it; imported JavaScript cannot be unloaded.
5
+ export async function preloadModules(files,{maxRssMb,timeoutMs},load=file=>import(pathToFileURL(file).href)){
6
+ const deadline=performance.now()+timeoutMs,loaded=[];
7
+ for(const file of files){
8
+ if(performance.now()>=deadline||process.memoryUsage().rss>=maxRssMb*1024*1024)break;
9
+ await load(path.resolve(file));loaded.push(file);
10
+ }
11
+ return loaded.length;
12
+ }
@@ -1,9 +1,23 @@
1
1
  import presets from './profiles.json' with { type: 'json' };
2
2
 
3
+ export function rscResourceLimits(environment=process.env,production=environment.NODE_ENV!=='development'){
4
+ if(!production||/--max[-_]semi[-_]space[-_]size(?:[=\s]|$)/.test(environment.NODE_OPTIONS||''))return undefined;
5
+ return {maxYoungGenerationSizeMb:runtimeProfile(environment,production).youngGenerationMiB};
6
+ }
7
+
3
8
  export function runtimeProfile(environment = process.env, production = environment.NODE_ENV !== 'development') {
4
9
  const requested = environment.PRNEXT_PROFILE || (environment.PRNEXT_MEMORY_PROFILE === 'compact' ? 'compact' : 'balanced');
5
10
  const name = requested === 'standard' ? 'classic' : requested;
6
11
  if (!Object.hasOwn(presets, name)) throw new Error(`Unknown runtime profile ${JSON.stringify(name)}; use ${Object.keys(presets).join(', ')}`);
7
12
  const effective = production ? name : 'classic';
13
+ if(production&&environment.PRNEXT_EFFECTIVE_PROFILE){
14
+ let value;
15
+ try{value=JSON.parse(environment.PRNEXT_EFFECTIVE_PROFILE);}catch{throw new Error('Invalid effective runtime profile');}
16
+ const settings=value?.[1],original=presets[effective];
17
+ if(!Array.isArray(value)||value.length!==2||value[0]!==effective||!settings||
18
+ Object.keys(settings).length!==Object.keys(original).length||Object.keys(original).some(key=>!Object.hasOwn(settings,key)||
19
+ (original[key]===null?settings[key]!==null&&(!Number.isSafeInteger(settings[key])||settings[key]<1):typeof original[key]==='boolean'?typeof settings[key]!=='boolean':!Number.isSafeInteger(settings[key])||settings[key]<1||settings[key]>Math.max(512,original[key]))))throw new Error('Invalid effective runtime profile');
20
+ return {name:effective,...settings};
21
+ }
8
22
  return { name: effective, ...presets[effective] };
9
23
  }
@@ -40,9 +40,9 @@
40
40
  "renderIdleSeconds": null
41
41
  },
42
42
  "memory": {
43
- "optimizeForSize": true,
43
+ "optimizeForSize": false,
44
44
  "semiSpaceMiB": 2,
45
- "youngGenerationMiB": 4,
45
+ "youngGenerationMiB": 8,
46
46
  "renderStarts": 8,
47
47
  "liveResponses": 28,
48
48
  "apiConcurrency": 64,
@@ -1,3 +1,4 @@
1
+ import {reportRequestError} from './instrumentation.mjs';
1
2
  import { localePath } from '../compat/locale.cjs';
2
3
  import React from 'react';
3
4
  import DefaultApp, { loadGetInitialProps } from '../compat/app.cjs';
@@ -94,6 +95,7 @@ export async function renderPageRequest(options) {
94
95
  try { result = await renderPage({ ...options, capturedResponse }); }
95
96
  catch (error) {
96
97
  if ([400, 504].includes(error?.statusCode) || options.signal?.aborted) throw error;
98
+ reportRequestError(error);
97
99
  console.error('[prnext]', error?.stack || error);
98
100
  return renderPageError(options, { statusCode: 500, error, responseHeaders: capturedResponse.getHeaders() });
99
101
  }
@@ -266,6 +268,7 @@ export async function renderIsrPage(options) {
266
268
  catch (error) {
267
269
  if (!options.capturePageFailure) throw error;
268
270
  const pageFailure = summarizePageFailure(error);
271
+ reportRequestError(error);
269
272
  console.error('[prnext] Page generation failed:', pageFailure.stack || pageFailure.message);
270
273
  return { status: 500, headers: {}, body: Buffer.alloc(0), pageFailure };
271
274
  }
@@ -0,0 +1,44 @@
1
+ /** Shared browser route matching; importing this never loads either router. */
2
+ export function matchPagePattern(pattern, pathname) {
3
+ let segments;
4
+ try { segments = pathname.replace(/\/+$/, '').split('/').slice(1).map(decodeURIComponent); }
5
+ catch { return null; }
6
+ if (segments.length === 1 && !segments[0]) segments = [];
7
+ if (segments.some(segment => !segment || /[/\\\0]/.test(segment) || segment === '.' || segment === '..')) return null;
8
+ const parts = pattern === '/' ? [] : pattern.split('/').slice(1), params = Object.create(null), rank = [];
9
+ let index = 0;
10
+ for (const part of parts) {
11
+ const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(part);
12
+ const catchall = /^\[\.\.\.([^\]]+)\]$/.exec(part);
13
+ const parameter = /^\[([^\]]+)\]$/.exec(part);
14
+ if (optional || catchall) {
15
+ if (catchall && index === segments.length) return null;
16
+ if (index !== segments.length) params[(optional || catchall)[1]] = segments.slice(index);
17
+ index = segments.length; rank.push(optional ? 0 : 1);
18
+ } else if (parameter) {
19
+ if (index === segments.length) return null;
20
+ params[parameter[1]] = segments[index++]; rank.push(2);
21
+ } else {
22
+ if (segments[index++] !== part) return null;
23
+ rank.push(3);
24
+ }
25
+ }
26
+ return index === segments.length ? { params, rank } : null;
27
+ }
28
+
29
+ export function isHtmlDocumentRoute(routes, pathname) {
30
+ let winner;
31
+ for (const route of routes || []) {
32
+ const match = matchPagePattern(route.pattern, pathname);
33
+ if (!match) continue;
34
+ const rank = winner && (() => {
35
+ for (let index = 0; index < Math.max(match.rank.length, winner.rank.length); index++) {
36
+ const difference = (match.rank[index] ?? 4) - (winner.rank[index] ?? 4);
37
+ if (difference) return difference;
38
+ }
39
+ return 0;
40
+ })();
41
+ if (!winner || rank > 0) winner = { ...match, route };
42
+ }
43
+ return winner?.route.htmlOnly === true;
44
+ }