nmtjs 0.16.1 → 0.17.0-beta.2

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 (224) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +41 -1
  3. package/dist/index.d.ts +22 -161
  4. package/dist/index.js +24 -53
  5. package/dist/index.js.map +1 -1
  6. package/package.json +11 -63
  7. package/src/index.ts +71 -91
  8. package/bin/cli.js +0 -2
  9. package/dist/cli.d.ts +0 -1
  10. package/dist/cli.js +0 -137
  11. package/dist/cli.js.map +0 -1
  12. package/dist/codegen.d.ts +0 -5
  13. package/dist/codegen.js +0 -18
  14. package/dist/codegen.js.map +0 -1
  15. package/dist/config.d.ts +0 -47
  16. package/dist/config.js +0 -16
  17. package/dist/config.js.map +0 -1
  18. package/dist/entrypoints/cli.d.ts +0 -1
  19. package/dist/entrypoints/cli.js +0 -12
  20. package/dist/entrypoints/cli.js.map +0 -1
  21. package/dist/entrypoints/main.d.ts +0 -5
  22. package/dist/entrypoints/main.js +0 -248
  23. package/dist/entrypoints/main.js.map +0 -1
  24. package/dist/entrypoints/thread.d.ts +0 -16
  25. package/dist/entrypoints/thread.js +0 -182
  26. package/dist/entrypoints/thread.js.map +0 -1
  27. package/dist/entrypoints/worker.d.ts +0 -9
  28. package/dist/entrypoints/worker.js +0 -143
  29. package/dist/entrypoints/worker.js.map +0 -1
  30. package/dist/resolver.d.ts +0 -2
  31. package/dist/resolver.js +0 -7
  32. package/dist/resolver.js.map +0 -1
  33. package/dist/runtime/application/index.d.ts +0 -1
  34. package/dist/runtime/application/index.js +0 -2
  35. package/dist/runtime/application/index.js.map +0 -1
  36. package/dist/runtime/constants.d.ts +0 -8
  37. package/dist/runtime/constants.js +0 -5
  38. package/dist/runtime/constants.js.map +0 -1
  39. package/dist/runtime/enums.d.ts +0 -10
  40. package/dist/runtime/enums.js +0 -13
  41. package/dist/runtime/enums.js.map +0 -1
  42. package/dist/runtime/hooks.d.ts +0 -1
  43. package/dist/runtime/hooks.js +0 -2
  44. package/dist/runtime/hooks.js.map +0 -1
  45. package/dist/runtime/index.d.ts +0 -28
  46. package/dist/runtime/index.js +0 -29
  47. package/dist/runtime/index.js.map +0 -1
  48. package/dist/runtime/injectables.d.ts +0 -69
  49. package/dist/runtime/injectables.js +0 -40
  50. package/dist/runtime/injectables.js.map +0 -1
  51. package/dist/runtime/jobs/job.d.ts +0 -146
  52. package/dist/runtime/jobs/job.js +0 -89
  53. package/dist/runtime/jobs/job.js.map +0 -1
  54. package/dist/runtime/jobs/manager.d.ts +0 -131
  55. package/dist/runtime/jobs/manager.js +0 -309
  56. package/dist/runtime/jobs/manager.js.map +0 -1
  57. package/dist/runtime/jobs/router.d.ts +0 -296
  58. package/dist/runtime/jobs/router.js +0 -458
  59. package/dist/runtime/jobs/router.js.map +0 -1
  60. package/dist/runtime/jobs/runner.d.ts +0 -103
  61. package/dist/runtime/jobs/runner.js +0 -425
  62. package/dist/runtime/jobs/runner.js.map +0 -1
  63. package/dist/runtime/jobs/step.d.ts +0 -26
  64. package/dist/runtime/jobs/step.js +0 -18
  65. package/dist/runtime/jobs/step.js.map +0 -1
  66. package/dist/runtime/jobs/types.d.ts +0 -86
  67. package/dist/runtime/jobs/types.js +0 -5
  68. package/dist/runtime/jobs/types.js.map +0 -1
  69. package/dist/runtime/metrics/metric.d.ts +0 -8
  70. package/dist/runtime/metrics/metric.js +0 -11
  71. package/dist/runtime/metrics/metric.js.map +0 -1
  72. package/dist/runtime/metrics/registry.d.ts +0 -3
  73. package/dist/runtime/metrics/registry.js +0 -5
  74. package/dist/runtime/metrics/registry.js.map +0 -1
  75. package/dist/runtime/metrics/server.d.ts +0 -7
  76. package/dist/runtime/metrics/server.js +0 -53
  77. package/dist/runtime/metrics/server.js.map +0 -1
  78. package/dist/runtime/plugin.d.ts +0 -2
  79. package/dist/runtime/plugin.js +0 -2
  80. package/dist/runtime/plugin.js.map +0 -1
  81. package/dist/runtime/runtime.d.ts +0 -27
  82. package/dist/runtime/runtime.js +0 -77
  83. package/dist/runtime/runtime.js.map +0 -1
  84. package/dist/runtime/scheduler/index.d.ts +0 -22
  85. package/dist/runtime/scheduler/index.js +0 -20
  86. package/dist/runtime/scheduler/index.js.map +0 -1
  87. package/dist/runtime/server/applications.d.ts +0 -91
  88. package/dist/runtime/server/applications.js +0 -171
  89. package/dist/runtime/server/applications.js.map +0 -1
  90. package/dist/runtime/server/config.d.ts +0 -148
  91. package/dist/runtime/server/config.js +0 -33
  92. package/dist/runtime/server/config.js.map +0 -1
  93. package/dist/runtime/server/environment.d.ts +0 -43
  94. package/dist/runtime/server/environment.js +0 -33
  95. package/dist/runtime/server/environment.js.map +0 -1
  96. package/dist/runtime/server/error-policy.d.ts +0 -62
  97. package/dist/runtime/server/error-policy.js +0 -73
  98. package/dist/runtime/server/error-policy.js.map +0 -1
  99. package/dist/runtime/server/hmr-coordinator.d.ts +0 -37
  100. package/dist/runtime/server/hmr-coordinator.js +0 -94
  101. package/dist/runtime/server/hmr-coordinator.js.map +0 -1
  102. package/dist/runtime/server/jobs.d.ts +0 -54
  103. package/dist/runtime/server/jobs.js +0 -176
  104. package/dist/runtime/server/jobs.js.map +0 -1
  105. package/dist/runtime/server/lifecycle.d.ts +0 -85
  106. package/dist/runtime/server/lifecycle.js +0 -177
  107. package/dist/runtime/server/lifecycle.js.map +0 -1
  108. package/dist/runtime/server/managed-worker.d.ts +0 -158
  109. package/dist/runtime/server/managed-worker.js +0 -420
  110. package/dist/runtime/server/managed-worker.js.map +0 -1
  111. package/dist/runtime/server/pool-manager.d.ts +0 -85
  112. package/dist/runtime/server/pool-manager.js +0 -129
  113. package/dist/runtime/server/pool-manager.js.map +0 -1
  114. package/dist/runtime/server/proxy.d.ts +0 -21
  115. package/dist/runtime/server/proxy.js +0 -76
  116. package/dist/runtime/server/proxy.js.map +0 -1
  117. package/dist/runtime/server/server.d.ts +0 -60
  118. package/dist/runtime/server/server.js +0 -163
  119. package/dist/runtime/server/server.js.map +0 -1
  120. package/dist/runtime/server/types.d.ts +0 -31
  121. package/dist/runtime/server/types.js +0 -2
  122. package/dist/runtime/server/types.js.map +0 -1
  123. package/dist/runtime/server/worker-pool.d.ts +0 -121
  124. package/dist/runtime/server/worker-pool.js +0 -171
  125. package/dist/runtime/server/worker-pool.js.map +0 -1
  126. package/dist/runtime/store/index.d.ts +0 -3
  127. package/dist/runtime/store/index.js +0 -23
  128. package/dist/runtime/store/index.js.map +0 -1
  129. package/dist/runtime/subscription/manager.d.ts +0 -49
  130. package/dist/runtime/subscription/manager.js +0 -170
  131. package/dist/runtime/subscription/manager.js.map +0 -1
  132. package/dist/runtime/subscription/redis.d.ts +0 -18
  133. package/dist/runtime/subscription/redis.js +0 -119
  134. package/dist/runtime/subscription/redis.js.map +0 -1
  135. package/dist/runtime/types.d.ts +0 -95
  136. package/dist/runtime/types.js +0 -2
  137. package/dist/runtime/types.js.map +0 -1
  138. package/dist/runtime/workers/application.d.ts +0 -29
  139. package/dist/runtime/workers/application.js +0 -77
  140. package/dist/runtime/workers/application.js.map +0 -1
  141. package/dist/runtime/workers/base.d.ts +0 -14
  142. package/dist/runtime/workers/base.js +0 -41
  143. package/dist/runtime/workers/base.js.map +0 -1
  144. package/dist/runtime/workers/cli.d.ts +0 -1
  145. package/dist/runtime/workers/cli.js +0 -2
  146. package/dist/runtime/workers/cli.js.map +0 -1
  147. package/dist/runtime/workers/job.d.ts +0 -20
  148. package/dist/runtime/workers/job.js +0 -170
  149. package/dist/runtime/workers/job.js.map +0 -1
  150. package/dist/vite/builder.d.ts +0 -5
  151. package/dist/vite/builder.js +0 -79
  152. package/dist/vite/builder.js.map +0 -1
  153. package/dist/vite/config.d.ts +0 -28
  154. package/dist/vite/config.js +0 -22
  155. package/dist/vite/config.js.map +0 -1
  156. package/dist/vite/plugins.d.ts +0 -2
  157. package/dist/vite/plugins.js +0 -43
  158. package/dist/vite/plugins.js.map +0 -1
  159. package/dist/vite/runners/worker.d.ts +0 -3
  160. package/dist/vite/runners/worker.js +0 -34
  161. package/dist/vite/runners/worker.js.map +0 -1
  162. package/dist/vite/server.d.ts +0 -3
  163. package/dist/vite/server.js +0 -36
  164. package/dist/vite/server.js.map +0 -1
  165. package/dist/vite/servers/main.d.ts +0 -8
  166. package/dist/vite/servers/main.js +0 -24
  167. package/dist/vite/servers/main.js.map +0 -1
  168. package/dist/vite/servers/worker.d.ts +0 -13
  169. package/dist/vite/servers/worker.js +0 -124
  170. package/dist/vite/servers/worker.js.map +0 -1
  171. package/src/cli.ts +0 -162
  172. package/src/codegen.ts +0 -30
  173. package/src/config.ts +0 -69
  174. package/src/entrypoints/cli.ts +0 -13
  175. package/src/entrypoints/main.ts +0 -317
  176. package/src/entrypoints/thread.ts +0 -223
  177. package/src/entrypoints/worker.ts +0 -174
  178. package/src/resolver.ts +0 -7
  179. package/src/runtime/application/index.ts +0 -1
  180. package/src/runtime/constants.ts +0 -13
  181. package/src/runtime/enums.ts +0 -12
  182. package/src/runtime/hooks.ts +0 -1
  183. package/src/runtime/index.ts +0 -29
  184. package/src/runtime/injectables.ts +0 -101
  185. package/src/runtime/jobs/job.ts +0 -467
  186. package/src/runtime/jobs/manager.ts +0 -454
  187. package/src/runtime/jobs/router.ts +0 -967
  188. package/src/runtime/jobs/runner.ts +0 -583
  189. package/src/runtime/jobs/step.ts +0 -66
  190. package/src/runtime/jobs/types.ts +0 -116
  191. package/src/runtime/metrics/metric.ts +0 -18
  192. package/src/runtime/metrics/registry.ts +0 -11
  193. package/src/runtime/metrics/server.ts +0 -69
  194. package/src/runtime/plugin.ts +0 -2
  195. package/src/runtime/runtime.ts +0 -103
  196. package/src/runtime/scheduler/index.ts +0 -39
  197. package/src/runtime/server/applications.ts +0 -264
  198. package/src/runtime/server/config.ts +0 -199
  199. package/src/runtime/server/environment.ts +0 -71
  200. package/src/runtime/server/error-policy.ts +0 -132
  201. package/src/runtime/server/hmr-coordinator.ts +0 -116
  202. package/src/runtime/server/jobs.ts +0 -266
  203. package/src/runtime/server/lifecycle.ts +0 -234
  204. package/src/runtime/server/managed-worker.ts +0 -546
  205. package/src/runtime/server/pool-manager.ts +0 -187
  206. package/src/runtime/server/proxy.ts +0 -119
  207. package/src/runtime/server/server.ts +0 -218
  208. package/src/runtime/server/types.ts +0 -32
  209. package/src/runtime/server/worker-pool.ts +0 -275
  210. package/src/runtime/store/index.ts +0 -30
  211. package/src/runtime/subscription/manager.ts +0 -307
  212. package/src/runtime/subscription/redis.ts +0 -159
  213. package/src/runtime/types.ts +0 -84
  214. package/src/runtime/workers/application.ts +0 -133
  215. package/src/runtime/workers/base.ts +0 -65
  216. package/src/runtime/workers/cli.ts +0 -0
  217. package/src/runtime/workers/job.ts +0 -142
  218. package/src/vite/builder.ts +0 -94
  219. package/src/vite/config.ts +0 -45
  220. package/src/vite/plugins.ts +0 -53
  221. package/src/vite/runners/worker.ts +0 -55
  222. package/src/vite/server.ts +0 -55
  223. package/src/vite/servers/main.ts +0 -41
  224. package/src/vite/servers/worker.ts +0 -177
