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.
- package/dist/cjs/Root.cjs +7 -1
- package/dist/cjs/Root.js +7 -2
- package/dist/cjs/Root.js.map +1 -1
- package/dist/cjs/Root.native.js +8 -2
- package/dist/cjs/Root.native.js.map +1 -1
- package/dist/cjs/createApp.cjs +2 -2
- package/dist/cjs/createApp.js +2 -2
- package/dist/cjs/createApp.js.map +1 -1
- package/dist/cjs/createHandleRequest.cjs +5 -1
- package/dist/cjs/createHandleRequest.js +6 -2
- package/dist/cjs/createHandleRequest.js.map +1 -1
- package/dist/cjs/createHandleRequest.native.js +5 -1
- package/dist/cjs/createHandleRequest.native.js.map +1 -1
- package/dist/cjs/fork/SSRNavigationContainer.cjs +18 -9
- package/dist/cjs/fork/SSRNavigationContainer.js +17 -6
- package/dist/cjs/fork/SSRNavigationContainer.js.map +1 -1
- package/dist/cjs/fork/SSRNavigationContainer.native.js +21 -10
- package/dist/cjs/fork/SSRNavigationContainer.native.js.map +1 -1
- package/dist/cjs/router/linkingConfig.cjs +2 -1
- package/dist/cjs/router/linkingConfig.js +2 -2
- package/dist/cjs/router/linkingConfig.js.map +1 -1
- package/dist/cjs/router/linkingConfig.native.js +2 -1
- package/dist/cjs/router/linkingConfig.native.js.map +1 -1
- package/dist/cjs/serve.cjs +67 -28
- package/dist/cjs/serve.js +68 -28
- package/dist/cjs/serve.js.map +1 -1
- package/dist/cjs/serve.native.js +96 -35
- package/dist/cjs/serve.native.js.map +1 -1
- package/dist/cjs/server/oneServe.cjs +112 -39
- package/dist/cjs/server/oneServe.js +94 -41
- package/dist/cjs/server/oneServe.js.map +2 -2
- package/dist/cjs/server/oneServe.native.js +180 -80
- package/dist/cjs/server/oneServe.native.js.map +1 -1
- package/dist/cjs/utils/evictOldest.cjs +34 -0
- package/dist/cjs/utils/evictOldest.js +29 -0
- package/dist/cjs/utils/evictOldest.js.map +6 -0
- package/dist/cjs/utils/evictOldest.native.js +34 -0
- package/dist/cjs/utils/evictOldest.native.js.map +1 -0
- package/dist/cjs/utils/isResponse.cjs +1 -1
- package/dist/cjs/utils/isResponse.js +1 -1
- package/dist/cjs/utils/isResponse.js.map +1 -1
- package/dist/cjs/utils/isResponse.native.js +1 -1
- package/dist/cjs/utils/isResponse.native.js.map +1 -1
- package/dist/cjs/vite/one-server-only.cjs +9 -6
- package/dist/cjs/vite/one-server-only.js +8 -7
- package/dist/cjs/vite/one-server-only.js.map +1 -1
- package/dist/cjs/vite/resolveResponse.cjs +17 -4
- package/dist/cjs/vite/resolveResponse.js +15 -2
- package/dist/cjs/vite/resolveResponse.js.map +1 -1
- package/dist/cjs/vite/resolveResponse.native.js +17 -4
- package/dist/cjs/vite/resolveResponse.native.js.map +1 -1
- package/dist/esm/Root.js +7 -1
- package/dist/esm/Root.js.map +1 -1
- package/dist/esm/Root.mjs +7 -1
- package/dist/esm/Root.mjs.map +1 -1
- package/dist/esm/Root.native.js +7 -1
- package/dist/esm/Root.native.js.map +1 -1
- package/dist/esm/createApp.js +2 -2
- package/dist/esm/createApp.js.map +1 -1
- package/dist/esm/createApp.mjs +2 -2
- package/dist/esm/createApp.mjs.map +1 -1
- package/dist/esm/createHandleRequest.js +6 -2
- package/dist/esm/createHandleRequest.js.map +1 -1
- package/dist/esm/createHandleRequest.mjs +5 -2
- package/dist/esm/createHandleRequest.mjs.map +1 -1
- package/dist/esm/createHandleRequest.native.js +5 -2
- package/dist/esm/createHandleRequest.native.js.map +1 -1
- package/dist/esm/fork/SSRNavigationContainer.js +17 -6
- package/dist/esm/fork/SSRNavigationContainer.js.map +1 -1
- package/dist/esm/fork/SSRNavigationContainer.mjs +18 -9
- package/dist/esm/fork/SSRNavigationContainer.mjs.map +1 -1
- package/dist/esm/fork/SSRNavigationContainer.native.js +21 -10
- package/dist/esm/fork/SSRNavigationContainer.native.js.map +1 -1
- package/dist/esm/router/linkingConfig.js +2 -1
- package/dist/esm/router/linkingConfig.js.map +1 -1
- package/dist/esm/router/linkingConfig.mjs +2 -1
- package/dist/esm/router/linkingConfig.mjs.map +1 -1
- package/dist/esm/router/linkingConfig.native.js +2 -1
- package/dist/esm/router/linkingConfig.native.js.map +1 -1
- package/dist/esm/serve.js +68 -28
- package/dist/esm/serve.js.map +1 -1
- package/dist/esm/serve.mjs +67 -28
- package/dist/esm/serve.mjs.map +1 -1
- package/dist/esm/serve.native.js +96 -35
- package/dist/esm/serve.native.js.map +1 -1
- package/dist/esm/server/oneServe.js +95 -42
- package/dist/esm/server/oneServe.js.map +2 -2
- package/dist/esm/server/oneServe.mjs +113 -40
- package/dist/esm/server/oneServe.mjs.map +1 -1
- package/dist/esm/server/oneServe.native.js +181 -81
- package/dist/esm/server/oneServe.native.js.map +1 -1
- package/dist/esm/utils/evictOldest.js +13 -0
- package/dist/esm/utils/evictOldest.js.map +6 -0
- package/dist/esm/utils/evictOldest.mjs +11 -0
- package/dist/esm/utils/evictOldest.mjs.map +1 -0
- package/dist/esm/utils/evictOldest.native.js +8 -0
- package/dist/esm/utils/evictOldest.native.js.map +1 -0
- package/dist/esm/utils/isResponse.js +1 -1
- package/dist/esm/utils/isResponse.js.map +1 -1
- package/dist/esm/utils/isResponse.mjs +1 -1
- package/dist/esm/utils/isResponse.mjs.map +1 -1
- package/dist/esm/utils/isResponse.native.js +1 -1
- package/dist/esm/utils/isResponse.native.js.map +1 -1
- package/dist/esm/vite/one-server-only.js +8 -7
- package/dist/esm/vite/one-server-only.js.map +1 -1
- package/dist/esm/vite/one-server-only.mjs +9 -6
- package/dist/esm/vite/one-server-only.mjs.map +1 -1
- package/dist/esm/vite/resolveResponse.js +15 -2
- package/dist/esm/vite/resolveResponse.js.map +1 -1
- package/dist/esm/vite/resolveResponse.mjs +16 -4
- package/dist/esm/vite/resolveResponse.mjs.map +1 -1
- package/dist/esm/vite/resolveResponse.native.js +16 -4
- package/dist/esm/vite/resolveResponse.native.js.map +1 -1
- package/package.json +9 -9
- package/src/Root.tsx +14 -1
- package/src/createApp.tsx +8 -2
- package/src/createHandleRequest.ts +9 -2
- package/src/fork/SSRNavigationContainer.tsx +30 -7
- package/src/router/linkingConfig.ts +2 -2
- package/src/serve.ts +134 -48
- package/src/server/oneServe.ts +153 -47
- package/src/utils/evictOldest.ts +13 -0
- package/src/utils/isResponse.ts +4 -4
- package/src/vite/one-server-only.tsx +25 -11
- package/src/vite/resolveResponse.ts +20 -1
- package/types/Root.d.ts.map +1 -1
- package/types/createApp.d.ts.map +1 -1
- package/types/createHandleRequest.d.ts +4 -0
- package/types/createHandleRequest.d.ts.map +1 -1
- package/types/fork/SSRNavigationContainer.d.ts.map +1 -1
- package/types/router/linkingConfig.d.ts.map +1 -1
- package/types/serve.d.ts.map +1 -1
- package/types/server/oneServe.d.ts.map +1 -1
- package/types/utils/evictOldest.d.ts +6 -0
- package/types/utils/evictOldest.d.ts.map +1 -0
- package/types/vite/one-server-only.d.ts +9 -3
- package/types/vite/one-server-only.d.ts.map +1 -1
- package/types/vite/resolveResponse.d.ts +4 -0
- 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
|
|
25
|
-
const
|
|
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
|
-
|
|
28
|
-
const
|
|
67
|
+
child.on('exit', (code, signal) => {
|
|
68
|
+
const idx = workers.indexOf(child)
|
|
69
|
+
if (idx >= 0) workers.splice(idx, 1)
|
|
29
70
|
|
|
30
|
-
|
|
71
|
+
if (code === 0 || signal === 'SIGTERM' || signal === 'SIGINT') return
|
|
31
72
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
async function startWorker(args: Parameters<typeof serve>[0]) {
|
|
76
162
|
const outDir =
|
|
77
|
-
args
|
|
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
|
|
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
|
|
184
|
+
app: args?.app,
|
|
99
185
|
...oneOptions.server,
|
|
100
186
|
...removeUndefined({
|
|
101
|
-
port: args
|
|
102
|
-
host: args
|
|
103
|
-
compress: args
|
|
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) {
|
package/src/server/oneServe.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
432
|
-
|
|
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:
|
|
435
|
-
params:
|
|
470
|
+
pathname: matchPathname,
|
|
471
|
+
params: matchParams,
|
|
436
472
|
loaderData: result.loaderData,
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
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
|
|
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
|
|
681
|
-
//
|
|
682
|
-
|
|
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
|
+
}
|
package/src/utils/isResponse.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// for some reason instanceof isnt working reliably
|
|
2
2
|
export function isResponse(res: any): res is Response {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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>,
|
package/types/Root.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|