one 1.12.1 → 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 (155) 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/cli/build.cjs +24 -10
  7. package/dist/cjs/cli/build.js +24 -9
  8. package/dist/cjs/cli/build.js.map +1 -1
  9. package/dist/cjs/cli/build.native.js +29 -14
  10. package/dist/cjs/cli/build.native.js.map +1 -1
  11. package/dist/cjs/createApp.cjs +2 -2
  12. package/dist/cjs/createApp.js +2 -2
  13. package/dist/cjs/createApp.js.map +1 -1
  14. package/dist/cjs/createHandleRequest.cjs +5 -1
  15. package/dist/cjs/createHandleRequest.js +6 -2
  16. package/dist/cjs/createHandleRequest.js.map +1 -1
  17. package/dist/cjs/createHandleRequest.native.js +5 -1
  18. package/dist/cjs/createHandleRequest.native.js.map +1 -1
  19. package/dist/cjs/fork/SSRNavigationContainer.cjs +18 -9
  20. package/dist/cjs/fork/SSRNavigationContainer.js +17 -6
  21. package/dist/cjs/fork/SSRNavigationContainer.js.map +1 -1
  22. package/dist/cjs/fork/SSRNavigationContainer.native.js +21 -10
  23. package/dist/cjs/fork/SSRNavigationContainer.native.js.map +1 -1
  24. package/dist/cjs/router/linkingConfig.cjs +2 -1
  25. package/dist/cjs/router/linkingConfig.js +2 -2
  26. package/dist/cjs/router/linkingConfig.js.map +1 -1
  27. package/dist/cjs/router/linkingConfig.native.js +2 -1
  28. package/dist/cjs/router/linkingConfig.native.js.map +1 -1
  29. package/dist/cjs/serve.cjs +67 -28
  30. package/dist/cjs/serve.js +68 -28
  31. package/dist/cjs/serve.js.map +1 -1
  32. package/dist/cjs/serve.native.js +96 -35
  33. package/dist/cjs/serve.native.js.map +1 -1
  34. package/dist/cjs/server/oneServe.cjs +112 -39
  35. package/dist/cjs/server/oneServe.js +94 -41
  36. package/dist/cjs/server/oneServe.js.map +2 -2
  37. package/dist/cjs/server/oneServe.native.js +180 -80
  38. package/dist/cjs/server/oneServe.native.js.map +1 -1
  39. package/dist/cjs/utils/evictOldest.cjs +34 -0
  40. package/dist/cjs/utils/evictOldest.js +29 -0
  41. package/dist/cjs/utils/evictOldest.js.map +6 -0
  42. package/dist/cjs/utils/evictOldest.native.js +34 -0
  43. package/dist/cjs/utils/evictOldest.native.js.map +1 -0
  44. package/dist/cjs/utils/isResponse.cjs +1 -1
  45. package/dist/cjs/utils/isResponse.js +1 -1
  46. package/dist/cjs/utils/isResponse.js.map +1 -1
  47. package/dist/cjs/utils/isResponse.native.js +1 -1
  48. package/dist/cjs/utils/isResponse.native.js.map +1 -1
  49. package/dist/cjs/vite/one-server-only.cjs +9 -6
  50. package/dist/cjs/vite/one-server-only.js +8 -7
  51. package/dist/cjs/vite/one-server-only.js.map +1 -1
  52. package/dist/cjs/vite/resolveResponse.cjs +17 -4
  53. package/dist/cjs/vite/resolveResponse.js +15 -2
  54. package/dist/cjs/vite/resolveResponse.js.map +1 -1
  55. package/dist/cjs/vite/resolveResponse.native.js +17 -4
  56. package/dist/cjs/vite/resolveResponse.native.js.map +1 -1
  57. package/dist/esm/Root.js +7 -1
  58. package/dist/esm/Root.js.map +1 -1
  59. package/dist/esm/Root.mjs +7 -1
  60. package/dist/esm/Root.mjs.map +1 -1
  61. package/dist/esm/Root.native.js +7 -1
  62. package/dist/esm/Root.native.js.map +1 -1
  63. package/dist/esm/cli/build.js +24 -9
  64. package/dist/esm/cli/build.js.map +1 -1
  65. package/dist/esm/cli/build.mjs +26 -12
  66. package/dist/esm/cli/build.mjs.map +1 -1
  67. package/dist/esm/cli/build.native.js +29 -14
  68. package/dist/esm/cli/build.native.js.map +1 -1
  69. package/dist/esm/createApp.js +2 -2
  70. package/dist/esm/createApp.js.map +1 -1
  71. package/dist/esm/createApp.mjs +2 -2
  72. package/dist/esm/createApp.mjs.map +1 -1
  73. package/dist/esm/createHandleRequest.js +6 -2
  74. package/dist/esm/createHandleRequest.js.map +1 -1
  75. package/dist/esm/createHandleRequest.mjs +5 -2
  76. package/dist/esm/createHandleRequest.mjs.map +1 -1
  77. package/dist/esm/createHandleRequest.native.js +5 -2
  78. package/dist/esm/createHandleRequest.native.js.map +1 -1
  79. package/dist/esm/fork/SSRNavigationContainer.js +17 -6
  80. package/dist/esm/fork/SSRNavigationContainer.js.map +1 -1
  81. package/dist/esm/fork/SSRNavigationContainer.mjs +18 -9
  82. package/dist/esm/fork/SSRNavigationContainer.mjs.map +1 -1
  83. package/dist/esm/fork/SSRNavigationContainer.native.js +21 -10
  84. package/dist/esm/fork/SSRNavigationContainer.native.js.map +1 -1
  85. package/dist/esm/router/linkingConfig.js +2 -1
  86. package/dist/esm/router/linkingConfig.js.map +1 -1
  87. package/dist/esm/router/linkingConfig.mjs +2 -1
  88. package/dist/esm/router/linkingConfig.mjs.map +1 -1
  89. package/dist/esm/router/linkingConfig.native.js +2 -1
  90. package/dist/esm/router/linkingConfig.native.js.map +1 -1
  91. package/dist/esm/serve.js +68 -28
  92. package/dist/esm/serve.js.map +1 -1
  93. package/dist/esm/serve.mjs +67 -28
  94. package/dist/esm/serve.mjs.map +1 -1
  95. package/dist/esm/serve.native.js +96 -35
  96. package/dist/esm/serve.native.js.map +1 -1
  97. package/dist/esm/server/oneServe.js +95 -42
  98. package/dist/esm/server/oneServe.js.map +2 -2
  99. package/dist/esm/server/oneServe.mjs +113 -40
  100. package/dist/esm/server/oneServe.mjs.map +1 -1
  101. package/dist/esm/server/oneServe.native.js +181 -81
  102. package/dist/esm/server/oneServe.native.js.map +1 -1
  103. package/dist/esm/utils/evictOldest.js +13 -0
  104. package/dist/esm/utils/evictOldest.js.map +6 -0
  105. package/dist/esm/utils/evictOldest.mjs +11 -0
  106. package/dist/esm/utils/evictOldest.mjs.map +1 -0
  107. package/dist/esm/utils/evictOldest.native.js +8 -0
  108. package/dist/esm/utils/evictOldest.native.js.map +1 -0
  109. package/dist/esm/utils/isResponse.js +1 -1
  110. package/dist/esm/utils/isResponse.js.map +1 -1
  111. package/dist/esm/utils/isResponse.mjs +1 -1
  112. package/dist/esm/utils/isResponse.mjs.map +1 -1
  113. package/dist/esm/utils/isResponse.native.js +1 -1
  114. package/dist/esm/utils/isResponse.native.js.map +1 -1
  115. package/dist/esm/vite/one-server-only.js +8 -7
  116. package/dist/esm/vite/one-server-only.js.map +1 -1
  117. package/dist/esm/vite/one-server-only.mjs +9 -6
  118. package/dist/esm/vite/one-server-only.mjs.map +1 -1
  119. package/dist/esm/vite/resolveResponse.js +15 -2
  120. package/dist/esm/vite/resolveResponse.js.map +1 -1
  121. package/dist/esm/vite/resolveResponse.mjs +16 -4
  122. package/dist/esm/vite/resolveResponse.mjs.map +1 -1
  123. package/dist/esm/vite/resolveResponse.native.js +16 -4
  124. package/dist/esm/vite/resolveResponse.native.js.map +1 -1
  125. package/package.json +13 -11
  126. package/src/Root.tsx +14 -1
  127. package/src/cli/build.ts +38 -12
  128. package/src/createApp.tsx +8 -2
  129. package/src/createHandleRequest.ts +9 -2
  130. package/src/fork/SSRNavigationContainer.tsx +30 -7
  131. package/src/router/linkingConfig.ts +2 -2
  132. package/src/serve.ts +134 -48
  133. package/src/server/oneServe.ts +153 -47
  134. package/src/utils/evictOldest.ts +13 -0
  135. package/src/utils/isResponse.ts +4 -4
  136. package/src/vite/one-server-only.tsx +25 -11
  137. package/src/vite/resolveResponse.ts +20 -1
  138. package/src/vite/types.ts +20 -2
  139. package/types/Root.d.ts.map +1 -1
  140. package/types/cli/build.d.ts.map +1 -1
  141. package/types/createApp.d.ts.map +1 -1
  142. package/types/createHandleRequest.d.ts +4 -0
  143. package/types/createHandleRequest.d.ts.map +1 -1
  144. package/types/fork/SSRNavigationContainer.d.ts.map +1 -1
  145. package/types/router/linkingConfig.d.ts.map +1 -1
  146. package/types/serve.d.ts.map +1 -1
  147. package/types/server/oneServe.d.ts.map +1 -1
  148. package/types/utils/evictOldest.d.ts +6 -0
  149. package/types/utils/evictOldest.d.ts.map +1 -0
  150. package/types/vite/one-server-only.d.ts +9 -3
  151. package/types/vite/one-server-only.d.ts.map +1 -1
  152. package/types/vite/resolveResponse.d.ts +4 -0
  153. package/types/vite/resolveResponse.d.ts.map +1 -1
  154. package/types/vite/types.d.ts +18 -2
  155. package/types/vite/types.d.ts.map +1 -1
