nmtjs 0.15.0-beta.13 → 0.15.0-beta.15

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 (46) hide show
  1. package/dist/entrypoints/main.js +79 -10
  2. package/dist/entrypoints/main.js.map +1 -1
  3. package/dist/entrypoints/thread.js +129 -27
  4. package/dist/entrypoints/thread.js.map +1 -1
  5. package/dist/runtime/application/api/api.d.ts +2 -1
  6. package/dist/runtime/application/api/api.js +7 -3
  7. package/dist/runtime/application/api/api.js.map +1 -1
  8. package/dist/runtime/application/api/constants.d.ts +2 -0
  9. package/dist/runtime/application/api/constants.js +1 -0
  10. package/dist/runtime/application/api/constants.js.map +1 -1
  11. package/dist/runtime/application/api/router.d.ts +4 -1
  12. package/dist/runtime/application/api/router.js +6 -2
  13. package/dist/runtime/application/api/router.js.map +1 -1
  14. package/dist/runtime/server/applications.d.ts +4 -1
  15. package/dist/runtime/server/applications.js +38 -15
  16. package/dist/runtime/server/applications.js.map +1 -1
  17. package/dist/runtime/server/config.d.ts +4 -0
  18. package/dist/runtime/server/config.js.map +1 -1
  19. package/dist/runtime/server/pool.d.ts +4 -2
  20. package/dist/runtime/server/pool.js +91 -20
  21. package/dist/runtime/server/pool.js.map +1 -1
  22. package/dist/runtime/server/proxy.js +7 -1
  23. package/dist/runtime/server/proxy.js.map +1 -1
  24. package/dist/runtime/server/server.d.ts +17 -1
  25. package/dist/runtime/server/server.js.map +1 -1
  26. package/dist/runtime/types.d.ts +13 -3
  27. package/dist/runtime/workers/application.d.ts +2 -2
  28. package/dist/runtime/workers/application.js +10 -1
  29. package/dist/runtime/workers/application.js.map +1 -1
  30. package/dist/vite/servers/worker.d.ts +5 -2
  31. package/dist/vite/servers/worker.js +27 -0
  32. package/dist/vite/servers/worker.js.map +1 -1
  33. package/package.json +12 -12
  34. package/src/entrypoints/main.ts +103 -15
  35. package/src/entrypoints/thread.ts +145 -34
  36. package/src/runtime/application/api/api.ts +12 -3
  37. package/src/runtime/application/api/constants.ts +5 -0
  38. package/src/runtime/application/api/router.ts +13 -3
  39. package/src/runtime/server/applications.ts +53 -14
  40. package/src/runtime/server/config.ts +4 -0
  41. package/src/runtime/server/pool.ts +100 -23
  42. package/src/runtime/server/proxy.ts +7 -1
  43. package/src/runtime/server/server.ts +23 -1
  44. package/src/runtime/types.ts +16 -1
  45. package/src/runtime/workers/application.ts +16 -1
  46. package/src/vite/servers/worker.ts +37 -1
@@ -2,10 +2,16 @@ import type { Worker } from 'node:worker_threads'
2
2
  import EventEmitter from 'node:events'
3
3
  import { fileURLToPath } from 'node:url'
4
4
 
5
- import type { ServerConfig } from 'nmtjs/runtime'
5
+ import type {
6
+ ApplicationWorkerErrorEvent,
7
+ ApplicationWorkerReadyEvent,
8
+ ServerConfig,
9
+ } from 'nmtjs/runtime'
6
10
  import type { ViteDevServer } from 'vite'
7
11
  import { ApplicationServer, isServerConfig } from 'nmtjs/runtime'
8
12
 
