@thomas.f/prnext 0.1.12-alpha → 0.1.13-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 (50) hide show
  1. package/README.md +262 -33
  2. package/build/actions.mjs +4 -0
  3. package/build/app-config.mjs +2 -0
  4. package/build/app.mjs +20 -5
  5. package/build/bundle-runtime.mjs +82 -0
  6. package/build/compiler.mjs +3 -1
  7. package/build/config.mjs +9 -2
  8. package/build/font-cache.mjs +50 -0
  9. package/build/font-files.mjs +5 -3
  10. package/build/fonts.mjs +1 -1
  11. package/build/index.mjs +5 -0
  12. package/build/metadata-conflicts.mjs +13 -0
  13. package/build/metadata-images.mjs +3 -3
  14. package/build/metadata-routes.mjs +3 -3
  15. package/build/native-packages.mjs +5 -1
  16. package/build/npm-packages.mjs +2 -1
  17. package/build/optimize-imports.mjs +112 -0
  18. package/build/proxy-body-config.mjs +10 -0
  19. package/build/scan.mjs +9 -7
  20. package/cli.mjs +16 -0
  21. package/compat/after.cjs +51 -0
  22. package/compat/index.d.ts +3 -1
  23. package/compat/server.cjs +2 -1
  24. package/compat/server.d.cts +2 -0
  25. package/native/launch.mjs +19 -0
  26. package/package.json +7 -7
  27. package/runtime/api.mjs +15 -5
  28. package/runtime/app-render.mjs +78 -33
  29. package/runtime/app-route-plan.mjs +45 -0
  30. package/runtime/app-routing-state.mjs +101 -21
  31. package/runtime/app-routing.mjs +37 -18
  32. package/runtime/app-segments.mjs +25 -11
  33. package/runtime/app-static.mjs +1 -1
  34. package/runtime/app-stream-html.mjs +2 -1
  35. package/runtime/cache-request.mjs +10 -2
  36. package/runtime/cloud/archive.mjs +316 -0
  37. package/runtime/cloud/client.mjs +658 -0
  38. package/runtime/cloud/environment.mjs +60 -0
  39. package/runtime/cloud/state.mjs +141 -0
  40. package/runtime/cloud/upload.mjs +48 -0
  41. package/runtime/html-envelope.mjs +70 -7
  42. package/runtime/http.mjs +7 -4
  43. package/runtime/middleware.mjs +3 -3
  44. package/runtime/request-transport.mjs +1 -1
  45. package/runtime/route-static.mjs +1 -1
  46. package/runtime/rsc-worker.mjs +27 -4
  47. package/runtime/stream-utils.mjs +22 -3
  48. package/runtime/transport.mjs +16 -0
  49. package/runtime/worker-metadata.mjs +23 -0
  50. package/runtime/worker.mjs +36 -9
@@ -2,6 +2,7 @@
2
2
  // prnext-concurrency:512
3
3
  import { requestFrames } from './request-transport.mjs';
4
4
  import { requestWork } from './request-work.mjs';
5
+ import { freezeClientModules } from './worker-metadata.mjs';
5
6
  import { AsyncResource } from 'node:async_hooks';
6
7
  const workScope = new AsyncResource('PRNextWorkObserver', { triggerAsyncId: 0 });
7
8
  import { readFile } from 'node:fs/promises';
@@ -35,6 +36,7 @@ loadEnvConfig(projectRoot, { dev: !!manifest.dev });
35
36
  // browser graph and the RSC worker's mode, including dev launched from CI.
36
37
  process.env.NODE_ENV = manifest.dev ? 'development' : 'production';
37
38
  const production = !manifest.dev;
39
+ if (production) freezeClientModules(manifest.app?.clientModules);
38
40
  // Load each renderer beside the application only when that request needs it.
39
41
  // Middleware and API workers do not pay for React or the Pages/App renderers.
