one 1.12.2 → 1.12.3

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 (139) hide show
  1. package/dist/cjs/Root.cjs +7 -1
  2. package/dist/cjs/Root.js +7 -2
  3. package/dist/cjs/Root.js.map +1 -1
  4. package/dist/cjs/Root.native.js +8 -2
  5. package/dist/cjs/Root.native.js.map +1 -1
  6. package/dist/cjs/createApp.cjs +2 -2
  7. package/dist/cjs/createApp.js +2 -2
  8. package/dist/cjs/createApp.js.map +1 -1
  9. package/dist/cjs/createHandleRequest.cjs +5 -1
  10. package/dist/cjs/createHandleRequest.js +6 -2
  11. package/dist/cjs/createHandleRequest.js.map +1 -1
  12. package/dist/cjs/createHandleRequest.native.js +5 -1
  13. package/dist/cjs/createHandleRequest.native.js.map +1 -1
  14. package/dist/cjs/fork/SSRNavigationContainer.cjs +18 -9
  15. package/dist/cjs/fork/SSRNavigationContainer.js +17 -6
  16. package/dist/cjs/fork/SSRNavigationContainer.js.map +1 -1
  17. package/dist/cjs/fork/SSRNavigationContainer.native.js +21 -10
  18. package/dist/cjs/fork/SSRNavigationContainer.native.js.map +1 -1
  19. package/dist/cjs/router/linkingConfig.cjs +2 -1
  20. package/dist/cjs/router/linkingConfig.js +2 -2
  21. package/dist/cjs/router/linkingConfig.js.map +1 -1
  22. package/dist/cjs/router/linkingConfig.native.js +2 -1
  23. package/dist/cjs/router/linkingConfig.native.js.map +1 -1
  24. package/dist/cjs/serve.cjs +67 -28
  25. package/dist/cjs/serve.js +68 -28
  26. package/dist/cjs/serve.js.map +1 -1
  27. package/dist/cjs/serve.native.js +96 -35
  28. package/dist/cjs/serve.native.js.map +1 -1
  29. package/dist/cjs/server/oneServe.cjs +112 -39
  30. package/dist/cjs/server/oneServe.js +94 -41
  31. package/dist/cjs/server/oneServe.js.map +2 -2
  32. package/dist/cjs/server/oneServe.native.js +180 -80
  33. package/dist/cjs/server/oneServe.native.js.map +1 -1
  34. package/dist/cjs/utils/evictOldest.cjs +34 -0
  35. package/dist/cjs/utils/evictOldest.js +29 -0
  36. package/dist/cjs/utils/evictOldest.js.map +6 -0
  37. package/dist/cjs/utils/evictOldest.native.js +34 -0
  38. package/dist/cjs/utils/evictOldest.native.js.map +1 -0
  39. package/dist/cjs/utils/isResponse.cjs +1 -1
  40. package/dist/cjs/utils/isResponse.js +1 -1
  41. package/dist/cjs/utils/isResponse.js.map +1 -1
  42. package/dist/cjs/utils/isResponse.native.js +1 -1
  43. package/dist/cjs/utils/isResponse.native.js.map +1 -1
  44. package/dist/cjs/vite/one-server-only.cjs +9 -6
  45. package/dist/cjs/vite/one-server-only.js +8 -7
  46. package/dist/cjs/vite/one-server-only.js.map +1 -1
  47. package/dist/cjs/vite/resolveResponse.cjs +17 -4
  48. package/dist/cjs/vite/resolveResponse.js +15 -2
  49. package/dist/cjs/vite/resolveResponse.js.map +1 -1
  50. package/dist/cjs/vite/resolveResponse.native.js +17 -4
  51. package/dist/cjs/vite/resolveResponse.native.js.map +1 -1
  52. package/dist/esm/Root.js +7 -1
  53. package/dist/esm/Root.js.map +1 -1
  54. package/dist/esm/Root.mjs +7 -1
  55. package/dist/esm/Root.mjs.map +1 -1
  56. package/dist/esm/Root.native.js +7 -1
  57. package/dist/esm/Root.native.js.map +1 -1
  58. package/dist/esm/createApp.js +2 -2
  59. package/dist/esm/createApp.js.map +1 -1
  60. package/dist/esm/createApp.mjs +2 -2
  61. package/dist/esm/createApp.mjs.map +1 -1
  62. package/dist/esm/createHandleRequest.js +6 -2
  63. package/dist/esm/createHandleRequest.js.map +1 -1
  64. package/dist/esm/createHandleRequest.mjs +5 -2
  65. package/dist/esm/createHandleRequest.mjs.map +1 -1
  66. package/dist/esm/createHandleRequest.native.js +5 -2
  67. package/dist/esm/createHandleRequest.native.js.map +1 -1
  68. package/dist/esm/fork/SSRNavigationContainer.js +17 -6
  69. package/dist/esm/fork/SSRNavigationContainer.js.map +1 -1
  70. package/dist/esm/fork/SSRNavigationContainer.mjs +18 -9
  71. package/dist/esm/fork/SSRNavigationContainer.mjs.map +1 -1
  72. package/dist/esm/fork/SSRNavigationContainer.native.js +21 -10
  73. package/dist/esm/fork/SSRNavigationContainer.native.js.map +1 -1
  74. package/dist/esm/router/linkingConfig.js +2 -1
  75. package/dist/esm/router/linkingConfig.js.map +1 -1
  76. package/dist/esm/router/linkingConfig.mjs +2 -1
  77. package/dist/esm/router/linkingConfig.mjs.map +1 -1
  78. package/dist/esm/router/linkingConfig.native.js +2 -1
  79. package/dist/esm/router/linkingConfig.native.js.map +1 -1
  80. package/dist/esm/serve.js +68 -28
  81. package/dist/esm/serve.js.map +1 -1
  82. package/dist/esm/serve.mjs +67 -28
  83. package/dist/esm/serve.mjs.map +1 -1
  84. package/dist/esm/serve.native.js +96 -35
  85. package/dist/esm/serve.native.js.map +1 -1
  86. package/dist/esm/server/oneServe.js +95 -42
  87. package/dist/esm/server/oneServe.js.map +2 -2
  88. package/dist/esm/server/oneServe.mjs +113 -40
  89. package/dist/esm/server/oneServe.mjs.map +1 -1
  90. package/dist/esm/server/oneServe.native.js +181 -81
  91. package/dist/esm/server/oneServe.native.js.map +1 -1
  92. package/dist/esm/utils/evictOldest.js +13 -0
  93. package/dist/esm/utils/evictOldest.js.map +6 -0
  94. package/dist/esm/utils/evictOldest.mjs +11 -0
  95. package/dist/esm/utils/evictOldest.mjs.map +1 -0
  96. package/dist/esm/utils/evictOldest.native.js +8 -0
  97. package/dist/esm/utils/evictOldest.native.js.map +1 -0
  98. package/dist/esm/utils/isResponse.js +1 -1
  99. package/dist/esm/utils/isResponse.js.map +1 -1
  100. package/dist/esm/utils/isResponse.mjs +1 -1
  101. package/dist/esm/utils/isResponse.mjs.map +1 -1
  102. package/dist/esm/utils/isResponse.native.js +1 -1
  103. package/dist/esm/utils/isResponse.native.js.map +1 -1
  104. package/dist/esm/vite/one-server-only.js +8 -7
  105. package/dist/esm/vite/one-server-only.js.map +1 -1
  106. package/dist/esm/vite/one-server-only.mjs +9 -6
  107. package/dist/esm/vite/one-server-only.mjs.map +1 -1
  108. package/dist/esm/vite/resolveResponse.js +15 -2
  109. package/dist/esm/vite/resolveResponse.js.map +1 -1
  110. package/dist/esm/vite/resolveResponse.mjs +16 -4
  111. package/dist/esm/vite/resolveResponse.mjs.map +1 -1
  112. package/dist/esm/vite/resolveResponse.native.js +16 -4
  113. package/dist/esm/vite/resolveResponse.native.js.map +1 -1
  114. package/package.json +9 -9
  115. package/src/Root.tsx +14 -1
  116. package/src/createApp.tsx +8 -2
  117. package/src/createHandleRequest.ts +9 -2
  118. package/src/fork/SSRNavigationContainer.tsx +30 -7
  119. package/src/router/linkingConfig.ts +2 -2
  120. package/src/serve.ts +134 -48
  121. package/src/server/oneServe.ts +153 -47
  122. package/src/utils/evictOldest.ts +13 -0
  123. package/src/utils/isResponse.ts +4 -4
  124. package/src/vite/one-server-only.tsx +25 -11
  125. package/src/vite/resolveResponse.ts +20 -1
  126. package/types/Root.d.ts.map +1 -1
  127. package/types/createApp.d.ts.map +1 -1
  128. package/types/createHandleRequest.d.ts +4 -0
  129. package/types/createHandleRequest.d.ts.map +1 -1
  130. package/types/fork/SSRNavigationContainer.d.ts.map +1 -1
  131. package/types/router/linkingConfig.d.ts.map +1 -1
  132. package/types/serve.d.ts.map +1 -1
  133. package/types/server/oneServe.d.ts.map +1 -1
  134. package/types/utils/evictOldest.d.ts +6 -0
  135. package/types/utils/evictOldest.d.ts.map +1 -0
  136. package/types/vite/one-server-only.d.ts +9 -3
  137. package/types/vite/one-server-only.d.ts.map +1 -1
  138. package/types/vite/resolveResponse.d.ts +4 -0
  139. package/types/vite/resolveResponse.d.ts.map +1 -1