13
+ import type { WorkerServerEventMap as BaseWorkerServerEventMap } from '../vite/servers/worker.ts'
14
+
9
15
  declare global {
10
16
  const __VITE_CONFIG__: string
11
17
  const __APPLICATIONS_CONFIG__: string
@@ -27,15 +33,22 @@ const applicationsConfig: Record<
27
33
  { type: 'neemata' | 'custom'; specifier: string }
28
34
  > = __APPLICATIONS_CONFIG__ ? JSON.parse(__APPLICATIONS_CONFIG__) : {}
29
35
 
30
- let _viteServerEvents: EventEmitter<{ worker: [Worker] }> | undefined
36
+ type WorkerEventMap = BaseWorkerServerEventMap & {
37
+ 'worker-error': [ApplicationWorkerErrorEvent]
38
+ 'worker-ready': [ApplicationWorkerReadyEvent]
39
+ }
40
+
41
+ let _viteServerEvents: EventEmitter<WorkerEventMap> | undefined
31
42
  let _viteWorkerServer: ViteDevServer | undefined
32
43
 
33
- let server: ApplicationServer
44
+ let server: ApplicationServer | undefined
45
+ let hasActiveWorkerError = false
46
+ const isDev = _vite?.mode === 'development'
34
47
 
35
48
  if (import.meta.env.DEV && import.meta.hot) {
36
49
  import.meta.hot.accept('#server', async (module) => {
37
- await server.stop()
38
- await createServer(module?.default)
50
+ await shutdownServer()
51
+ await bootWithHandling(module?.default)
39
52
  })
40
53
  }
41
54
 
@@ -45,7 +58,9 @@ if (_vite) {
45
58
  /* @vite-ignore */
46
59
  _vite.options.configPath
47
60
  ).then((m) => m.default as import('../config.ts').NeemataConfig)
48
- _viteServerEvents = new EventEmitter<{ worker: [Worker] }>()
61
+ _viteServerEvents = new EventEmitter<WorkerEventMap>()
62
+ _viteServerEvents.on('worker-error', handleWorkerError)
63
+ _viteServerEvents.on('worker-ready', handleWorkerReady)
49
64
  _viteWorkerServer = await createWorkerServer(
50
65
  _vite.options,
51
66
  _vite.mode,
@@ -54,18 +69,41 @@ if (_vite) {
54
69
  )
55
70
  }
56
71
 
57
- async function createServer(config: ServerConfig) {
58
- if (!isServerConfig(config)) throw new InvalidServerConfigError()
59
- server = new ApplicationServer(config, applicationsConfig, {
72
+ async function bootServer(configValue: ServerConfig | undefined) {
73
+ if (!isServerConfig(configValue)) throw new InvalidServerConfigError()
74
+ const workerConfig = {
60
75
  path: fileURLToPath(import.meta.resolve(`./thread${_ext}`)),
61
76
  workerData: { vite: _vite?.mode },
62
77
  worker: _viteServerEvents
63
- ? (worker) => {
78
+ ? (worker: Worker) => {
64
79
  _viteServerEvents.emit('worker', worker)
65
80
  }
66
81
  : undefined,
67
- })
68
- await server.start()
82
+ events: _viteServerEvents,
83
+ }
84
+ const appServer = new ApplicationServer(
85
+ configValue,
86
+ applicationsConfig,
87
+ workerConfig,
88
+ )
89
+
90
+ try {
91
+ await appServer.start()
92
+ server = appServer
93
+ clearWorkerErrorOverlay()
94
+ } catch (error) {
95
+ await appServer.stop().catch(() => {})
96
+ throw error
97
+ }
98
+ }
99
+
100
+ async function bootWithHandling(configValue: ServerConfig | undefined) {
101
+ try {
102
+ await bootServer(configValue)
103
+ } catch (error) {
104
+ handleStartupError(error)
105
+ if (!isDev) throw error
106
+ }
69
107
  }
70
108
 
71
109
  let isTerminating = false
@@ -73,7 +111,7 @@ let isTerminating = false
73
111
  async function handleTermination() {
74
112
  if (isTerminating) return
75
113
  isTerminating = true
76
- await server?.stop()
114
+ await shutdownServer()
77
115
  _viteWorkerServer?.close()
78
116
  process.exit(0)
79
117
  }
@@ -82,17 +120,66 @@ function handleUnexpectedError(error: Error) {
82
120
  console.error(new Error('Unexpected Error:', { cause: error }))
83
121
  }
84
122
 
123
+ async function shutdownServer() {
124
+ if (!server) return
125
+ try {
126
+ await server.stop()
127
+ } catch (error) {
128
+ console.error(
129
+ new Error('Failed to stop application server', { cause: error as Error }),
130
+ )
131
+ } finally {
132
+ server = undefined
133
+ }
134
+ }
135
+
136
+ function handleWorkerError(event: ApplicationWorkerErrorEvent) {
137
+ hasActiveWorkerError = true
138
+ console.error(
139
+ new Error(`Worker ${event.application} thread ${event.threadId} error`, {
140
+ cause: event.error,
141
+ }),
142
+ )
143
+ }
144
+
145
+ function handleWorkerReady(_: ApplicationWorkerReadyEvent) {
146
+ clearWorkerErrorOverlay()
147
+ }
148
+
149
+ function handleStartupError(error: unknown) {
150
+ const normalized = error instanceof Error ? error : new Error(String(error))
151
+ if (_viteServerEvents) {
152
+ _viteServerEvents.emit('worker-error', {
153
+ application: 'server',
154
+ threadId: -1,
155
+ error: normalized,
156
+ } as ApplicationWorkerErrorEvent)
157
+ } else {
158
+ hasActiveWorkerError = true
159
+ console.error(
160
+ new Error('Failed to start application server', { cause: normalized }),
161
+ )
162
+ }
163
+ }
164
+
165
+ function clearWorkerErrorOverlay() {
166
+ if (!hasActiveWorkerError) return
167
+ hasActiveWorkerError = false
168
+ }
169
+
85
170
  process.once('SIGTERM', handleTermination)
86
171
  process.once('SIGINT', handleTermination)
87
172
  process.on('uncaughtException', handleUnexpectedError)
88
173
  process.on('unhandledRejection', handleUnexpectedError)
89
174
 
90
- await createServer(
175
+ await bootWithHandling(
91
176
  await import(
92
177
  // @ts-expect-error
93
178
  '#server'
94
179
  ).then((m) => m.default),
95
- )
180
+ ).catch(() => {
181
+ if (!isDev) process.exit(1)
182
+ })
96
183
 
97
184
  const { format } = Intl.NumberFormat('en', {
98
185
  notation: 'compact',
@@ -108,5 +195,6 @@ const printMem = () => {
108
195
  `Memory Usage: RSS=${format(memoryUsage.rss)}, HeapTotal=${format(memoryUsage.heapTotal)}, HeapUsed=${format(memoryUsage.heapUsed)}, External=${format(memoryUsage.external)}, ArrayBuffers=${format(memoryUsage.arrayBuffers)}`,
109
196
  )
110
197
  }
198
+ void printMem
111
199
  // printMem()
112
200
  // setInterval(printMem, 5000)
@@ -2,7 +2,11 @@ import type { MessagePort } from 'node:worker_threads'
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { workerData as _workerData } from 'node:worker_threads'
4
4
 
5
- import type { ThreadPortMessage } from 'nmtjs/runtime'
5
+ import type {
6
+ ThreadErrorMessage,
7
+ ThreadPortMessage,
8
+ WorkerThreadErrorOrigin,
9
+ } from 'nmtjs/runtime'
6
10
  import type { ModuleRunner } from 'vite/module-runner'
7
11
 
8
12
  export type RunWorkerOptions = {
@@ -18,56 +22,163 @@ const workerData = _workerData as RunWorkerOptions
18
22
  const ext = new URL(import.meta.url).pathname.endsWith('.ts') ? '.ts' : '.js'
19
23
  const workerPath = fileURLToPath(import.meta.resolve(`./worker${ext}`))
20
24
 
25
+ type WorkerModule = typeof import('./worker.ts')
26
+ type WorkerRuntime = Awaited<ReturnType<WorkerModule['run']>>
27
+
28
+ const kReportedError = Symbol.for('nmtjs.worker.reported-error')
29
+
30
+ let runner: ModuleRunner | undefined
31
+ let workerModule: WorkerModule
32
+ let runtime: WorkerRuntime | undefined
33
+ let runtimeStarted = false
34
+
21
35
  process.on('uncaughtException', (error) => {
22
- console.error(new Error('Uncaught Exception:', { cause: error }))
36
+ reportError(error, 'runtime', { fatal: true })
23
37
  })
24
38
 
25
39
  process.on('unhandledRejection', (error) => {
26
- console.error(new Error('Unhandled Promise Rejection:', { cause: error }))
40
+ reportError(error, 'runtime', { fatal: true })
27
41
  })
28
42
 
29
- process.on('exit', async (code) => {
30
- await runner?.close()
43
+ process.on('exit', () => {
44
+ void cleanup()
31
45
  })
32
46
 
33
- let runner: ModuleRunner
34
- let workerModule: typeof import('./worker.ts')
35
-
36
- try {
37
- if (workerData.vite) {
38
- const { createModuleRunner } = (await import(
39
- '../vite/runners/worker.ts'
40
- )) as typeof import('../vite/runners/worker.ts')
41
-
42
- runner = createModuleRunner(workerData.vite)
43
- workerModule = await runner.import(workerPath)
44
- } else {
45
- runner = undefined as any
46
- workerModule = await import(
47
- /* @vite-ignore */
48
- workerPath
49
- )
47
+ async function cleanup() {
48
+ await stopRuntime()
49
+ await closeRunner()
50
+ }
51
+
52
+ async function closeRunner() {
53
+ if (!runner) return
54
+ try {
55
+ await runner.close()
56
+ } catch (error) {
57
+ reportError(error, 'runtime', { fatal: false })
58
+ } finally {
59
+ runner = undefined
50
60
  }
61
+ }
51
62
 
52
- const runtime = await workerModule.run(workerData.runtime)
63
+ async function stopRuntime() {
64
+ if (!runtime) return
65
+ try {
66
+ if (runtimeStarted) {
67
+ await runtime.stop()
68
+ }
69
+ } catch (error) {
70
+ reportError(error, 'runtime', { fatal: false })
71
+ } finally {
72
+ runtimeStarted = false
73
+ }
74
+ }
53
75
 
54
- process.on('exit', async () => {
55
- await runtime.stop()
56
- })
76
+ function normalizeError(value: unknown): Error {
77
+ if (value instanceof Error) return value
78
+ if (typeof value === 'string') return new Error(value)
79
+ if (value && typeof value === 'object') {
80
+ try {
81
+ return new Error(JSON.stringify(value))
82
+ } catch {}
83
+ }
84
+ return new Error(String(value))
85
+ }
86
+
87
+ function serializeError(
88
+ error: Error,
89
+ origin: WorkerThreadErrorOrigin,
90
+ fatal: boolean,
91
+ ): ThreadErrorMessage {
92
+ return {
93
+ message: error.message,
94
+ name: error.name,
95
+ stack: error.stack,
96
+ origin,
97
+ fatal,
98
+ }
99
+ }
100
+
101
+ function reportError(
102
+ value: unknown,
103
+ origin: WorkerThreadErrorOrigin,
104
+ options: { fatal?: boolean } = {},
105
+ ): Error {
106
+ const fatal = options.fatal ?? origin !== 'runtime'
107
+ const error = normalizeError(value)
108
+ if (!(error as any)[kReportedError]) {
109
+ try {
110
+ workerData.port.postMessage({
111
+ type: 'error',
112
+ data: serializeError(error, origin, fatal),
113
+ } satisfies ThreadPortMessage)
114
+ } catch {}
115
+ console.error(new Error(`Worker thread ${origin} error`, { cause: error }))
116
+ ;(error as any)[kReportedError] = true
117
+ }
118
+ return error
119
+ }
120
+
121
+ async function loadWorkerModule() {
122
+ try {
123
+ if (workerData.vite) {
124
+ const { createModuleRunner } = (await import(
125
+ '../vite/runners/worker.ts'
126
+ )) as typeof import('../vite/runners/worker.ts')
127
+
128
+ runner = createModuleRunner(workerData.vite)
129
+ workerModule = await runner.import(workerPath)
130
+ } else {
131
+ runner = undefined
132
+ workerModule = await import(
133
+ /* @vite-ignore */
134
+ workerPath
135
+ )
136
+ }
137
+ } catch (error) {
138
+ throw reportError(error, 'bootstrap')
139
+ }
140
+ }
141
+
142
+ async function initializeRuntime() {
143
+ try {
144
+ runtime = await workerModule.run(workerData.runtime)
145
+ } catch (error) {
146
+ throw reportError(error, 'bootstrap')
147
+ }
57
148
 
58
149
  workerData.port.on('message', async (msg) => {
59
150
  if (msg.type === 'stop') {
60
- await runtime.stop()
151
+ await cleanup()
61
152
  process.exit(0)
62
153
  }
63
154
  })
155
+ }
64
156
 
65
- const hosts = (await runtime?.start()) || undefined
157
+ async function startRuntime() {
158
+ if (!runtime) return
159
+ try {
160
+ const hosts = (await runtime.start()) || undefined
161
+ runtimeStarted = true
162
+ workerData.port.postMessage({
163
+ type: 'ready',
164
+ data: { hosts },
165
+ } satisfies ThreadPortMessage)
166
+ } catch (error) {
167
+ throw reportError(error, 'start')
168
+ }
169
+ }
66
170
 
67
- workerData.port.postMessage({
68
- type: 'ready',
69
- data: { hosts },
70
- } satisfies ThreadPortMessage)
71
- } catch (error) {
72
- console.error(new Error('Worker thread error:', { cause: error }))
171
+ async function main() {
172
+ await loadWorkerModule()
173
+ await initializeRuntime()
174
+ await startRuntime()
73
175
  }
176
+
177
+ main().catch(async (error) => {
178
+ const normalized = error instanceof Error ? error : normalizeError(error)
179
+ if (!(normalized as any)[kReportedError]) {
180
+ reportError(normalized, 'bootstrap')
181
+ }
182
+ await cleanup()
183
+ process.exit(1)
184
+ })
@@ -17,12 +17,14 @@ import { ProtocolError } from '@nmtjs/protocol/server'
17
17
  import { NeemataTypeError, registerDefaultLocale, type } from '@nmtjs/type'
18
18
  import { prettifyError } from 'zod/mini'
19
19
 
20
+ import type { kDefaultProcedure as kDefaultProcedureKey } from './constants.ts'
20
21
  import type { AnyFilter } from './filters.ts'
21
22
  import type { AnyGuard } from './guards.ts'
22
23
  import type { AnyMiddleware } from './middlewares.ts'
23
24
  import type { AnyProcedure } from './procedure.ts'
24
25
  import type { AnyRouter } from './router.ts'
25
26
  import type { ApiCallContext } from './types.ts'
27
+ import { kDefaultProcedure } from './constants.ts'
26
28
 
27
29
  registerDefaultLocale()
28
30
 
@@ -39,7 +41,10 @@ export type ApiOptions = {
39
41
  timeout?: number
40
42
  container: Container
41
43
  logger: Logger
42
- procedures: Map<string, { procedure: AnyProcedure; path: AnyRouter[] }>
44
+ procedures: Map<
45
+ string | kDefaultProcedureKey,
46
+ { procedure: AnyProcedure; path: AnyRouter[] }
47
+ >
43
48
  guards: Set<AnyGuard>
44
49
  middlewares: Set<AnyMiddleware>
45
50
  filters: Set<AnyFilter>
@@ -57,8 +62,12 @@ export class ApplicationApi implements GatewayApi {
57
62
  constructor(public options: ApiOptions) {}
58
63
 
59
64
  find(procedureName: string) {
60
- const result = this.options.procedures.get(procedureName)
61
- if (result) return result
65
+ const procedure = this.options.procedures.get(procedureName)
66
+ if (procedure) return procedure
67
+
68
+ const fallback = this.options.procedures.get(kDefaultProcedure)
69
+ if (fallback) return fallback
70
+
62
71
  throw NotFound()
63
72
  }
64
73
 
@@ -1,6 +1,11 @@
1
1
  export const kProcedure: unique symbol = Symbol.for('neemata:ProcedureKey')
2
2
  export type kProcedure = typeof kProcedure
3
3
 
4
+ export const kDefaultProcedure: unique symbol = Symbol.for(
5
+ 'neemata:DefaultProcedureKey',
6
+ )
7
+ export type kDefaultProcedure = typeof kDefaultProcedure
8
+
4
9
  export const kRouter: unique symbol = Symbol.for('neemata:RouterKey')
5
10
  export type kRouter = typeof kRouter
6
11
 
@@ -6,6 +6,7 @@ import type {
6
6
  TRouteContract,
7
7
  TRouterContract,
8
8
  } from '@nmtjs/contract'
9
+ import type { AnyType } from '@nmtjs/type/any'
9
10
  import { c, IsRouterContract } from '@nmtjs/contract'
10
11
 
11
12
  import type { AnyGuard } from './guards.ts'
@@ -25,6 +26,7 @@ export interface AnyRouter {
25
26
  export interface AnyRootRouter extends AnyRouter {
26
27
  [kRootRouter]: any
27
28
  contract: TAnyRouterContract<Record<string, TRouteContract>, undefined>
29
+ default?: AnyProcedure<any>
28
30
  }
29
31
 
30
32
  export interface Router<Contract extends TAnyRouterContract> extends AnyRouter {
@@ -56,6 +58,7 @@ export interface RootRouter<
56
58
  >,
57
59
  > extends Router<Contract> {
58
60
  [kRootRouter]: any
61
+ default?: AnyProcedure<any>
59
62
  }
60
63
 
61
64
  export type MergeRoutersRoutesContracts<
@@ -79,17 +82,24 @@ export type ExtractRouterContracts<
79
82
  : []
80
83
 
81
84
  export function createRootRouter<Routers extends readonly AnyRouter[]>(
82
- ...routers: Routers
85
+ routers: Routers,
86
+ defaultProcedure?: AnyProcedure<
87
+ TProcedureContract<AnyType, AnyType, true | undefined, string | undefined>
88
+ >,
83
89
  ): RootRouter<
84
90
  TRouterContract<
85
- MergeRoutersRoutesContracts<ExtractRouterContracts<Routers>>,
91
+ MergeRoutersRoutesContracts<ExtractRouterContracts<[...Routers]>>,
86
92
  undefined
87
93
  >
88
94
  > {
89
95
  const routes: Record<string, any> = {}
90
96
  for (const router of routers) Object.assign(routes, router.routes)
91
97
  const router = createRouter({ name: undefined, routes: routes })
92
- return Object.freeze({ ...router, [kRootRouter]: true }) as any
98
+ return Object.freeze({
99
+ ...router,
100
+ default: defaultProcedure,
101
+ [kRootRouter]: true,
102
+ }) as any
93
103
  }
94
104
 
95
105
  export function createRouter<
@@ -3,9 +3,14 @@ import { EventEmitter } from 'node:events'
3
3
  import type { Logger } from '@nmtjs/core'
4
4
  import type { ProxyableTransportType } from '@nmtjs/gateway'
5
5
 
6
+ import type { ThreadPortMessageTypes, WorkerThreadError } from '../types.ts'
6
7
  import type { ServerApplicationConfig, ServerConfig } from './config.ts'
7
8
  import type { Thread } from './pool.ts'
8
- import type { ApplicationServerWorkerConfig } from './server.ts'
9
+ import type {
10
+ ApplicationServerWorkerConfig,
11
+ ApplicationWorkerErrorEvent,
12
+ ApplicationWorkerReadyEvent,
13
+ } from './server.ts'
9
14
  import { Pool } from './pool.ts'
10
15
 
11
16
  export type ApplicationProxyUpstream = {
@@ -48,6 +53,10 @@ export class ApplicationServerApplications extends EventEmitter<{
48
53
  })
49
54
  }
50
55
 
56
+ get appsNames() {
57
+ return this.params.applications
58
+ }
59
+
51
60
  async start() {
52
61
  const { logger, applications, applicationsConfig, serverConfig } =
53
62
  this.params
@@ -88,29 +97,45 @@ export class ApplicationServerApplications extends EventEmitter<{
88
97
  })
89
98
 
90
99
  thread.on('ready', ({ hosts }) => {
91
- if (!hosts?.length) return
100
+ this.removeThreadUpstreams(thread)
92
101
 
93
102
  const keys: Array<{ application: string; key: string }> = []
103
+ const sanitizedHosts: ThreadPortMessageTypes['ready']['hosts'] = []
94
104
 
95
- for (const host of hosts) {
96
- const url = new URL(host.url)
97
- // Convert 0.0.0.0 to 127.0.0.1 for local proxy access
98
- if (url.hostname === '0.0.0.0') url.hostname = '127.0.0.1'
105
+ if (hosts?.length) {
106
+ for (const host of hosts) {
107
+ const url = new URL(host.url)
108
+ if (url.hostname === '0.0.0.0') url.hostname = '127.0.0.1'
99
109
 
100
- const upstream: ApplicationProxyUpstream = {
101
- type: host.type,
102
- url: url.toString(),
103
- }
110
+ const normalizedUrl = url.toString()
111
+ sanitizedHosts.push({ type: host.type, url: normalizedUrl })
112
+
113
+ const upstream: ApplicationProxyUpstream = {
114
+ type: host.type,
115
+ url: normalizedUrl,
116
+ }
104
117
 
105
- const key = `${upstream.type}:${upstream.url}`
106
- keys.push({ application: applicationName, key })
107
- this.addUpstream(applicationName, key, upstream)
118
+ const key = `${upstream.type}:${upstream.url}`
119
+ keys.push({ application: applicationName, key })
120
+ this.addUpstream(applicationName, key, upstream)
121
+ }
108
122
  }
109
123
 
110
124
  this.upstreamsByThread.set(thread, keys)
125
+
126
+ this.emitWorkerReady({
127
+ application: applicationName,
128
+ threadId: thread.worker.threadId,
129
+ hosts: sanitizedHosts.length ? sanitizedHosts : undefined,
130
+ })
111
131
  })
112
132
 
113
- thread.once('error', () => {
133
+ thread.on('error', (error: WorkerThreadError) => {
134
+ this.emitWorkerError({
135
+ application: applicationName,
136
+ threadId: thread.worker.threadId,
137
+ error,
138
+ })
114
139
  this.removeThreadUpstreams(thread)
115
140
  })
116
141
 
@@ -168,4 +193,18 @@ export class ApplicationServerApplications extends EventEmitter<{
168
193
  }
169
194
  }
170
195
  }
196
+
197
+ protected emitWorkerReady(event: ApplicationWorkerReadyEvent) {
198
+ this.params.workerConfig.events?.emit('worker-ready', event)
199
+ }
200
+
201
+ protected emitWorkerError(event: ApplicationWorkerErrorEvent) {
202
+ this.params.logger.error(
203
+ new Error(
204
+ `Worker [${event.application}] thread ${event.threadId} error`,
205
+ { cause: event.error },
206
+ ),
207
+ )
208
+ this.params.workerConfig.events?.emit('worker-error', event)
209
+ }
171
210
  }
@@ -1,6 +1,7 @@
1
1
  import type { TSError } from '@nmtjs/common'
2
2
  import type { LoggingOptions } from '@nmtjs/core'
3
3
  import type { Transport } from '@nmtjs/gateway'
4
+ import type { ApplicationOptions, PortUpstreamOptions } from '@nmtjs/proxy'
4
5
  import type { Applications } from 'nmtjs/runtime/types'
5
6
 
6
7
  import type { ApplicationConfig } from '../application/config.ts'
@@ -64,6 +65,9 @@ export interface ServerConfig {
64
65
  proxy?: {
65
66
  port: number
66
67
  hostname: string
68
+ applications: {
69
+ [K in keyof Applications]?: Omit<ApplicationOptions, 'name'>
70
+ }
67
71
  threads?: number
68
72
  healthChecks?: { interval?: number }
69
73
  tls?: { key: string; cert: string }