40
42
  const runtimeModule = async name => {
@@ -58,6 +60,7 @@ let incrementalRuntime;
58
60
 
59
61
  async function handleRequest(input, output, signalController, responseComplete) {
60
62
  let responseStarted = false;
63
+ const finalizers = new Set();
61
64
  try {
62
65
  if (input.renderMode === 'incremental-cache' && input.routeId === '__prnext_incremental_cache') {
63
66
  incrementalRuntime ??= await runtimeModule('incremental-cache');
@@ -76,7 +79,7 @@ async function handleRequest(input, output, signalController, responseComplete)
76
79
  if (route.router !== 'app' && !['isr', 'error404', 'error500'].includes(input.renderMode) && route.fallback === false && !prerendered.has(requestedPath) && !prerendered.has(requestedPath.replace(/\/$/, '') || '/')) {
77
80
  await output(input.id, { status: 404, body: 'Not Found' }, input); return;
78
81
  }
79
- const options = { ...input, modulePath: path.resolve(distDir, route.module), route, production, manifest, distDir, signal: signalController.signal };
82
+ const options = { ...input, modulePath: path.resolve(distDir, route.module), route, production, manifest, distDir, signal: signalController.signal, deferFinalizer: finalize => finalizers.add(finalize) };
80
83
  let result;
81
84
  if (isMiddleware) {
82
85
  middlewareRuntime ??= await import(pathToFileURL(path.join(distDir, 'runtime/middleware.mjs')).href);
@@ -105,11 +108,9 @@ async function handleRequest(input, output, signalController, responseComplete)
105
108
  input.body = undefined;
106
109
  options.body = undefined;
107
110
  responseStarted = true;
108
- try {
109
- await output(input.id, result, { ...input, production, signalController });
110
- responseComplete?.();
111
- }
112
- finally { await result.finalizeCache?.(); }
111
+ if (result.finalizeCache) finalizers.add(result.finalizeCache);
112
+ await output(input.id, result, { ...input, production, signalController });
113
+ responseComplete?.();
113
114
  } catch (error) {
114
115
  const cancelled = signalController.signal.aborted || error instanceof ProtocolWriteError;
115
116
  signalController.abort(error);
@@ -122,6 +123,21 @@ async function handleRequest(input, output, signalController, responseComplete)
122
123
  // for its one error/redirect response. Do not mistake that cleanup signal
123
124
  // for a client disconnect while writing the replacement headers.
124
125
  await output(input?.id ?? null, navigationResponse(error) || errorResponse(error, production), { ...input, production });
126
+ responseComplete?.();
127
+ } finally {
128
+ if (finalizers.size) {
129
+ const pending = (async () => {
130
+ for (const finalize of finalizers) {
131
+ try { await finalize(); }
132
+ catch (error) { console.error('[prnext] Post-response work failed:', error?.stack || error); }
133
+ }
134
+ })();
135
+ // Release this socket's response lane while retaining worker activity.
136
+ // Otherwise an immediate follow-up request waits behind after() work.
137
+ const track = signalController.signal[requestWork];
138
+ if (track) track(pending);
139
+ else await pending;
140
+ }
125
141
  }
126
142
  }
127
143
 
@@ -173,7 +189,7 @@ if (token && laneLimit > 1) {
173
189
  if (invocations.size >= laneLimit) throw new Error('Worker invocation capacity exceeded');
174
190
  controller = new AbortController();
175
191
  const active = controller;
176
- let pendingWork = 0, unsupervisedWork = 0, finished = false;
192
+ let pendingWork = 0, unsupervisedWork = 0, finished = false, backgroundDeadline = 0;
177
193
  invocations.add(active);
178
194
  announceActivity();
179
195
  active.signal[requestWork] = (promise, options) => {
@@ -183,6 +199,10 @@ if (token && laneLimit > 1) {
183
199
  // legitimate cache work merely because its HTTP body has ended.
184
200
  const supervised = options?.supervised === true;
185
201
  if (!supervised) unsupervisedWork++;
202
+ if (!supervised && Number.isFinite(options?.timeoutMs) && options.timeoutMs > 0 && options.timeoutMs <= 300000) {
203
+ backgroundDeadline = Math.max(backgroundDeadline, performance.now() + options.timeoutMs);
204
+ if (active.signal.aborted) { clearTimeout(workTimer); retireIfAbandoned(); }
205
+ }
186
206
  const settled = () => {
187
207
  pendingWork--;
188
208
  if (!supervised) unsupervisedWork--;
@@ -199,7 +219,7 @@ if (token && laneLimit > 1) {
199
219
  if (pendingWork && !unsupervisedWork) return;
200
220
  workTimer = setTimeout(() => {
201
221
  if (invocations.has(active) && (!pendingWork || unsupervisedWork)) process.exit(70);
202
- }, 5000);
222
+ }, Math.max(5000, backgroundDeadline - performance.now()));
203
223
  workTimer.unref();
204
224
  };
205
225
  active.signal.addEventListener('abort', retireIfAbandoned, { once: true });
@@ -209,7 +229,7 @@ if (token && laneLimit > 1) {
209
229
  if (!pendingWork) { invocations.delete(active); clearTimeout(workTimer); }
210
230
  else {
211
231
  if (unsupervisedWork && !active.signal.aborted) {
212
- workTimer = setTimeout(() => { if (invocations.has(active) && unsupervisedWork) process.exit(70); }, 30000);
232
+ workTimer = setTimeout(() => { if (invocations.has(active) && unsupervisedWork) process.exit(70); }, Math.max(30000, backgroundDeadline - performance.now()));
213
233
  workTimer.unref();
214
234
  }
215
235
  }
@@ -259,4 +279,11 @@ if (token && laneLimit > 1) {
259
279
  try { await handleRequest(input, output, new AbortController()); }
260
280
  catch (error) { console.error('[prnext] Worker transport failed:', error?.stack || error); process.exit(1); }
261
281
  }
282
+ // The maintenance/ISR transport is serial and awaits its own finalizers.
283
+ // RSC settlement can still outlive its terminal frame; drain it explicitly
284
+ // before closing the isolate's MessagePorts after parent EOF.
285
+ if (appRuntime || appStaticRuntime) {
286
+ const runtime = appRuntime || await runtimeModule('app-render');
287
+ await runtime.drainAppRuntime();
288
+ }
262
289
  }