nmtjs 0.15.0-beta.2 → 0.15.0-beta.20

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 (254) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +3 -2
  3. package/dist/cli.js.map +1 -0
  4. package/dist/config.d.ts +51 -0
  5. package/dist/config.js +1 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/entrypoints/cli.d.ts +1 -0
  8. package/dist/entrypoints/cli.js +1 -0
  9. package/dist/entrypoints/cli.js.map +1 -0
  10. package/dist/entrypoints/main.d.ts +5 -0
  11. package/dist/entrypoints/main.js +83 -15
  12. package/dist/entrypoints/main.js.map +1 -0
  13. package/dist/entrypoints/thread.d.ts +14 -0
  14. package/dist/entrypoints/thread.js +130 -24
  15. package/dist/entrypoints/thread.js.map +1 -0
  16. package/dist/entrypoints/worker.d.ts +3 -0
  17. package/dist/entrypoints/worker.js +4 -3
  18. package/dist/entrypoints/worker.js.map +1 -0
  19. package/dist/index.d.ts +69 -0
  20. package/dist/{_exports/index.js → index.js} +9 -5
  21. package/dist/index.js.map +1 -0
  22. package/dist/resolver.d.ts +2 -0
  23. package/dist/resolver.js +1 -0
  24. package/dist/resolver.js.map +1 -0
  25. package/dist/runtime/application/api/api.d.ts +49 -0
  26. package/dist/runtime/application/api/api.js +193 -0
  27. package/dist/runtime/application/api/api.js.map +1 -0
  28. package/dist/runtime/application/api/constants.d.ts +14 -0
  29. package/dist/runtime/application/api/constants.js +8 -0
  30. package/dist/runtime/application/api/constants.js.map +1 -0
  31. package/dist/runtime/application/api/filters.d.ts +14 -0
  32. package/dist/runtime/application/api/filters.js +11 -0
  33. package/dist/runtime/application/api/filters.js.map +1 -0
  34. package/dist/runtime/application/api/guards.d.ts +13 -0
  35. package/dist/runtime/application/api/guards.js +8 -0
  36. package/dist/runtime/application/api/guards.js.map +1 -0
  37. package/dist/runtime/application/api/index.d.ts +8 -0
  38. package/dist/runtime/application/api/index.js +9 -0
  39. package/dist/runtime/application/api/index.js.map +1 -0
  40. package/dist/runtime/application/api/middlewares.d.ts +14 -0
  41. package/dist/runtime/application/api/middlewares.js +12 -0
  42. package/dist/runtime/application/api/middlewares.js.map +1 -0
  43. package/dist/runtime/application/api/procedure.d.ts +67 -0
  44. package/dist/runtime/application/api/procedure.js +50 -0
  45. package/dist/runtime/application/api/procedure.js.map +1 -0
  46. package/dist/runtime/application/api/router.d.ts +71 -0
  47. package/dist/runtime/application/api/router.js +51 -0
  48. package/dist/runtime/application/api/router.js.map +1 -0
  49. package/dist/runtime/application/api/types.d.ts +32 -0
  50. package/dist/runtime/application/api/types.js +2 -0
  51. package/dist/runtime/application/api/types.js.map +1 -0
  52. package/dist/runtime/application/config.d.ts +26 -0
  53. package/dist/runtime/application/config.js +21 -0
  54. package/dist/runtime/application/config.js.map +1 -0
  55. package/dist/runtime/application/constants.d.ts +2 -0
  56. package/dist/runtime/application/constants.js +2 -0
  57. package/dist/runtime/application/constants.js.map +1 -0
  58. package/dist/runtime/application/hook.d.ts +19 -0
  59. package/dist/runtime/application/hook.js +11 -0
  60. package/dist/runtime/application/hook.js.map +1 -0
  61. package/dist/runtime/application/hooks.d.ts +3 -0
  62. package/dist/runtime/application/hooks.js +4 -0
  63. package/dist/runtime/application/hooks.js.map +1 -0
  64. package/dist/runtime/application/index.d.ts +5 -0
  65. package/dist/runtime/application/index.js +6 -0
  66. package/dist/runtime/application/index.js.map +1 -0
  67. package/dist/runtime/constants.d.ts +8 -0
  68. package/dist/runtime/constants.js +5 -0
  69. package/dist/runtime/constants.js.map +1 -0
  70. package/dist/runtime/core/hooks.d.ts +4 -0
  71. package/dist/runtime/core/hooks.js +4 -0
  72. package/dist/runtime/core/hooks.js.map +1 -0
  73. package/dist/runtime/core/plugin.d.ts +8 -0
  74. package/dist/runtime/core/plugin.js +4 -0
  75. package/dist/runtime/core/plugin.js.map +1 -0
  76. package/dist/runtime/core/runtime.d.ts +27 -0
  77. package/dist/runtime/core/runtime.js +81 -0
  78. package/dist/runtime/core/runtime.js.map +1 -0
  79. package/dist/runtime/enums.d.ts +21 -0
  80. package/dist/runtime/enums.js +26 -0
  81. package/dist/runtime/enums.js.map +1 -0
  82. package/dist/runtime/index.d.ts +21 -0
  83. package/dist/runtime/index.js +22 -0
  84. package/dist/runtime/index.js.map +1 -0
  85. package/dist/runtime/injectables.d.ts +23 -0
  86. package/dist/runtime/injectables.js +20 -0
  87. package/dist/runtime/injectables.js.map +1 -0
  88. package/dist/runtime/jobs/job.d.ts +132 -0
  89. package/dist/runtime/jobs/job.js +68 -0
  90. package/dist/runtime/jobs/job.js.map +1 -0
  91. package/dist/runtime/jobs/manager.d.ts +103 -0
  92. package/dist/runtime/jobs/manager.js +200 -0
  93. package/dist/runtime/jobs/manager.js.map +1 -0
  94. package/dist/runtime/jobs/router.d.ts +251 -0
  95. package/dist/runtime/jobs/router.js +396 -0
  96. package/dist/runtime/jobs/router.js.map +1 -0
  97. package/dist/runtime/jobs/runner.d.ts +64 -0
  98. package/dist/runtime/jobs/runner.js +256 -0
  99. package/dist/runtime/jobs/runner.js.map +1 -0
  100. package/dist/runtime/jobs/step.d.ts +22 -0
  101. package/dist/runtime/jobs/step.js +18 -0
  102. package/dist/runtime/jobs/step.js.map +1 -0
  103. package/dist/runtime/jobs/ui.d.ts +3 -0
  104. package/dist/runtime/jobs/ui.js +17 -0
  105. package/dist/runtime/jobs/ui.js.map +1 -0
  106. package/dist/runtime/pubsub/manager.d.ts +48 -0
  107. package/dist/runtime/pubsub/manager.js +119 -0
  108. package/dist/runtime/pubsub/manager.js.map +1 -0
  109. package/dist/runtime/pubsub/redis.d.ts +16 -0
  110. package/dist/runtime/pubsub/redis.js +98 -0
  111. package/dist/runtime/pubsub/redis.js.map +1 -0
  112. package/dist/runtime/scheduler/index.d.ts +22 -0
  113. package/dist/runtime/scheduler/index.js +20 -0
  114. package/dist/runtime/scheduler/index.js.map +1 -0
  115. package/dist/runtime/server/applications.d.ts +52 -0
  116. package/dist/runtime/server/applications.js +133 -0
  117. package/dist/runtime/server/applications.js.map +1 -0
  118. package/dist/runtime/server/config.d.ts +121 -0
  119. package/dist/runtime/server/config.js +33 -0
  120. package/dist/runtime/server/config.js.map +1 -0
  121. package/dist/runtime/server/jobs.d.ts +41 -0
  122. package/dist/runtime/server/jobs.js +181 -0
  123. package/dist/runtime/server/jobs.js.map +1 -0
  124. package/dist/runtime/server/pool.d.ts +54 -0
  125. package/dist/runtime/server/pool.js +194 -0
  126. package/dist/runtime/server/pool.js.map +1 -0
  127. package/dist/runtime/server/proxy.d.ts +21 -0
  128. package/dist/runtime/server/proxy.js +79 -0
  129. package/dist/runtime/server/proxy.js.map +1 -0
  130. package/dist/runtime/server/server.d.ts +53 -0
  131. package/dist/runtime/server/server.js +90 -0
  132. package/dist/runtime/server/server.js.map +1 -0
  133. package/dist/runtime/store/index.d.ts +3 -0
  134. package/dist/runtime/store/index.js +23 -0
  135. package/dist/runtime/store/index.js.map +1 -0
  136. package/dist/runtime/types.d.ts +103 -0
  137. package/dist/runtime/types.js +2 -0
  138. package/dist/runtime/types.js.map +1 -0
  139. package/dist/runtime/workers/application.d.ts +47 -0
  140. package/dist/runtime/workers/application.js +162 -0
  141. package/dist/runtime/workers/application.js.map +1 -0
  142. package/dist/runtime/workers/base.d.ts +16 -0
  143. package/dist/runtime/workers/base.js +46 -0
  144. package/dist/runtime/workers/base.js.map +1 -0
  145. package/dist/runtime/workers/cli.d.ts +1 -0
  146. package/dist/runtime/workers/cli.js +2 -0
  147. package/dist/runtime/workers/cli.js.map +1 -0
  148. package/dist/runtime/workers/job.d.ts +20 -0
  149. package/dist/runtime/workers/job.js +172 -0
  150. package/dist/runtime/workers/job.js.map +1 -0
  151. package/dist/typings.d.ts +5 -0
  152. package/dist/typings.js +4 -3
  153. package/dist/typings.js.map +1 -0
  154. package/dist/vite/builder.d.ts +5 -0
  155. package/dist/vite/builder.js +5 -1
  156. package/dist/vite/builder.js.map +1 -0
  157. package/dist/vite/config.d.ts +28 -0
  158. package/dist/vite/config.js +1 -0
  159. package/dist/vite/config.js.map +1 -0
  160. package/dist/vite/plugins.d.ts +2 -0
  161. package/dist/vite/plugins.js +1 -0
  162. package/dist/vite/plugins.js.map +1 -0
  163. package/dist/vite/runners/worker.d.ts +4 -0
  164. package/dist/vite/runners/worker.js +1 -0
  165. package/dist/vite/runners/worker.js.map +1 -0
  166. package/dist/vite/server.d.ts +3 -0
  167. package/dist/vite/server.js +6 -1
  168. package/dist/vite/server.js.map +1 -0
  169. package/dist/vite/servers/main.d.ts +8 -0
  170. package/dist/vite/servers/main.js +1 -0
  171. package/dist/vite/servers/main.js.map +1 -0
  172. package/dist/vite/servers/worker.d.ts +11 -0
  173. package/dist/vite/servers/worker.js +28 -0
  174. package/dist/vite/servers/worker.js.map +1 -0
  175. package/package.json +31 -18
  176. package/src/cli.ts +144 -0
  177. package/src/config.ts +64 -0
  178. package/src/entrypoints/cli.ts +13 -0
  179. package/src/entrypoints/main.ts +200 -0
  180. package/src/entrypoints/thread.ts +184 -0
  181. package/src/entrypoints/worker.ts +48 -0
  182. package/src/index.ts +82 -0
  183. package/src/resolver.ts +16 -0
  184. package/src/runtime/application/api/api.ts +265 -0
  185. package/src/runtime/application/api/constants.ts +22 -0
  186. package/src/runtime/application/api/filters.ts +39 -0
  187. package/src/runtime/application/api/guards.ts +29 -0
  188. package/src/runtime/application/api/index.ts +8 -0
  189. package/src/runtime/application/api/middlewares.ts +37 -0
  190. package/src/runtime/application/api/procedure.ts +229 -0
  191. package/src/runtime/application/api/router.ts +193 -0
  192. package/src/runtime/application/api/types.ts +124 -0
  193. package/src/runtime/application/config.ts +69 -0
  194. package/src/runtime/application/constants.ts +4 -0
  195. package/src/runtime/application/hook.ts +51 -0
  196. package/src/runtime/application/hooks.ts +3 -0
  197. package/src/runtime/application/index.ts +5 -0
  198. package/src/runtime/constants.ts +13 -0
  199. package/src/runtime/core/hooks.ts +5 -0
  200. package/src/runtime/core/plugin.ts +13 -0
  201. package/src/runtime/core/runtime.ts +109 -0
  202. package/src/runtime/enums.ts +24 -0
  203. package/src/runtime/index.ts +21 -0
  204. package/src/runtime/injectables.ts +61 -0
  205. package/src/runtime/jobs/job.ts +370 -0
  206. package/src/runtime/jobs/manager.ts +332 -0
  207. package/src/runtime/jobs/router.ts +835 -0
  208. package/src/runtime/jobs/runner.ts +320 -0
  209. package/src/runtime/jobs/step.ts +65 -0
  210. package/src/runtime/jobs/ui.ts +21 -0
  211. package/src/runtime/pubsub/manager.ts +211 -0
  212. package/src/runtime/pubsub/redis.ts +108 -0
  213. package/src/runtime/scheduler/index.ts +39 -0
  214. package/src/runtime/server/applications.ts +210 -0
  215. package/src/runtime/server/config.ts +158 -0
  216. package/src/runtime/server/jobs.ts +250 -0
  217. package/src/runtime/server/pool.ts +260 -0
  218. package/src/runtime/server/proxy.ts +118 -0
  219. package/src/runtime/server/server.ts +155 -0
  220. package/src/runtime/store/index.ts +30 -0
  221. package/src/runtime/types.ts +93 -0
  222. package/src/runtime/workers/application.ts +209 -0
  223. package/src/runtime/workers/base.ts +68 -0
  224. package/src/runtime/workers/cli.ts +0 -0
  225. package/src/runtime/workers/job.ts +153 -0
  226. package/src/typings.ts +30 -0
  227. package/src/vite/builder.ts +122 -0
  228. package/src/vite/config.ts +45 -0
  229. package/src/vite/plugins.ts +26 -0
  230. package/src/vite/runners/worker.ts +57 -0
  231. package/src/vite/server.ts +39 -0
  232. package/src/vite/servers/main.ts +34 -0
  233. package/src/vite/servers/worker.ts +143 -0
  234. package/dist/_exports/application.js +0 -1
  235. package/dist/_exports/common.js +0 -1
  236. package/dist/_exports/contract.js +0 -2
  237. package/dist/_exports/core.js +0 -1
  238. package/dist/_exports/gateway.js +0 -1
  239. package/dist/_exports/http-transport/bun.js +0 -1
  240. package/dist/_exports/http-transport/deno.js +0 -1
  241. package/dist/_exports/http-transport/node.js +0 -1
  242. package/dist/_exports/http-transport.js +0 -1
  243. package/dist/_exports/json-format.js +0 -1
  244. package/dist/_exports/protocol/client.js +0 -1
  245. package/dist/_exports/protocol/server.js +0 -1
  246. package/dist/_exports/protocol.js +0 -1
  247. package/dist/_exports/runtime/types.js +0 -1
  248. package/dist/_exports/runtime.js +0 -1
  249. package/dist/_exports/type.js +0 -2
  250. package/dist/_exports/ws-transport/bun.js +0 -1
  251. package/dist/_exports/ws-transport/deno.js +0 -1
  252. package/dist/_exports/ws-transport/node.js +0 -1
  253. package/dist/_exports/ws-transport.js +0 -1
  254. package/dist/command.js +0 -30