package/src/Root.tsx CHANGED
@@ -27,12 +27,25 @@ import { ServerLocationContext } from './router/serverLocationContext'
27
27
  import { useInitializeOneRouter } from './router/useInitializeOneRouter'
28
28
  import { useViteRoutes } from './router/useViteRoutes'
29
29
  import type { GlobbedRouteImports } from './types'
30
+ import { evictOldest } from './utils/evictOldest'
30
31
  import { ServerRenderID } from './useServerHeadInsertion'
31
32
  import { PreloadLinks } from './views/PreloadLinks'
32
33
  import { RootErrorBoundary } from './views/RootErrorBoundary'
33
34
  import { ScrollBehavior } from './views/ScrollBehavior'
34
35
  import type { One } from './vite/types'
35
36
 
37
+ // cache URL objects for SSR to avoid per-render allocation
38
+ // keyed by path string, evicted when cache grows too large
39
+ const ssrLocationCache = new Map<string, URL>()
40
+ function getCachedSSRLocation(path: string): URL {
41
+ let url = ssrLocationCache.get(path)
42
+ if (url) return url
43
+ url = new URL(path, getURL())
44
+ evictOldest(ssrLocationCache, 5000, 1000)
45
+ ssrLocationCache.set(path, url)
46
+ return url
47
+ }
48
+
36
49
  type RootProps = Omit<InnerProps, 'context'> & {
37
50
  onRenderId?: (id: string) => void
38
51
  path: string
@@ -87,7 +100,7 @@ export function Root(props: RootProps) {
87
100
  const location =
88
101
  typeof window !== 'undefined' && window.location
89
102
  ? new URL(path || window.location.href || '/', window.location.href)
90
- : new URL(path || '/', getURL())
103
+ : getCachedSSRLocation(path || '/')
91
104
 
92
105
  const store = useInitializeOneRouter(context, location)
93
106
  const userScheme = useUserScheme()
package/src/cli/build.ts CHANGED
@@ -24,7 +24,7 @@ import { buildVercelOutputDirectory } from '../vercel/build/buildVercelOutputDir
24
24
  import { getManifest } from '../vite/getManifest'
25
25
  import { loadUserOneOptions } from '../vite/loadConfig'
26
26
  import { runWithAsyncLocalContext } from '../vite/one-server-only'
27
- import type { One, RouteInfo } from '../vite/types'
27
+ import type { DeployConfig, DeployTarget, One, RouteInfo } from '../vite/types'
28
28
  import { buildPage, printBuildTimings } from './buildPage'
29
29
  import { checkNodeVersion } from './checkNodeVersion'
30
30
  import { getWorkerPool, terminateWorkerPool } from './workerPool'
@@ -35,6 +35,25 @@ import { getCriticalCSSOutputPaths } from '../vite/plugins/criticalCSSPlugin'
35
35
 
36
36
  const { ensureDir, writeJSON } = FSExtra
37
37
 
38
+ function normalizeDeploy(
39
+ deploy?: DeployTarget | DeployConfig
40
+ ): { target: DeployTarget; url?: string } | undefined {
41
+ if (!deploy) return undefined
42
+ if (typeof deploy === 'string') return { target: deploy }
43
+ return deploy
44
+ }
45
+
46
+ // reads package.json name, strips npm scope prefix for use as cloudflare worker name
47
+ async function getCloudflareProjectName(root: string): Promise<string> {
48
+ try {
49
+ const pkg = JSON.parse(await FSExtra.readFile(join(root, 'package.json'), 'utf-8'))
50
+ if (pkg.name) {
51
+ return pkg.name.replace(/^@[^/]+\//, '')
52
+ }
53
+ } catch {}
54
+ return 'one-app'
55
+ }
56
+
38
57
  // concurrency limit for parallel page builds
39
58
  // can be overridden with ONE_BUILD_CONCURRENCY env var
40
59
  // default based on CPU count for I/O parallelism benefits
@@ -117,6 +136,22 @@ export async function build(args: {
117
136
  process.env.ONE_DEFAULT_RENDER_MODE = oneOptions.web.defaultRenderMode
118
137
  }
119
138
 
139
+ const deployConfig = normalizeDeploy(oneOptions.web?.deploy)
140
+
141
+ // auto-detect ONE_SERVER_URL from deploy config when not explicitly set
142
+ if (!process.env.ONE_SERVER_URL && deployConfig) {
143
+ const url =
144
+ deployConfig.url ??
145
+ (deployConfig.target === 'cloudflare'
146
+ ? `https://${await getCloudflareProjectName(process.cwd())}.workers.dev`
147
+ : undefined)
148
+
149
+ if (url) {
150
+ process.env.ONE_SERVER_URL = url
151
+ console.info(`\n ☁️ ONE_SERVER_URL: ${url}\n`)
152
+ }
153
+ }
154
+
120
155
  // respect vite's build.outDir config, default to 'dist'
121
156
  const outDir = viteLoadedConfig?.config?.build?.outDir ?? 'dist'
122
157
 
@@ -981,7 +1016,7 @@ export async function build(args: {
981
1016
 
982
1017
  const postBuildLogs: string[] = []
983
1018
 
984
- const platform = oneOptions.web?.deploy
1019
+ const platform = deployConfig?.target
985
1020
 
986
1021
  if (platform) {
987
1022
  postBuildLogs.push(`[one.build] platform ${platform}`)
@@ -1205,16 +1240,7 @@ export default {
1205
1240
 
1206
1241
  // Use jsonc for wrangler config (recommended for new projects)
1207
1242
  // Use assets with run_worker_first so all requests go through worker (enables middleware on SSG pages)
1208
- // Read project name from package.json if available
1209
- let projectName = 'one-app'
1210
- try {
1211
- const pkgPath = join(options.root, 'package.json')
1212
- const pkg = JSON.parse(await FSExtra.readFile(pkgPath, 'utf-8'))
1213
- if (pkg.name) {
1214
- // strip scope prefix for CF worker name
1215
- projectName = pkg.name.replace(/^@[^/]+\//, '')
1216
- }
1217
- } catch {}
1243
+ const projectName = await getCloudflareProjectName(options.root)
1218
1244
 
1219
1245
  const wranglerConfig = `{
1220
1246
  "name": ${JSON.stringify(projectName)},
package/src/createApp.tsx CHANGED
@@ -41,7 +41,10 @@ export function createApp(options: CreateAppProps) {
41
41
  render: async (props: RenderAppProps) => {
42
42
  // set render mode env before setup so users can conditionally skip setup in ssg/spa
43
43
  const renderMode = props.mode === 'spa-shell' ? 'spa' : props.mode
44
- process.env.ONE_RENDER_MODE = renderMode
44
+ // only set if changed to avoid process.env setter overhead
45
+ if (process.env.ONE_RENDER_MODE !== renderMode) {
46
+ process.env.ONE_RENDER_MODE = renderMode
47
+ }
45
48
 
46
49
  if (!setupDone && options.getSetupPromise) {
47
50
  await options.getSetupPromise()
@@ -165,7 +168,10 @@ export function createApp(options: CreateAppProps) {
165
168
  // streaming SSR - returns ReadableStream, no post-processing
166
169
  renderStream: async (props: RenderAppProps): Promise<ReadableStream> => {
167
170
  const renderMode = props.mode === 'spa-shell' ? 'spa' : props.mode
168
- process.env.ONE_RENDER_MODE = renderMode
171
+ // only set if changed to avoid process.env setter overhead
172
+ if (process.env.ONE_RENDER_MODE !== renderMode) {
173
+ process.env.ONE_RENDER_MODE = renderMode
174
+ }
169
175
 
170
176
  if (!setupDone && options.getSetupPromise) {
171
177
  await options.getSetupPromise()
@@ -290,12 +290,19 @@ export async function resolvePageRoute(
290
290
  })
291
291
  }
292
292
 
293
+ // weakmap cache to avoid re-parsing the same request URL
294
+ const _urlCache = new WeakMap<Request, URL>()
295
+
293
296
  export function getURLfromRequestURL(request: Request) {
297
+ let url = _urlCache.get(request)
298
+ if (url) return url
294
299
  const urlString = request.url || ''
295
- return new URL(
300
+ url = new URL(
296
301
  urlString || '',
297
302
  request.headers.get('host') ? `http://${request.headers.get('host')}` : ''
298
303
  )
304
+ _urlCache.set(request, url)
305
+ return url
299
306
  }
300
307
 
301
308
  function compileRouteRegex(route: RouteInfo): RouteInfoCompiled {
@@ -456,7 +463,7 @@ export function createHandleRequest(
456
463
  }
457
464
  }
458
465
 
459
- function getLoaderParams(
466
+ export function getLoaderParams(
460
467
  url: URL,
461
468
  config: { compiledRegex: RegExp; routeKeys: Record<string, string> }
462
469
  ) {
@@ -49,14 +49,37 @@ const getPartialState = (state: any): any => {
49
49
  }
50
50
  }
51
51
 
52
- // cache by initialState reference
53
- let _cachedState: any = null
54
- let _cachedCtx: any = null
52
+ // cache state contexts by navigation state identity to avoid deep clone thrashing
53
+ // under concurrent SSR requests with different URLs
54
+ const stateCtxCache = new WeakMap<
55
+ object,
56
+ {
57
+ state: any
58
+ getKey: () => string | undefined
59
+ setKey: () => void
60
+ getState: () => any
61
+ setState: () => void
62
+ getIsInitial: () => boolean
63
+ }
64
+ >()
55
65
 
56
66
  function getStateContext(initialState: any) {
57
- if (_cachedState === initialState && _cachedCtx) return _cachedCtx
67
+ if (!initialState) {
68
+ return {
69
+ state: undefined,
70
+ getKey: () => undefined as string | undefined,
71
+ setKey: noop,
72
+ getState: () => undefined,
73
+ setState: noop,
74
+ getIsInitial: () => true,
75
+ }
76
+ }
77
+
78
+ const cached = stateCtxCache.get(initialState)
79
+ if (cached) return cached
80
+
58
81
  const partial = getPartialState(initialState)
59
- _cachedCtx = {
82
+ const ctx = {
60
83
  state: partial,
61
84
  getKey: () => undefined as string | undefined,
62
85
  setKey: noop,
@@ -64,8 +87,8 @@ function getStateContext(initialState: any) {
64
87
  setState: noop,
65
88
  getIsInitial: () => true,
66
89
  }
67
- _cachedState = initialState
68
- return _cachedCtx
90
+ stateCtxCache.set(initialState, ctx)
91
+ return ctx
69
92
  }
70
93
 
71
94
  export function SSRNavigationContainer({
@@ -1,5 +1,6 @@
1
1
  import { isWebClient } from '../constants'
2
2
  import type { OneRouter } from '../interfaces/router'
3
+ import { evictOldest } from '../utils/evictOldest'
3
4
  import {
4
5
  getLinkingConfig as createLinkingConfig,
5
6
  type OneLinkingOptions,
@@ -62,8 +63,7 @@ export function getSSRInitialState(
62
63
  path,
63
64
  cachedBaseLinkingConfig.config
64
65
  )
65
- // bound cache to prevent memory growth on sites with many unique URLs
66
- if (ssrStateCache.size > 10000) ssrStateCache.clear()
66
+ evictOldest(ssrStateCache, 5000, 1000)
67
67
  ssrStateCache.set(path, state)
68
68
  return state
69
69
  }
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) {