@@ -1,266 +0,0 @@
1
- import { inspect } from 'node:util'
2
-
3
- import type { Logger } from '@nmtjs/core'
4
- import type { RedisClient } from 'bullmq'
5
- import { UnrecoverableError, Worker } from 'bullmq'
6
-
7
- import type { AnyJob } from '../jobs/job.ts'
8
- import type { Store, WorkerJobTask } from '../types.ts'
9
- import type { ServerConfig } from './config.ts'
10
- import type { ErrorPolicy } from './error-policy.ts'
11
- import type { ApplicationServerWorkerConfig } from './types.ts'
12
- import type {
13
- ManagedWorkerFactory,
14
- WorkerPoolConfig,
15
- WorkerPoolFactory,
16
- } from './worker-pool.ts'
17
- import { WorkerType } from '../enums.ts'
18
- import { getJobQueueName } from '../jobs/manager.ts'
19
- import { JobRunnersPool } from './worker-pool.ts'
20
-
21
- type JobsPoolConfig = Exclude<ServerConfig['jobs'], undefined>['pools']
22
-
23
- function enrichBullMqErrorStack(error: unknown): Error {
24
- const normalized =
25
- error instanceof Error
26
- ? error
27
- : new Error(
28
- typeof error === 'string' ? error : inspect(error, false, 20, false),
29
- )
30
-
31
- normalized.stack = inspect(normalized, false, 20, false)
32
-
33
- return normalized
34
- }
35
-
36
- function getJobsByPool(jobs: Iterable<AnyJob>): Map<string, AnyJob[]> {
37
- const jobsByPool = new Map<string, AnyJob[]>()
38
-
39
- for (const job of jobs) {
40
- const poolJobs = jobsByPool.get(job.options.pool)
41
- if (poolJobs) {
42
- poolJobs.push(job)
43
- } else {
44
- jobsByPool.set(job.options.pool, [job])
45
- }
46
- }
47
-
48
- return jobsByPool
49
- }
50
-
51
- function assertActiveJobPoolsConfigured(
52
- jobsByPool: Map<string, AnyJob[]>,
53
- pools: JobsPoolConfig,
54
- ) {
55
- const missingPoolMessages = [...jobsByPool]
56
- .filter(([poolName]) => !pools[poolName])
57
- .flatMap(([poolName, jobs]) =>
58
- jobs.map((job) => `${job.name} -> ${poolName}`),
59
- )
60
-
61
- if (missingPoolMessages.length > 0) {
62
- throw new Error(
63
- `Invalid jobs pool configuration: missing pool config for jobs: ${missingPoolMessages.join(', ')}`,
64
- )
65
- }
66
- }
67
-
68
- /**
69
- * ApplicationServerJobs manages job worker pools and BullMQ queue workers.
70
- *
71
- * Changes from the old Pool-based implementation:
72
- * - Uses JobRunnersPool (extends WorkerPool) for worker management
73
- * - Uses ManagedWorker for restart logic and state tracking
74
- * - Integrates with ErrorPolicy for restart decisions
75
- * - Proper health tracking per job pool
76
- */
77
- export class ApplicationServerJobs {
78
- /**
79
- * BullMQ workers - one per job (dedicated queues)
80
- */
81
- queueWorkers = new Set<Worker>()
82
-
83
- jobs: Map<string, AnyJob>
84
-
85
- /**
86
- * Shared resource pools by configured pool name.
87
- * All jobs using the same pool name share the same pool for resource management.
88
- */
89
- protected pools = new Map<string, JobRunnersPool>()
90
-
91
- constructor(
92
- readonly params: {
93
- logger: Logger
94
- serverConfig: ServerConfig
95
- workerConfig: ApplicationServerWorkerConfig
96
- store: Store
97
- errorPolicy: ErrorPolicy
98
- workerFactory: ManagedWorkerFactory
99
- poolFactory: WorkerPoolFactory
100
- },
101
- ) {
102
- this.jobs = params.serverConfig.jobs
103
- ? params.serverConfig.jobs.jobs
104
- : new Map()
105
- }
106
-
107
- async start() {
108
- const {
109
- logger,
110
- serverConfig,
111
- workerConfig,
112
- store,
113
- errorPolicy,
114
- workerFactory,
115
- } = this.params
116
- const jobsConfig = serverConfig.jobs
117
-
118
- if (!jobsConfig) {
119
- logger.debug('Jobs are not configured, skipping')
120
- return
121
- }
122
-
123
- const jobsByPool = getJobsByPool(this.jobs.values())
124
- assertActiveJobPoolsConfigured(jobsByPool, jobsConfig.pools)
125
-
126
- // Step 1: Initialize shared resource pools used by active jobs
127
- for (const poolName of jobsByPool.keys()) {
128
- const poolConfig = jobsConfig.pools[poolName]!
129
-
130
- // Create a JobRunnersPool for this pool
131
- const config: WorkerPoolConfig = {
132
- name: `job-pool-${poolName}`,
133
- workerType: WorkerType.Job,
134
- path: workerConfig.path,
135
- workerData: { ...workerConfig.workerData },
136
- onWorker: workerConfig.worker,
137
- }
138
-
139
- const pool = new JobRunnersPool(
140
- config,
141
- errorPolicy,
142
- workerFactory,
143
- logger,
144
- )
145
-
146
- // Add workers to the pool
147
- for (let i = 0; i < poolConfig.threads; i++) {
148
- pool.add({ runtime: { type: 'jobs', jobWorkerPool: poolName } }, i)
149
- }
150
-
151
- await pool.startAll()
152
- this.pools.set(poolName, pool)
153
-
154
- logger.info(
155
- {
156
- pool: poolName,
157
- threads: poolConfig.threads,
158
- jobsPerThread: poolConfig.jobs,
159
- },
160
- 'Job runner pool started',
161
- )
162
- }
163
-
164
- // Step 2: Create a dedicated BullMQ Worker for each job
165
- // Calculate how many jobs use each pool for fair concurrency distribution
166
- for (const job of this.jobs.values()) {
167
- const queueName = getJobQueueName(job)
168
- const poolName = job.options.pool
169
- const pool = this.pools.get(poolName)
170
-
171
- if (!pool) {
172
- throw new Error(`Job "${job.name}" pool "${poolName}" is not started`)
173
- }
174
-
175
- const poolConfig = jobsConfig.pools[poolName]!
176
- const poolCapacity = poolConfig.threads * poolConfig.jobs
177
- const jobCountInPool = jobsByPool.get(poolName)?.length ?? 1
178
- // Use job-specific concurrency if provided, otherwise distribute pool capacity evenly
179
- const defaultConcurrency = Math.max(
180
- 1,
181
- Math.floor(poolCapacity / jobCountInPool),
182
- )
183
- const concurrency = job.options.concurrency ?? defaultConcurrency
184
-
185
- const queueWorker = new Worker(
186
- queueName,
187
- async (bullJob) => {
188
- const task: WorkerJobTask = {
189
- jobId: String(bullJob.id ?? ''),
190
- jobName: bullJob.name,
191
- data: bullJob.data,
192
- }
193
-
194
- const result = await pool.run(task)
195
- switch (result.type) {
196
- case 'success':
197
- return result.result
198
- case 'unrecoverable_error': {
199
- const error = enrichBullMqErrorStack(result.error)
200
- const unrecoverableError = new UnrecoverableError(error.message)
201
- unrecoverableError.stack = error.stack
202
- throw unrecoverableError
203
- }
204
- case 'job_not_found':
205
- case 'queue_job_not_found':
206
- throw new UnrecoverableError(result.type)
207
- case 'error':
208
- console.error(result.error)
209
- throw enrichBullMqErrorStack(result.error)
210
- default:
211
- throw new UnrecoverableError('Unknown job task result')
212
- }
213
- },
214
- { connection: store as unknown as RedisClient, concurrency },
215
- )
216
-
217
- this.queueWorkers.add(queueWorker)
218
- logger.info(
219
- { job: job.name, queue: queueName, pool: poolName, concurrency },
220
- 'Job queue worker started',
221
- )
222
- }
223
- }
224
-
225
- async stop() {
226
- const { logger } = this.params
227
-
228
- // Force-close BullMQ workers immediately — stops accepting new jobs
229
- // and doesn't wait for in-flight processor callbacks.
230
- // We must use force=true on the first call because BullMQ caches the
231
- // closing promise: a second call with force=true returns the original
232
- // (non-force) promise, making it ineffective.
233
- // Interrupted jobs will be retried via BullMQ's stalled job mechanism.
234
- await Promise.all(
235
- [...this.queueWorkers].map(async (worker) => {
236
- try {
237
- await worker.close(true)
238
- } catch (error) {
239
- logger.warn({ error }, 'Failed to close BullMQ worker')
240
- }
241
- }),
242
- )
243
- this.queueWorkers.clear()
244
-
245
- // Stop job runner thread pools — sends 'stop' to threads, waits for
246
- // graceful exit with timeout, force-kills if needed.
247
- // Pending pool.run() calls will be rejected by ManagedWorker.stop().
248
- await Promise.all(
249
- Array.from(this.pools.values()).map(async (pool) => {
250
- try {
251
- await pool.stopAll()
252
- } catch (error) {
253
- logger.warn({ error }, 'Failed to stop job pool')
254
- }
255
- }),
256
- )
257
- this.pools.clear()
258
- }
259
-
260
- /**
261
- * Get a job pool by name.
262
- */
263
- getPool(poolName: string): JobRunnersPool | undefined {
264
- return this.pools.get(poolName)
265
- }
266
- }
@@ -1,234 +0,0 @@
1
- import EventEmitter from 'node:events'
2
-
3
- import type { Logger } from '@nmtjs/core'
4
-
5
- import type { RuntimeEnvironment } from './environment.ts'
6
- import type { ApplicationServer } from './server.ts'
7
-
8
- /**
9
- * States in the server lifecycle state machine.
10
- */
11
- export type LifecycleState =
12
- | 'idle'
13
- | 'starting'
14
- | 'running'
15
- | 'reloading'
16
- | 'stopping'
17
- | 'stopped'
18
- | 'failed'
19
-
20
- /**
21
- * Events emitted by the ServerLifecycle.
22
- */
23
- export interface LifecycleEvents {
24
- 'state-change': [from: LifecycleState, to: LifecycleState]
25
- error: [error: Error, handled: boolean]
26
- }
27
-
28
- /**
29
- * Factory function type for creating ApplicationServer instances.
30
- */
31
- export type ServerFactory = () => ApplicationServer | Promise<ApplicationServer>
32
-
33
- /**
34
- * ServerLifecycle manages the application server lifecycle with a clean state machine.
35
- *
36
- * States:
37
- * - idle: Initial state, server not started
38
- * - starting: Server is being started
39
- * - running: Server is running normally
40
- * - reloading: Server is being reloaded (stop + start)
41
- * - stopping: Server is being stopped
42
- * - stopped: Server has been stopped (terminal state)
43
- * - failed: Server failed to start (can recover via reload in dev mode)
44
- *
45
- * Transitions:
46
- * - idle -> starting (via start())
47
- * - starting -> running (success) | failed (error)
48
- * - running -> reloading (via reload()) | stopping (via stop())
49
- * - reloading -> running (success) | failed (error)
50
- * - failed -> reloading (via reload()) | stopping (via stop())
51
- * - stopping -> stopped
52
- */
53
- export class ServerLifecycle extends EventEmitter<LifecycleEvents> {
54
- private state: LifecycleState = 'idle'
55
- private server: ApplicationServer | null = null
56
- private logger: Logger
57
-
58
- constructor(
59
- private readonly env: RuntimeEnvironment,
60
- private readonly createServer: ServerFactory,
61
- logger: Logger,
62
- ) {
63
- super()
64
- this.logger = logger.child({ component: 'ServerLifecycle' })
65
- }
66
-
67
- /** Current lifecycle state */
68
- get currentState(): LifecycleState {
69
- return this.state
70
- }
71
-
72
- /** Whether the server is currently running */
73
- get isRunning(): boolean {
74
- return this.state === 'running'
75
- }
76
-
77
- /** Whether the server can be started */
78
- get canStart(): boolean {
79
- return this.state === 'idle' || this.state === 'failed'
80
- }
81
-
82
- /** Whether the server can be stopped */
83
- get canStop(): boolean {
84
- return (
85
- this.state === 'running' ||
86
- this.state === 'failed' ||
87
- this.state === 'reloading'
88
- )
89
- }
90
-
91
- /** Whether the server can be reloaded */
92
- get canReload(): boolean {
93
- return this.state === 'running' || this.state === 'failed'
94
- }
95
-
96
- /** Get the current server instance (if running) */
97
- getServer(): ApplicationServer | null {
98
- return this.server
99
- }
100
-
101
- /**
102
- * Start the server.
103
- * Can only be called from 'idle' or 'failed' states.
104
- */
105
- async start(): Promise<void> {
106
- this.assertState('idle', 'failed')
107
- this.transition('starting')
108
-
109
- try {
110
- this.server = await this.createServer()
111
- await this.server.start()
112
- this.transition('running')
113
- } catch (error) {
114
- this.transition('failed')
115
- await this.handleStartupError(error as Error)
116
- }
117
- }
118
-
119
- /**
120
- * Stop the server.
121
- * Can be called from 'running', 'failed', or 'reloading' states.
122
- * No-op if already stopped or idle.
123
- */
124
- async stop(): Promise<void> {
125
- if (this.state === 'stopped' || this.state === 'idle') return
126
- this.assertState('running', 'failed', 'reloading')
127
-
128
- this.transition('stopping')
129
- try {
130
- await this.server?.stop()
131
- } catch (error) {
132
- this.logger.error(
133
- new Error('Error while stopping server (continuing shutdown)', {
134
- cause: error,
135
- }),
136
- )
137
- } finally {
138
- this.server = null
139
- this.transition('stopped')
140
- }
141
- }
142
-
143
- /**
144
- * Reload the server (stop current instance and start a new one).
145
- * Can be called from 'running' or 'failed' states.
146
- */
147
- async reload(): Promise<void> {
148
- this.assertState('running', 'failed')
149
- this.transition('reloading')
150
-
151
- try {
152
- // Stop current server if exists
153
- if (this.server) {
154
- await this.server.stop()
155
- this.server = null
156
- }
157
-
158
- // Create and start new server
159
- this.server = await this.createServer()
160
- await this.server.start()
161
- this.transition('running')
162
- } catch (error) {
163
- this.server = null
164
- this.transition('failed')
165
- await this.handleStartupError(error as Error)
166
- }
167
- }
168
-
169
- /**
170
- * Handle startup/reload error according to the error policy.
171
- */
172
- private async handleStartupError(error: Error): Promise<void> {
173
- const action = this.env.errorPolicy.onStartupError(error)
174
- const handled = action.type !== 'exit'
175
-
176
- this.emit('error', error, handled)
177
-
178
- switch (action.type) {
179
- case 'exit':
180
- this.logger.fatal(
181
- new Error('Startup failed, exiting', { cause: error }),
182
- )
183
- process.exit(action.code)
184
- break
185
- case 'wait':
186
- // Dev mode: stay alive, HMR will call reload()
187
- this.logger.error(
188
- new Error('Startup failed, waiting for fix via HMR', {
189
- cause: error,
190
- }),
191
- )
192
- break
193
- case 'restart':
194
- this.logger.warn(
195
- { error, delay: action.delay },
196
- 'Startup failed, scheduling restart',
197
- )
198
- setTimeout(() => {
199
- // Only restart if still in failed state
200
- if (this.state === 'failed') {
201
- this.start().catch((e) => {
202
- this.logger.error(new Error('Restart failed', { cause: e }))
203
- })
204
- }
205
- }, action.delay)
206
- break
207
- case 'ignore':
208
- this.logger.warn({ error }, 'Startup failed, ignoring')
209
- break
210
- }
211
- }
212
-
213
- /**
214
- * Transition to a new state, emitting the state-change event.
215
- */
216
- private transition(to: LifecycleState): void {
217
- const from = this.state
218
- this.state = to
219
- this.logger.debug({ from, to }, 'State transition')
220
- this.emit('state-change', from, to)
221
- }
222
-
223
- /**
224
- * Assert that the current state is one of the allowed states.
225
- * Throws if not.
226
- */
227
- private assertState(...allowed: LifecycleState[]): void {
228
- if (!allowed.includes(this.state)) {
229
- throw new Error(
230
- `Invalid state: ${this.state}, expected one of: ${allowed.join(', ')}`,
231
- )
232
- }
233
- }
234
- }