package/src/serve.ts CHANGED
@@ -21,60 +21,146 @@ export async function serve(
21
21
  ) {
22
22
  // cluster mode: --cluster or --cluster=N
23
23
  if (args.cluster) {
24
- const cluster = await import('node:cluster')
25
- const { cpus } = await import('node:os')
24
+ const { cpus, platform } = await import('node:os')
25
+ const numWorkers = typeof args.cluster === 'number' ? args.cluster : cpus().length
26
+
27
+ // check if we can use SO_REUSEPORT (linux with node 22.12+)
28
+ const [major, minor] = process.versions.node.split('.').map(Number)
29
+ const canReusePort =
30
+ !['win32', 'darwin'].includes(platform()) &&
31
+ (major > 22 || (major === 22 && minor >= 12) || major >= 23)
32
+
33
+ if (canReusePort) {
34
+ // SO_REUSEPORT: spawn independent child processes, each binds to port directly
35
+ // kernel distributes connections - no IPC bottleneck
36
+ return await serveWithReusePort(args, numWorkers)
37
+ } else {
38
+ // fallback: node cluster module (IPC-based, works on macOS)
39
+ return await serveWithCluster(args, numWorkers)
40
+ }
41
+ }
42
+
43
+ // single-process mode
44
+ return await startWorker(args)
45
+ }
46
+
47
+ async function serveWithReusePort(args: Parameters<typeof serve>[0], numWorkers: number) {
48
+ const { fork } = await import('node:child_process')
49
+
50
+ console.info(`[one] cluster: starting ${numWorkers} workers (SO_REUSEPORT)`)
51
+
52
+ const workers: ReturnType<typeof fork>[] = []
53
+ let recentCrashes = 0
54
+ let lastCrashTime = 0
55
+
56
+ function spawnWorker() {
57
+ const child = fork(
58
+ process.argv[1]!,
59
+ process.argv.slice(2).filter((a) => !a.startsWith('--cluster')),
60
+ {
61
+ env: { ...process.env, ONE_CLUSTER_WORKER: '1' },
62
+ stdio: 'inherit',
63
+ }
64
+ )
65
+ workers.push(child)
26
66
 
27
- if (cluster.default.isPrimary) {
28
- const numWorkers = typeof args.cluster === 'number' ? args.cluster : cpus().length
67
+ child.on('exit', (code, signal) => {
68
+ const idx = workers.indexOf(child)
69
+ if (idx >= 0) workers.splice(idx, 1)
29
70
 
30
- console.info(`[one] cluster: starting ${numWorkers} workers`)
71
+ if (code === 0 || signal === 'SIGTERM' || signal === 'SIGINT') return
31
72
 
32
- for (let i = 0; i < numWorkers; i++) {
33
- cluster.default.fork()
73
+ const now = Date.now()
74
+ if (now - lastCrashTime < 5000) {
75
+ recentCrashes++
76
+ } else {
77
+ recentCrashes = 1
34
78
  }
79
+ lastCrashTime = now
35
80
 
36
- // restart crashed workers with backoff
37
- let recentCrashes = 0
38
- let lastCrashTime = 0
39
-
40
- cluster.default.on('exit', (worker, code, signal) => {
41
- if (code === 0 || signal === 'SIGTERM' || signal === 'SIGINT') return
42
-
43
- const now = Date.now()
44
- if (now - lastCrashTime < 5000) {
45
- recentCrashes++
46
- } else {
47
- recentCrashes = 1
48
- }
49
- lastCrashTime = now
50
-
51
- if (recentCrashes > numWorkers * 2) {
52
- console.error(`[one] too many worker crashes, stopping`)
53
- process.exit(1)
54
- }
55
-
56
- console.error(
57
- `[one] worker ${worker.process.pid} died (code ${code}, signal ${signal}), restarting`
58
- )
59
- setTimeout(() => cluster.default.fork(), Math.min(recentCrashes * 500, 5000))
60
- })
61
-
62
- const shutdown = () => {
63
- for (const id in cluster.default.workers) {
64
- cluster.default.workers[id]?.process.kill('SIGTERM')
65
- }
66
- setTimeout(() => process.exit(0), 5000)
81
+ if (recentCrashes > numWorkers * 2) {
82
+ console.error(`[one] too many worker crashes, stopping`)
83
+ process.exit(1)
67
84
  }
68
- process.on('SIGINT', shutdown)
69
- process.on('SIGTERM', shutdown)
70
85
 
71
- return
86
+ console.error(
87
+ `[one] worker ${child.pid} died (code ${code}, signal ${signal}), restarting`
88
+ )
89
+ setTimeout(spawnWorker, Math.min(recentCrashes * 500, 5000))
90
+ })
91
+ }
92
+
93
+ for (let i = 0; i < numWorkers; i++) {
94
+ spawnWorker()
95
+ }
96
+
97
+ const shutdown = () => {
98
+ for (const w of workers) {
99
+ w.kill('SIGTERM')
72
100
  }
101
+ setTimeout(() => process.exit(0), 5000)
73
102
  }
103
+ process.on('SIGINT', shutdown)
104
+ process.on('SIGTERM', shutdown)
105
+
106
+ // keep primary alive
107
+ await new Promise(() => {})
108
+ }
109
+
110
+ async function serveWithCluster(args: Parameters<typeof serve>[0], numWorkers: number) {
111
+ const cluster = await import('node:cluster')
112
+
113
+ if (cluster.default.isPrimary) {
114
+ console.info(`[one] cluster: starting ${numWorkers} workers (IPC)`)
115
+
116
+ for (let i = 0; i < numWorkers; i++) {
117
+ cluster.default.fork()
118
+ }
119
+
120
+ let recentCrashes = 0
121
+ let lastCrashTime = 0
122
+
123
+ cluster.default.on('exit', (worker, code, signal) => {
124
+ if (code === 0 || signal === 'SIGTERM' || signal === 'SIGINT') return
125
+
126
+ const now = Date.now()
127
+ if (now - lastCrashTime < 5000) {
128
+ recentCrashes++
129
+ } else {
130
+ recentCrashes = 1
131
+ }
132
+ lastCrashTime = now
133
+
134
+ if (recentCrashes > numWorkers * 2) {
135
+ console.error(`[one] too many worker crashes, stopping`)
136
+ process.exit(1)
137
+ }
138
+
139
+ console.error(
140
+ `[one] worker ${worker.process.pid} died (code ${code}, signal ${signal}), restarting`
141
+ )
142
+ setTimeout(() => cluster.default.fork(), Math.min(recentCrashes * 500, 5000))
143
+ })
144
+
145
+ const shutdown = () => {
146
+ for (const id in cluster.default.workers) {
147
+ cluster.default.workers[id]?.process.kill('SIGTERM')
148
+ }
149
+ setTimeout(() => process.exit(0), 5000)
150
+ }
151
+ process.on('SIGINT', shutdown)
152
+ process.on('SIGTERM', shutdown)
153
+
154
+ return
155
+ }
156
+
157
+ // cluster worker
158
+ return await startWorker(args)
159
+ }
74
160
 
75
- // worker (or single-process mode)
161
+ async function startWorker(args: Parameters<typeof serve>[0]) {
76
162
  const outDir =
77
- args.outDir || (FSExtra.existsSync('buildInfo.json') ? '.' : null) || 'dist'
163
+ args?.outDir || (FSExtra.existsSync('buildInfo.json') ? '.' : null) || 'dist'
78
164
  const buildInfo = (await FSExtra.readJSON(`${outDir}/buildInfo.json`)) as One.BuildInfo
79
165
  const { oneOptions } = buildInfo
80
166
 
@@ -89,18 +175,18 @@ export async function serve(
89
175
 
90
176
  labelProcess('serve')
91
177
 
92
- if (args.loadEnv) {
178
+ if (args?.loadEnv) {
93
179
  await loadEnv('production')
94
180
  }
95
181
 
96
182
  return await vxrnServe({
97
183
  outDir: buildInfo.outDir || outDir,
98
- app: args.app,
184
+ app: args?.app,
99
185
  ...oneOptions.server,
100
186
  ...removeUndefined({
101
- port: args.port ? +args.port : undefined,
102
- host: args.host,
103
- compress: args.compress,
187
+ port: args?.port ? +args.port : undefined,
188
+ host: args?.host,
189
+ compress: args?.compress,
104
190
  }),
105
191
 
106
192
  async beforeRegisterRoutes(options, app) {
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises'
2
- import { extname, join, resolve } from 'node:path'
2
+ import { join, resolve } from 'node:path'
3
3
  import type { Hono, MiddlewareHandler } from 'hono'
4
4
  import type { BlankEnv } from 'hono/types'
5
5
  import {
@@ -9,6 +9,7 @@ import {
9
9
  } from '../constants'
10
10
  import {
11
11
  compileManifest,
12
+ getLoaderParams,
12
13
  getURLfromRequestURL,
13
14
  type RequestHandlers,
14
15
  runMiddlewares,
@@ -61,6 +62,7 @@ export async function oneServe(
61
62
  await import('../createHandleRequest')
62
63
  const { isResponse } = await import('../utils/isResponse')
63
64
  const { isStatusRedirect } = await import('../utils/isStatus')
65
+ const { withRequestContext } = await import('../vite/resolveResponse')
64
66
 
65
67
  const isAPIRequest = new WeakMap<any, boolean>()
66
68
 
@@ -117,6 +119,12 @@ export async function oneServe(
117
119
 
118
120
  const apiCJS = oneOptions.build?.api?.outputFormat === 'cjs'
119
121
 
122
+ // pre-computed constants to avoid per-request overhead
123
+ const useStreaming = !process.env.ONE_BUFFERED_SSR
124
+ const htmlHeaders = { 'content-type': 'text/html' }
125
+ // SSR responses get no-cache by default — include it in headers to avoid per-response mutation
126
+ const ssrHtmlHeaders = { 'content-type': 'text/html', 'cache-control': 'no-cache' }
127
+
120
128
  // cache resolved loader functions directly (not just modules)
121
129
  const loaderCache = new Map<string, Function | null>()
122
130
  const moduleImportCache = new Map<string, any>()
@@ -369,15 +377,31 @@ export async function oneServe(
369
377
  try {
370
378
  // collect layout loaders to run in parallel
371
379
  const layoutRoutes = route.layouts || []
372
- const layoutLoaderPromises = layoutRoutes.map((layout: any) => {
380
+
381
+ // fast path: check which layouts actually have loaders (sync on cache hit)
382
+ // skip importAndRunLoader entirely for layouts with no loader
383
+ const layoutLoaderPromises: Array<ReturnType<typeof importAndRunLoader>> = []
384
+ const noLoaderResults: Array<{ loaderData: unknown; routeId: string }> = []
385
+
386
+ for (const layout of layoutRoutes) {
373
387
  const serverPath = layout.loaderServerPath || layout.contextKey
374
- return importAndRunLoader(
375
- layout.contextKey,
376
- serverPath,
377
- layout.contextKey,
378
- loaderProps
379
- )
380
- })
388
+ const cacheKey = layout.contextKey || serverPath || ''
389
+ const cachedLoader = loaderCache.get(cacheKey)
390
+
391
+ if (cachedLoader === null) {
392
+ // loader already resolved to null - skip the async call entirely
393
+ noLoaderResults.push({ loaderData: undefined, routeId: layout.contextKey })
394
+ } else {
395
+ layoutLoaderPromises.push(
396
+ importAndRunLoader(
397
+ layout.contextKey,
398
+ serverPath,
399
+ layout.contextKey,
400
+ loaderProps
401
+ )
402
+ )
403
+ }
404
+ }
381
405
 
382
406
  // run page loader
383
407
  const pageLoaderPromise = importAndRunLoader(
@@ -396,10 +420,18 @@ export async function oneServe(
396
420
  let pageResult: { loaderData: unknown; routeId: string; isEnoent?: boolean }
397
421
 
398
422
  try {
399
- ;[layoutResults, pageResult] = await Promise.all([
400
- Promise.all(layoutLoaderPromises),
401
- pageLoaderPromise,
402
- ])
423
+ if (layoutLoaderPromises.length === 0) {
424
+ // fast path: all layout loaders are null or no layouts
425
+ layoutResults = noLoaderResults
426
+ pageResult = await pageLoaderPromise
427
+ } else {
428
+ const [asyncLayoutResults, pr] = await Promise.all([
429
+ Promise.all(layoutLoaderPromises),
430
+ pageLoaderPromise,
431
+ ])
432
+ layoutResults = [...noLoaderResults, ...asyncLayoutResults]
433
+ pageResult = pr
434
+ }
403
435
  } catch (err) {
404
436
  // Handle thrown responses (e.g., redirect) from any loader
405
437
  if (isResponse(err)) {
@@ -428,20 +460,24 @@ export async function oneServe(
428
460
  }
429
461
 
430
462
  // build matches array (layouts + page)
431
- const matches: One.RouteMatch[] = [
432
- ...layoutResults.map((result) => ({
463
+ const matchPathname = loaderProps?.path || '/'
464
+ const matchParams = loaderProps?.params || {}
465
+ const matches: One.RouteMatch[] = new Array(layoutResults.length + 1)
466
+ for (let i = 0; i < layoutResults.length; i++) {
467
+ const result = layoutResults[i]
468
+ matches[i] = {
433
469
  routeId: result.routeId,
434
- pathname: loaderProps?.path || '/',
435
- params: loaderProps?.params || {},
470
+ pathname: matchPathname,
471
+ params: matchParams,
436
472
  loaderData: result.loaderData,
437
- })),
438
- {
439
- routeId: pageResult.routeId,
440
- pathname: loaderProps?.path || '/',
441
- params: loaderProps?.params || {},
442
- loaderData: pageResult.loaderData,
443
- },
444
- ]
473
+ }
474
+ }
475
+ matches[layoutResults.length] = {
476
+ routeId: pageResult.routeId,
477
+ pathname: matchPathname,
478
+ params: matchParams,
479
+ loaderData: pageResult.loaderData,
480
+ }
445
481
 
446
482
  // for backwards compat, loaderData is still the page's loader data
447
483
  const loaderData = pageResult.loaderData
@@ -462,9 +498,6 @@ export async function oneServe(
462
498
  setSSRLoaderData(pageLoaderFn, pageResult.loaderData)
463
499
  }
464
500
 
465
- const headers = new Headers()
466
- headers.set('content-type', 'text/html')
467
-
468
501
  // prepare router for this SSR render (lightweight version bump)
469
502
  globalThis['__vxrnresetState']?.()
470
503
 
@@ -480,16 +513,20 @@ export async function oneServe(
480
513
  matches,
481
514
  }
482
515
 
483
- // streaming SSR by default, fall back to buffered with ONE_BUFFERED_SSR=1
484
516
  const _rl = ensureRenderLoaded()
485
517
  if (_rl) await _rl
486
518
 
487
- const streamFn = !process.env.ONE_BUFFERED_SSR ? renderStream : null
488
- if (streamFn) {
489
- const stream = await streamFn(renderProps)
519
+ const status = route.isNotFound ? 404 : 200
520
+ // use ssrHtmlHeaders (includes cache-control: no-cache) to avoid
521
+ // per-response header mutation in the Hono handler
522
+ const responseHeaders = route.isNotFound ? htmlHeaders : ssrHtmlHeaders
523
+
524
+ // streaming SSR by default, fall back to buffered with ONE_BUFFERED_SSR=1
525
+ if (useStreaming) {
526
+ const stream = await renderStream!(renderProps)
490
527
  return new Response(stream, {
491
- headers,
492
- status: route.isNotFound ? 404 : 200,
528
+ headers: responseHeaders,
529
+ status,
493
530
  })
494
531
  }
495
532
 
@@ -497,8 +534,8 @@ export async function oneServe(
497
534
  const rendered = await render!(renderProps)
498
535
 
499
536
  return new Response(rendered, {
500
- headers,
501
- status: route.isNotFound ? 404 : 200,
537
+ headers: responseHeaders,
538
+ status,
502
539
  })
503
540
  } catch (err) {
504
541
  // Handle thrown responses (e.g., redirect) that weren't caught above
@@ -545,9 +582,6 @@ url: ${url}`)
545
582
  loaderData: result.loaderData,
546
583
  }))
547
584
 
548
- const headers = new Headers()
549
- headers.set('content-type', 'text/html')
550
-
551
585
  globalThis['__vxrnresetState']?.()
552
586
 
553
587
  const _rl3 = ensureRenderLoaded()
@@ -567,7 +601,7 @@ url: ${url}`)
567
601
  })
568
602
 
569
603
  return new Response(rendered, {
570
- headers,
604
+ headers: htmlHeaders,
571
605
  status: route.isNotFound ? 404 : 200,
572
606
  })
573
607
  } catch (err) {
@@ -649,14 +683,15 @@ url: ${url}`)
649
683
  function createHonoHandler(
650
684
  route: RouteInfoCompiled
651
685
  ): MiddlewareHandler<BlankEnv, never, {}> {
686
+ // pre-compute per-route checks (constant for the lifetime of the handler)
687
+ const isDynamicOrNotFound =
688
+ route.page.endsWith('/+not-found') || Object.keys(route.routeKeys).length > 0
689
+
652
690
  return async (context, next) => {
653
691
  try {
654
692
  const request = context.req.raw
655
693
 
656
- if (
657
- route.page.endsWith('/+not-found') ||
658
- Reflect.ownKeys(route.routeKeys).length > 0
659
- ) {
694
+ if (isDynamicOrNotFound) {
660
695
  // Static assets should have the highest priority - which is the behavior of the dev server.
661
696
  // But if we handle every matching static asset here, it seems to break some of the static routes.
662
697
  // So we only handle it if there's a matching not-found or dynamic route, to prevent One from taking over the static asset.
@@ -677,9 +712,80 @@ url: ${url}`)
677
712
  }
678
713
  }
679
714
 
680
- // for js we want to serve our js files directly, as they can match a route on accident
681
- // middleware my want to handle this eventually as well but for now this is a fine balance
682
- if (extname(request.url) === '.js' || extname(request.url) === '.css') {
715
+ // for js/css we want to serve our files directly, as they can match a route on accident
716
+ // use the hono-parsed path to avoid parsing the full URL string
717
+ const reqPath = context.req.path
718
+ if (reqPath.endsWith('.js') || reqPath.endsWith('.css')) {
719
+ return next()
720
+ }
721
+
722
+ // fast path for SSR pages without middleware:
723
+ // skip URL parsing, resolvePageRoute, and resolveResponse entirely.
724
+ // use hono's pre-parsed path and compute params inline.
725
+ if (
726
+ route.type === 'ssr' &&
727
+ !route.middlewares?.length &&
728
+ !reqPath.endsWith(LOADER_JS_POSTFIX_UNCACHED)
729
+ ) {
730
+ if (debugRouter) {
731
+ console.info(`[one] ⚡ ${reqPath} → matched page route: ${route.page} (ssr)`)
732
+ }
733
+ const pathname = reqPath
734
+ // extract search from raw URL (after ?)
735
+ const rawUrl = request.url
736
+ const qIdx = rawUrl.indexOf('?')
737
+ const search = qIdx >= 0 ? rawUrl.slice(qIdx) : ''
738
+
739
+ // compute params from compiled regex using pathname
740
+ const params: Record<string, string> = {}
741
+ const match = route.compiledRegex.exec(pathname)
742
+ if (match?.groups) {
743
+ for (const [key, value] of Object.entries(match.groups)) {
744
+ const namedKey = route.routeKeys[key]
745
+ params[namedKey] = value as string
746
+ }
747
+ }
748
+
749
+ const loaderProps = {
750
+ path: pathname,
751
+ search,
752
+ request,
753
+ params,
754
+ }
755
+
756
+ // lazy-create URL only when needed (error paths, non-SSR branches)
757
+ const url = getURLfromRequestURL(request)
758
+
759
+ const response = await withRequestContext(async () => {
760
+ try {
761
+ return await requestHandlers.handlePage!({
762
+ request,
763
+ route,
764
+ url,
765
+ loaderProps,
766
+ })
767
+ } catch (err) {
768
+ if (isResponse(err)) {
769
+ return err as Response
770
+ }
771
+ throw err
772
+ }
773
+ })
774
+
775
+ if (response) {
776
+ if (isResponse(response)) {
777
+ if (isStatusRedirect(response.status)) {
778
+ const location = `${response.headers.get('location') || ''}`
779
+ response.headers.forEach((value, key) => {
780
+ context.header(key, value)
781
+ })
782
+ return context.redirect(location, response.status)
783
+ }
784
+ // cache-control is already set in ssrHtmlHeaders for SSR responses
785
+ return response as Response
786
+ }
787
+ return next()
788
+ }
683
789
  return next()
684
790
  }
685
791
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * evict the oldest entries from a map when it exceeds a size threshold.
3
+ * entries are iterated in insertion order, so the first entries are the oldest.
4
+ */
5
+ export function evictOldest(map: Map<any, any>, threshold: number, count: number) {
6
+ if (map.size > threshold) {
7
+ const iter = map.keys()
8
+ for (let i = 0; i < count; i++) {
9
+ const key = iter.next().value
10
+ if (key) map.delete(key)
11
+ }
12
+ }
13
+ }
@@ -1,7 +1,7 @@
1
1
  // for some reason instanceof isnt working reliably
2
2
  export function isResponse(res: any): res is Response {
3
- return (
4
- res instanceof Response ||
5
- (typeof res.status === 'number' && 'body' in res && typeof res.ok === 'boolean')
6
- )
3
+ // fast path: most responses are instances
4
+ if (res instanceof Response) return true
5
+ // fallback: duck-type check for response-like objects
6
+ return res != null && typeof res.status === 'number' && typeof res.ok === 'boolean'
7
7
  }
@@ -10,7 +10,16 @@ import { AsyncLocalStorage } from 'node:async_hooks'
10
10
  import { SERVER_CONTEXT_KEY } from '../constants'
11
11
  import type { One } from './types'
12
12
 
13
- type ALSInstance = AsyncLocalStorage<unknown>
13
+ // symbol key for storing context directly on the ALS id object (faster than WeakMap)
14
+ const _ctxKey = Symbol.for('__oneCtx')
15
+
16
+ /** shape of the object stored as the ALS context id */
17
+ export interface ALSId {
18
+ _id: number
19
+ [_ctxKey]?: One.ServerContext
20
+ }
21
+
22
+ type ALSInstance = AsyncLocalStorage<ALSId>
14
23
 
15
24
  const key = '__vxrnrequestAsyncLocalStore'
16
25
  const read = () => globalThis[key] as ALSInstance | undefined
@@ -18,7 +27,7 @@ const read = () => globalThis[key] as ALSInstance | undefined
18
27
  const ASYNC_LOCAL_STORE = {
19
28
  get current() {
20
29
  if (read()) return read()
21
- const _ = new AsyncLocalStorage()
30
+ const _ = new AsyncLocalStorage<ALSId>()
22
31
  globalThis[key] = _
23
32
  return _
24
33
  },
@@ -35,9 +44,9 @@ export const asyncHeadersCache =
35
44
  globalThis['__vxrnasyncHeadersCache'] ||= asyncHeadersCache
36
45
 
37
46
  export async function runWithAsyncLocalContext<A>(
38
- cb: (id: object) => Promise<A>
47
+ cb: (id: ALSId) => Promise<A>
39
48
  ): Promise<A> {
40
- const id = { _id: Math.random() }
49
+ const id: ALSId = { _id: Math.random() }
41
50
  let out: A = null as any
42
51
  await ASYNC_LOCAL_STORE.current!.run(id, async () => {
43
52
  out = await cb(id)
@@ -92,7 +101,7 @@ export function ensureAsyncLocalID() {
92
101
  throw new Error(`Internal One error, no AsyncLocalStorage id!`)
93
102
  }
94
103
 
95
- return id as object
104
+ return id as ALSId
96
105
  }
97
106
 
98
107
  export type MaybeServerContext = null | One.ServerContext
@@ -107,11 +116,14 @@ const serverContexts = globalThis[SERVER_CONTEXTS_KEY] as WeakMap<any, One.Serve
107
116
  export function setServerContext(data: One.ServerContext) {
108
117
  if (process.env.VITE_ENVIRONMENT === 'ssr') {
109
118
  const id = ensureAsyncLocalID()
110
- if (!serverContexts.has(id)) {
111
- serverContexts.set(id, {})
119
+ // fast path: store context directly on the id object to skip WeakMap ops
120
+ let context = id[_ctxKey]
121
+ if (!context) {
122
+ context = {}
123
+ id[_ctxKey] = context
124
+ // also set in WeakMap for backwards compatibility
125
+ serverContexts.set(id, context)
112
126
  }
113
-
114
- const context = serverContexts.get(id)!
115
127
  Object.assign(context, data)
116
128
  } else {
117
129
  throw new Error(`Don't call setServerContext on client`)
@@ -122,7 +134,8 @@ export function getServerContext() {
122
134
  const out = (() => {
123
135
  if (process.env.VITE_ENVIRONMENT === 'ssr') {
124
136
  const id = ensureAsyncLocalID()
125
- return serverContexts.get(id)
137
+ // fast path: read from id object directly
138
+ return id[_ctxKey] || serverContexts.get(id)
126
139
  }
127
140
  return globalThis[SERVER_CONTEXT_KEY] as MaybeServerContext
128
141
  })()
@@ -135,7 +148,8 @@ export function useServerContext() {
135
148
  try {
136
149
  const useContext = globalThis['__vxrnGetContextFromReactContext']
137
150
  if (useContext) {
138
- return serverContexts.get(useContext())
151
+ const id = useContext() as ALSId | null
152
+ if (id) return id[_ctxKey] || serverContexts.get(id)
139
153
  }
140
154
  } catch {
141
155
  // ok, not in react tree
@@ -1,16 +1,23 @@
1
1
  import { isResponse } from '../utils/isResponse'
2
2
  import {
3
+ type ALSId,
3
4
  asyncHeadersCache,
4
5
  mergeHeaders,
5
6
  requestAsyncLocalStore,
6
7
  runWithAsyncLocalContext,
7
8
  } from './one-server-only'
8
9
 
10
+ // lightweight monotonic id - avoids Math.random() per request
11
+ let _nextId = 1
12
+ function createId(): ALSId {
13
+ return { _id: _nextId++ }
14
+ }
15
+
9
16
  export async function resolveResponse(getResponse: () => Promise<Response>) {
10
17
  // inline ALS to reduce async nesting (each await = microtask = event loop pressure)
11
18
  const store = requestAsyncLocalStore
12
19
  if (store) {
13
- const id = { _id: Math.random() }
20
+ const id = createId()
14
21
  let response: Response
15
22
  await store.run(id, async () => {
16
23
  try {
@@ -40,6 +47,18 @@ export async function resolveResponse(getResponse: () => Promise<Response>) {
40
47
  })
41
48
  }
42
49
 
50
+ /**
51
+ * enter ALS context once for the entire request handler.
52
+ */
53
+ export function withRequestContext<T>(fn: () => Promise<T>): Promise<T> {
54
+ const store = requestAsyncLocalStore
55
+ if (store) {
56
+ const id = createId()
57
+ return store.run(id, fn) as Promise<T>
58
+ }
59
+ return fn()
60
+ }
61
+
43
62
  export function resolveAPIEndpoint(
44
63
  // this is the result of importing the file:
45
64
  runEndpoint: () => Promise<any>,
@@ -1 +1 @@
1
- {"version":3,"file":"Root.d.ts","sourceRoot":"","sources":["../src/Root.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,SAAS,EAMf,MAAM,OAAO,CAAA;AAYd,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAKlD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAEvC,KAAK,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG;IAC7C,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,mBAAmB,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,GAAG,CAAC,YAAY,CAAA;IAC/B,KAAK,CAAC,EAAE,GAAG,CAAC,KAAK,CAAA;CAClB,CAAA;AAED,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,GAAG,CAAC,YAAY,CAAA;IACzB,QAAQ,CAAC,EAAE,GAAG,CAAA;IACd,OAAO,CAAC,EAAE,iBAAiB,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACpD,wBAAwB,CAAC,EAAE,wBAAwB,GAAG;QACpD,KAAK,CAAC,EAAE;YACN,IAAI,EAAE,OAAO,CAAA;YACb,MAAM,EAAE;gBACN,OAAO,EAAE,MAAM,CAAA;gBACf,UAAU,EAAE,MAAM,CAAA;gBAClB,IAAI,EAAE,MAAM,CAAA;gBACZ,IAAI,EAAE,MAAM,CAAA;gBACZ,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;SACF,CAAA;KACF,CAAA;CACF,CAAA;AAQD,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,kDAgIpC"}
1
+ {"version":3,"file":"Root.d.ts","sourceRoot":"","sources":["../src/Root.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,SAAS,EAMf,MAAM,OAAO,CAAA;AAYd,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAMlD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAcvC,KAAK,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG;IAC7C,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,mBAAmB,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,GAAG,CAAC,YAAY,CAAA;IAC/B,KAAK,CAAC,EAAE,GAAG,CAAC,KAAK,CAAA;CAClB,CAAA;AAED,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,GAAG,CAAC,YAAY,CAAA;IACzB,QAAQ,CAAC,EAAE,GAAG,CAAA;IACd,OAAO,CAAC,EAAE,iBAAiB,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACpD,wBAAwB,CAAC,EAAE,wBAAwB,GAAG;QACpD,KAAK,CAAC,EAAE;YACN,IAAI,EAAE,OAAO,CAAA;YACb,MAAM,EAAE;gBACN,OAAO,EAAE,MAAM,CAAA;gBACf,UAAU,EAAE,MAAM,CAAA;gBAClB,IAAI,EAAE,MAAM,CAAA;gBACZ,IAAI,EAAE,MAAM,CAAA;gBACZ,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;SACF,CAAA;KACF,CAAA;CACF,CAAA;AAQD,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,kDAgIpC"}