@@ -0,0 +1,250 @@
1
+ import type { Logger } from '@nmtjs/core'
2
+ import type { RedisClient } from 'bullmq'
3
+ import { Queue, UnrecoverableError, Worker } from 'bullmq'
4
+
5
+ import type { AnyJob } from '../jobs/job.ts'
6
+ import type { JobsUI } from '../jobs/ui.ts'
7
+ import type { JobTaskResult, Store, WorkerJobTask } from '../types.ts'
8
+ import type { ServerConfig } from './config.ts'
9
+ import type { ApplicationServerWorkerConfig } from './server.ts'
10
+ import { JobWorkerPool } from '../enums.ts'
11
+ import { getJobQueueName } from '../jobs/manager.ts'
12
+ import { createJobsUI } from '../jobs/ui.ts'
13
+ import { Pool } from './pool.ts'
14
+
15
+ export class JobRunnersPool extends Pool {
16
+ protected runIndex = 0
17
+
18
+ async run(task: WorkerJobTask): Promise<JobTaskResult> {
19
+ if (this.threads.length === 0) {
20
+ throw new Error('No job runner threads available')
21
+ }
22
+
23
+ if (this.runIndex >= this.threads.length) {
24
+ this.runIndex = 0
25
+ }
26
+
27
+ const thread = this.threads[this.runIndex]!
28
+ this.runIndex++
29
+ return await thread.run(task)
30
+ }
31
+ }
32
+
33
+ export class ApplicationServerJobs {
34
+ /**
35
+ * BullMQ workers - one per job (dedicated queues)
36
+ */
37
+ queueWorkers = new Set<Worker>()
38
+ ui?: JobsUI
39
+ protected uiQueues: Queue[] = []
40
+
41
+ jobs: Map<string, AnyJob>
42
+
43
+ /**
44
+ * Shared resource pools by pool type (Io, Compute).
45
+ * All jobs of a given pool type share the same pool for resource management.
46
+ */
47
+ protected pools = new Map<JobWorkerPool, JobRunnersPool>()
48
+
49
+ constructor(
50
+ readonly params: {
51
+ logger: Logger
52
+ serverConfig: ServerConfig
53
+ workerConfig: ApplicationServerWorkerConfig
54
+ store: Store
55
+ },
56
+ ) {
57
+ this.jobs = params.serverConfig.jobs
58
+ ? params.serverConfig.jobs.jobs
59
+ : new Map()
60
+ }
61
+
62
+ async start() {
63
+ const { logger, serverConfig, workerConfig, store } = this.params
64
+ const jobsConfig = serverConfig.jobs
65
+
66
+ if (!jobsConfig) {
67
+ logger.debug('Jobs are not configured, skipping')
68
+ return
69
+ }
70
+
71
+ if (jobsConfig.ui) {
72
+ const hostname = jobsConfig.ui.hostname ?? '127.0.0.1'
73
+ const port = jobsConfig.ui.port ?? 3000
74
+
75
+ this.uiQueues = [...this.jobs.values()].map(
76
+ (job) =>
77
+ new Queue(getJobQueueName(job), {
78
+ connection: store as unknown as RedisClient,
79
+ }),
80
+ )
81
+
82
+ this.ui = createJobsUI(this.uiQueues)
83
+
84
+ await new Promise<void>((resolve, reject) => {
85
+ if (!this.ui) return reject(new Error('Jobs UI server is missing'))
86
+ this.ui.once('error', reject)
87
+ this.ui.listen(port, hostname, resolve)
88
+ })
89
+
90
+ const address = this.ui.address()
91
+ const resolved =
92
+ address && typeof address !== 'string'
93
+ ? { hostname: address.address, port: address.port }
94
+ : { hostname, port }
95
+
96
+ logger.info({ ...resolved }, 'Jobs UI started')
97
+ }
98
+
99
+ // Step 1: Initialize shared resource pools (Io, Compute)
100
+ const poolTypes = Object.values(JobWorkerPool)
101
+
102
+ for (const poolType of poolTypes) {
103
+ const poolConfig = jobsConfig.pools[poolType]
104
+ if (!poolConfig) continue
105
+
106
+ const pool = new JobRunnersPool({
107
+ path: workerConfig.path,
108
+ worker: workerConfig.worker,
109
+ workerData: { ...workerConfig.workerData },
110
+ })
111
+
112
+ for (let i = 0; i < poolConfig.threads; i++) {
113
+ pool.add({
114
+ index: i,
115
+ name: `job-pool-${poolType}`,
116
+ workerData: { runtime: { type: 'jobs', jobWorkerPool: poolType } },
117
+ })
118
+ }
119
+
120
+ await pool.start()
121
+ this.pools.set(poolType, pool)
122
+
123
+ logger.info(
124
+ {
125
+ pool: poolType,
126
+ threads: poolConfig.threads,
127
+ jobsPerThread: poolConfig.jobs,
128
+ },
129
+ 'Job runner pool started',
130
+ )
131
+ }
132
+
133
+ // Step 2: Create a dedicated BullMQ Worker for each job
134
+ // Calculate how many jobs use each pool for fair concurrency distribution
135
+ const jobsPerPool = new Map<JobWorkerPool, number>()
136
+ for (const job of this.jobs.values()) {
137
+ const count = jobsPerPool.get(job.options.pool) ?? 0
138
+ jobsPerPool.set(job.options.pool, count + 1)
139
+ }
140
+
141
+ for (const job of this.jobs.values()) {
142
+ const queueName = getJobQueueName(job)
143
+ const poolType = job.options.pool
144
+ const pool = this.pools.get(poolType)
145
+
146
+ if (!pool) {
147
+ logger.warn(
148
+ { job: job.name, pool: poolType },
149
+ 'No pool configured for job, skipping worker creation',
150
+ )
151
+ continue
152
+ }
153
+
154
+ const poolConfig = jobsConfig.pools[poolType]
155
+ const poolCapacity = poolConfig.threads * poolConfig.jobs
156
+ const jobCountInPool = jobsPerPool.get(poolType) ?? 1
157
+ // Use job-specific concurrency if provided, otherwise distribute pool capacity evenly
158
+ const defaultConcurrency = Math.max(
159
+ 1,
160
+ Math.floor(poolCapacity / jobCountInPool),
161
+ )
162
+ const concurrency = job.options.concurrency ?? defaultConcurrency
163
+
164
+ const queueWorker = new Worker(
165
+ queueName,
166
+ async (bullJob) => {
167
+ const task: WorkerJobTask = {
168
+ jobId: String(bullJob.id ?? ''),
169
+ jobName: bullJob.name,
170
+ data: bullJob.data,
171
+ }
172
+
173
+ const result = await pool.run(task)
174
+ switch (result.type) {
175
+ case 'success':
176
+ return result.result
177
+ case 'unrecoverable_error':
178
+ throw new UnrecoverableError(
179
+ typeof result.error === 'string'
180
+ ? result.error
181
+ : 'Unrecoverable error',
182
+ )
183
+ case 'job_not_found':
184
+ case 'queue_job_not_found':
185
+ throw new UnrecoverableError(result.type)
186
+ case 'error':
187
+ console.error(result.error)
188
+ throw result.error
189
+ default:
190
+ throw new UnrecoverableError('Unknown job task result')
191
+ }
192
+ },
193
+ { connection: store as unknown as RedisClient, concurrency },
194
+ )
195
+
196
+ this.queueWorkers.add(queueWorker)
197
+ logger.info(
198
+ { job: job.name, queue: queueName, pool: poolType, concurrency },
199
+ 'Job queue worker started',
200
+ )
201
+ }
202
+ }
203
+
204
+ async stop() {
205
+ const { logger } = this.params
206
+
207
+ if (this.ui) {
208
+ await new Promise<void>((resolve) => {
209
+ this.ui?.close(() => resolve())
210
+ }).catch((error) => {
211
+ logger.warn({ error }, 'Failed to stop Jobs UI server')
212
+ })
213
+ }
214
+
215
+ await Promise.all(
216
+ this.uiQueues.map(async (queue) => {
217
+ try {
218
+ await queue.close()
219
+ } catch (error) {
220
+ logger.warn({ error }, 'Failed to close Jobs UI queue')
221
+ }
222
+ }),
223
+ )
224
+ this.uiQueues = []
225
+ this.ui = undefined
226
+
227
+ // Stop accepting new jobs first.
228
+ await Promise.all(
229
+ [...this.queueWorkers].map(async (worker) => {
230
+ try {
231
+ await worker.close()
232
+ } catch (error) {
233
+ logger.warn({ error }, 'Failed to close BullMQ worker')
234
+ }
235
+ }),
236
+ )
237
+ this.queueWorkers.clear()
238
+
239
+ await Promise.all(
240
+ Array.from(this.pools.values()).map(async (pool) => {
241
+ try {
242
+ await pool.stop()
243
+ } catch (error) {
244
+ logger.warn({ error }, 'Failed to stop job pool')
245
+ }
246
+ }),
247
+ )
248
+ this.pools.clear()
249
+ }
250
+ }
@@ -0,0 +1,260 @@
1
+ import type { MessagePort, WorkerOptions } from 'node:worker_threads'
2
+ import { randomUUID } from 'node:crypto'
3
+ import EventEmitter, { once } from 'node:events'
4
+ import { MessageChannel, Worker } from 'node:worker_threads'
5
+
6
+ import type {
7
+ JobTaskResult,
8
+ ServerPortMessageTypes,
9
+ ThreadErrorMessage,
10
+ ThreadPortMessage,
11
+ ThreadPortMessageTypes,
12
+ WorkerJobTask,
13
+ WorkerThreadError,
14
+ } from '../types.ts'
15
+
16
+ const omitExecArgv = ['--expose-gc']
17
+
18
+ export type ThreadState =
19
+ | 'starting'
20
+ | 'error'
21
+ | 'terminating'
22
+ | 'pending'
23
+ | 'ready'
24
+
25
+ export class Thread extends EventEmitter<
26
+ {
27
+ error: [error: WorkerThreadError]
28
+ ready: [ThreadPortMessageTypes['ready']]
29
+ task: [ThreadPortMessageTypes['task']]
30
+ terminate: []
31
+ } & {
32
+ [K in `task-${ThreadPortMessageTypes['task']['id']}`]: [
33
+ ThreadPortMessageTypes['task']['task'],
34
+ ]
35
+ }
36
+ > {
37
+ worker: Worker
38
+ state: ThreadState = 'pending'
39
+ protected readyMessage?: ThreadPortMessageTypes['ready']
40
+ protected startPromise?: Promise<void>
41
+
42
+ constructor(
43
+ readonly port: MessagePort,
44
+ workerPath: string,
45
+ workerOptions: WorkerOptions,
46
+ ) {
47
+ super()
48
+ this.worker = new Worker(workerPath, {
49
+ ...workerOptions,
50
+ execArgv: process.execArgv.filter((f) => !omitExecArgv.includes(f)),
51
+ })
52
+
53
+ this.port.on('message', (msg: ThreadPortMessage) => {
54
+ const { type, data } = msg
55
+ switch (type) {
56
+ case 'ready': {
57
+ this.state = 'ready'
58
+ this.readyMessage = data
59
+ this.emit('ready', data)
60
+ break
61
+ }
62
+ case 'error': {
63
+ const error = createWorkerThreadError(data as ThreadErrorMessage)
64
+ this.state = 'error'
65
+ this.emit('error', error)
66
+ break
67
+ }
68
+ case 'task': {
69
+ this.emit('task', data as ThreadPortMessageTypes['task'])
70
+ const { id, task } = data as ThreadPortMessageTypes['task']
71
+ this.emit(`task-${id}`, task)
72
+ break
73
+ }
74
+ }
75
+ })
76
+
77
+ this.worker.once('exit', (code) => {
78
+ if (this.state === 'terminating') return
79
+ const error = createWorkerThreadError(
80
+ {
81
+ message: `Worker thread ${this.worker.threadId} exited unexpectedly with code ${code}`,
82
+ name: 'WorkerThreadExitError',
83
+ origin: 'runtime',
84
+ fatal: code !== 0,
85
+ },
86
+ false,
87
+ )
88
+ this.state = 'error'
89
+ this.emit('error', error)
90
+ })
91
+ }
92
+
93
+ async start() {
94
+ if (this.state === 'ready') return
95
+ if (this.startPromise) return this.startPromise
96
+ switch (this.state) {
97
+ case 'error':
98
+ case 'terminating':
99
+ case 'starting':
100
+ throw new Error('Cannot start worker thread in current state')
101
+ case 'pending':
102
+ break
103
+ }
104
+ this.state = 'starting'
105
+ this.startPromise = new Promise<void>((resolve, reject) => {
106
+ let settled = false
107
+ let timer: NodeJS.Timeout
108
+ const cleanup = () => {
109
+ if (settled) return
110
+ settled = true
111
+ if (timer) clearTimeout(timer)
112
+ this.off('ready', handleReady)
113
+ this.off('error', handleError)
114
+ this.startPromise = undefined
115
+ }
116
+ const handleReady = () => {
117
+ cleanup()
118
+ resolve()
119
+ }
120
+ const handleError = (error: WorkerThreadError) => {
121
+ cleanup()
122
+ reject(error)
123
+ }
124
+
125
+ this.once('ready', handleReady)
126
+ this.once('error', handleError)
127
+
128
+ timer = setTimeout(() => {
129
+ const error = createWorkerThreadError(
130
+ {
131
+ message: 'Worker thread did not become ready in time',
132
+ name: 'WorkerStartupTimeoutError',
133
+ origin: 'start',
134
+ fatal: true,
135
+ },
136
+ false,
137
+ )
138
+ cleanup()
139
+ this.state = 'error'
140
+ this.emit('error', error)
141
+ reject(error)
142
+ }, 15000)
143
+ })
144
+ await this.startPromise
145
+ this.startPromise = undefined
146
+ }
147
+
148
+ async stop() {
149
+ switch (this.state) {
150
+ case 'error':
151
+ case 'terminating':
152
+ case 'starting':
153
+ throw new Error('Cannot stop worker thread in this state')
154
+ case 'ready':
155
+ case 'pending': {
156
+ this.state = 'terminating'
157
+ // TODO: make timeout configurable
158
+ const signal = AbortSignal.timeout(10000)
159
+ const exit = once(this.worker, 'exit', { signal })
160
+ this.send('stop')
161
+ this.port.close()
162
+ try {
163
+ await exit
164
+ } catch (err) {
165
+ console.dir(err)
166
+ console.warn(
167
+ `Worker thread ${this.worker.threadId} did not terminate in time, terminating forcefully...`,
168
+ )
169
+ await this.worker.terminate()
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ async run(task: WorkerJobTask): Promise<JobTaskResult> {
176
+ const id = randomUUID()
177
+ this.send('task', { id, task })
178
+ const [result] = await once(this, `task-${id}`)
179
+ return result
180
+ }
181
+
182
+ async send<T extends keyof ServerPortMessageTypes>(
183
+ type: T,
184
+ ...[data]: ServerPortMessageTypes[T] extends undefined
185
+ ? []
186
+ : [data: ServerPortMessageTypes[T]]
187
+ ) {
188
+ this.port.postMessage({ type, data })
189
+ }
190
+ }
191
+
192
+ function createWorkerThreadError(
193
+ message: ThreadErrorMessage,
194
+ includeStack = true,
195
+ ): WorkerThreadError {
196
+ const error = new Error(message.message) as WorkerThreadError
197
+ if (message.name) error.name = message.name
198
+ if (includeStack && message.stack) {
199
+ error.stack = message.stack
200
+ }
201
+ error.origin = message.origin
202
+ error.fatal = message.fatal
203
+ return error
204
+ }
205
+
206
+ export class Pool extends EventEmitter<{
207
+ workerReady: [worker: Worker]
208
+ workerTaskResult: [worker: Worker]
209
+ }> {
210
+ protected readonly threads: Thread[] = []
211
+
212
+ constructor(
213
+ readonly options: {
214
+ path: string
215
+ workerData?: any
216
+ worker?: (worker: Worker) => any
217
+ },
218
+ ) {
219
+ super()
220
+ }
221
+
222
+ add(options: { index: number; name: string; workerData?: any }) {
223
+ return this.createThread(options)
224
+ }
225
+
226
+ async start() {
227
+ await Promise.all(this.threads.map((thread) => thread.start()))
228
+ }
229
+
230
+ async stop() {
231
+ await Promise.all(this.threads.map((thread) => thread.stop()))
232
+ }
233
+
234
+ protected createThread(
235
+ options: { index: number; name: string; workerData?: any },
236
+ index?: number,
237
+ ) {
238
+ const { port1, port2 } = new MessageChannel()
239
+ const thread = new Thread(port1, this.options.path, {
240
+ workerData: {
241
+ ...this.options.workerData,
242
+ ...options.workerData,
243
+ port: port2,
244
+ },
245
+ name: `${options.name}-${options.index + 1}`,
246
+ transferList: [port2],
247
+ })
248
+ if (index !== undefined) {
249
+ this.threads[index] = thread
250
+ } else {
251
+ index = this.threads.push(thread) - 1
252
+ }
253
+ this.options.worker?.(thread.worker)
254
+ thread.once('error', (error) => {
255
+ thread.worker.terminate()
256
+ this.createThread(options)
257
+ })
258
+ return thread
259
+ }
260
+ }
@@ -0,0 +1,118 @@
1
+ import type { Logger } from '@nmtjs/core'
2
+ import { Proxy as NeemataProxy } from '@nmtjs/proxy'
3
+
4
+ import type {
5
+ ApplicationProxyUpstream,
6
+ ApplicationServerApplications,
7
+ } from './applications.ts'
8
+ import type { ServerConfig } from './config.ts'
9
+
10
+ /**
11
+ * Transform ApplicationProxyUpstream to the format expected by Rust proxy.
12
+ */
13
+ function toProxyUpstream(upstream: ApplicationProxyUpstream) {
14
+ const url = new URL(upstream.url)
15
+ const isWebSocket = url.protocol === 'wss:' || url.protocol === 'ws:'
16
+ const secure = url.protocol === 'https:' || url.protocol === 'wss:'
17
+ const port = url.port ? Number.parseInt(url.port, 10) : secure ? 443 : 80
18
+
19
+ return {
20
+ type: 'port',
21
+ // WebSocket requires HTTP/1.1 for upgrade
22
+ transport: isWebSocket ? 'http' : upstream.type,
23
+ secure,
24
+ hostname: url.hostname,
25
+ port,
26
+ }
27
+ }
28
+
29
+ export class ApplicationServerProxy {
30
+ proxyServer: NeemataProxy
31
+
32
+ protected readonly onAdd: (application: string, upstream: any) => void
33
+ protected readonly onRemove: (application: string, upstream: any) => void
34
+
35
+ constructor(
36
+ readonly params: {
37
+ logger: Logger
38
+ config: ServerConfig['proxy']
39
+ applications: ApplicationServerApplications
40
+ },
41
+ ) {
42
+ const { config } = params
43
+ if (!config) {
44
+ throw new Error('Proxy config is required')
45
+ }
46
+
47
+ this.proxyServer = new NeemataProxy({
48
+ listen: `${config.hostname}:${config.port}`,
49
+ tls: config.tls
50
+ ? { keyPath: config.tls.key, certPath: config.tls.cert }
51
+ : undefined,
52
+ applications: Object.entries(config.applications)
53
+ .filter(([_, options]) => options !== undefined)
54
+ .map(([app, options]) => ({
55
+ name: app,
56
+ routing: options!.routing,
57
+ sni: options!.sni,
58
+ })),
59
+ })
60
+
61
+ this.onAdd = (application, upstream) => {
62
+ const proxyUpstream = toProxyUpstream(upstream)
63
+ this.params.logger.debug(
64
+ { application, upstream: proxyUpstream },
65
+ 'Adding upstream to proxy',
66
+ )
67
+ void this.proxyServer
68
+ .addUpstream(application, proxyUpstream)
69
+ .catch((error) => {
70
+ this.params.logger.warn(
71
+ { error, application, upstream: proxyUpstream },
72
+ 'Failed to add upstream to proxy',
73
+ )
74
+ })
75
+ }
76
+
77
+ this.onRemove = (application, upstream) => {
78
+ const proxyUpstream = toProxyUpstream(upstream)
79
+ this.params.logger.debug(
80
+ { application, upstream: proxyUpstream },
81
+ 'Removing upstream from proxy',
82
+ )
83
+
84
+ void this.proxyServer
85
+ .removeUpstream(application, proxyUpstream)
86
+ .catch((error) => {
87
+ this.params.logger.warn(
88
+ { error, application, upstream: proxyUpstream },
89
+ 'Failed to remove upstream from proxy',
90
+ )
91
+ })
92
+ }
93
+
94
+ params.applications.on('add', this.onAdd)
95
+ params.applications.on('remove', this.onRemove)
96
+ }
97
+
98
+ async start() {
99
+ const { config } = this.params
100
+ if (!config) {
101
+ throw new Error('Proxy config is required')
102
+ }
103
+ this.params.logger.info(
104
+ { hostname: config.hostname, port: config.port, threads: config.threads },
105
+ 'Starting proxy server...',
106
+ )
107
+ await this.proxyServer.start()
108
+ }
109
+
110
+ async stop() {
111
+ this.params.logger.info('Stopping proxy server...')
112
+
113
+ this.params.applications.off('add', this.onAdd)
114
+ this.params.applications.off('remove', this.onRemove)
115
+
116
+ await this.proxyServer.stop()
117
+ }
118
+ }