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
@@ -6,9 +6,11 @@ import { MessageChannel, Worker } from 'node:worker_threads'
6
6
  import type {
7
7
  JobTaskResult,
8
8
  ServerPortMessageTypes,
9
+ ThreadErrorMessage,
9
10
  ThreadPortMessage,
10
11
  ThreadPortMessageTypes,
11
12
  WorkerJobTask,
13
+ WorkerThreadError,
12
14
  } from '../types.ts'
13
15
 
14
16
  const omitExecArgv = ['--expose-gc']
@@ -22,7 +24,7 @@ export type ThreadState =
22
24
 
23
25
  export class Thread extends EventEmitter<
24
26
  {
25
- error: [error: Error]
27
+ error: [error: WorkerThreadError]
26
28
  ready: [ThreadPortMessageTypes['ready']]
27
29
  task: [ThreadPortMessageTypes['task']]
28
30
  terminate: []
@@ -34,6 +36,8 @@ export class Thread extends EventEmitter<
34
36
  > {
35
37
  worker: Worker
36
38
  state: ThreadState = 'pending'
39
+ protected readyMessage?: ThreadPortMessageTypes['ready']
40
+ protected startPromise?: Promise<void>
37
41
 
38
42
  constructor(
39
43
  readonly port: MessagePort,
@@ -46,40 +50,99 @@ export class Thread extends EventEmitter<
46
50
  execArgv: process.execArgv.filter((f) => !omitExecArgv.includes(f)),
47
51
  })
48
52
 
49
- const handler = (msg: ThreadPortMessage) => {
53
+ this.port.on('message', (msg: ThreadPortMessage) => {
50
54
  const { type, data } = msg
51
- this.emit(type, data as any)
52
- if (type === 'task') {
53
- const { id, task } = data as ThreadPortMessageTypes['task']
54
- this.emit(`task-${id}`, task)
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
+ }
55
74
  }
56
- }
75
+ })
57
76
 
58
- this.port.on('message', handler)
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
+ })
59
91
  }
60
92
 
61
93
  async start() {
94
+ if (this.state === 'ready') return
95
+ if (this.startPromise) return this.startPromise
62
96
  switch (this.state) {
63
97
  case 'error':
64
98
  case 'terminating':
65
99
  case 'starting':
66
100
  throw new Error('Cannot start worker thread in current state')
67
- case 'pending': {
68
- // TODO: make timeout configurable
69
- const signal = AbortSignal.timeout(15000)
70
- try {
71
- await once(this, 'ready', { signal })
72
- } catch (err) {
73
- const error = new Error(
74
- 'Worker thread did not become ready in time',
75
- { cause: err },
76
- )
77
- this.emit('error', error)
78
- this.stop()
79
- throw error
80
- }
81
- }
101
+ case 'pending':
102
+ break
82
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
83
146
  }
84
147
 
85
148
  async stop() {
@@ -126,6 +189,20 @@ export class Thread extends EventEmitter<
126
189
  }
127
190
  }
128
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
+
129
206
  export class Pool extends EventEmitter<{
130
207
  workerReady: [worker: Worker]
131
208
  workerTaskResult: [worker: Worker]
@@ -49,7 +49,13 @@ export class ApplicationServerProxy {
49
49
  tls: config.tls
50
50
  ? { keyPath: config.tls.key, certPath: config.tls.cert }
51
51
  : undefined,
52
- applications: [],
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
+ })),
53
59
  })
54
60
 
55
61
  this.onAdd = (application, upstream) => {
@@ -1,9 +1,14 @@
1
+ import type EventEmitter from 'node:events'
1
2
  import type { Worker } from 'node:worker_threads'
2
3
 
3
4
  import type { Logger } from '@nmtjs/core'
4
5
  import { createLogger } from '@nmtjs/core'
5
6
 
6
- import type { Store } from '../types.ts'
7
+ import type {
8
+ Store,
9
+ ThreadPortMessageTypes,
10
+ WorkerThreadError,
11
+ } from '../types.ts'
7
12
  import type { ServerConfig } from './config.ts'
8
13
  import { createStoreClient } from '../store/index.ts'
9
14
  import { ApplicationServerApplications } from './applications.ts'
@@ -16,10 +21,27 @@ export type ApplicationServerRunOptions = {
16
21
  jobs: boolean
17
22
  }
18
23
 
24
+ export type ApplicationWorkerReadyEvent = {
25
+ application: string
26
+ threadId: number
27
+ hosts?: ThreadPortMessageTypes['ready']['hosts']
28
+ }
29
+
30
+ export type ApplicationWorkerErrorEvent = {
31
+ application: string
32
+ threadId: number
33
+ error: WorkerThreadError
34
+ }
35
+
19
36
  export type ApplicationServerWorkerConfig = {
20
37
  path: string
21
38
  workerData?: any
22
39
  worker?: (worker: Worker) => any
40
+ events?: EventEmitter<{
41
+ worker: [Worker]
42
+ 'worker-ready': [ApplicationWorkerReadyEvent]
43
+ 'worker-error': [ApplicationWorkerErrorEvent]
44
+ }>
23
45
  }
24
46
 
25
47
  export class ApplicationServer {
@@ -7,6 +7,21 @@ import type { ApplicationConfig } from './application/config.ts'
7
7
  import type { BaseRuntime } from './core/runtime.ts'
8
8
  import type { LifecycleHook, StoreType } from './enums.ts'
9
9
 
10
+ export type WorkerThreadErrorOrigin = 'bootstrap' | 'start' | 'runtime'
11
+
12
+ export type ThreadErrorMessage = {
13
+ message: string
14
+ name?: string
15
+ stack?: string
16
+ origin: WorkerThreadErrorOrigin
17
+ fatal: boolean
18
+ }
19
+
20
+ export type WorkerThreadError = Error & {
21
+ origin?: WorkerThreadErrorOrigin
22
+ fatal?: boolean
23
+ }
24
+
10
25
  export type ServerPortMessageTypes = {
11
26
  stop: undefined
12
27
  task: { id: string; task: WorkerJobTask }
@@ -14,7 +29,7 @@ export type ServerPortMessageTypes = {
14
29
 
15
30
  export type ThreadPortMessageTypes = {
16
31
  ready: { hosts?: { type: ProxyableTransportType; url: string }[] }
17
- error: { error: Error }
32
+ error: ThreadErrorMessage
18
33
  task: { id: string; task: JobTaskResult }
19
34
  }
20
35
 
@@ -11,6 +11,7 @@ import type {
11
11
  AnyMiddleware,
12
12
  AnyProcedure,
13
13
  AnyRouter,
14
+ kDefaultProcedure as kDefaultProcedureKey,
14
15
  } from '../application/index.ts'
15
16
  import type { ServerConfig } from '../server/config.ts'
16
17
  import { ApplicationApi } from '../application/api/api.ts'
@@ -19,6 +20,7 @@ import {
19
20
  isProcedure,
20
21
  isRootRouter,
21
22
  isRouter,
23
+ kDefaultProcedure,
22
24
  kRootRouter,
23
25
  } from '../application/index.ts'
24
26
  import { LifecycleHook, WorkerType } from '../enums.ts'
@@ -37,7 +39,10 @@ export class ApplicationWorkerRuntime extends BaseWorkerRuntime {
37
39
  transports!: GatewayOptions['transports']
38
40
 
39
41
  routers = new Map<string | kRootRouter, AnyRouter>()
40
- procedures = new Map<string, { procedure: AnyProcedure; path: AnyRouter[] }>()
42
+ procedures = new Map<
43
+ string | kDefaultProcedureKey,
44
+ { procedure: AnyProcedure; path: AnyRouter[] }
45
+ >()
41
46
  filters = new Set<AnyFilter>()
42
47
  middlewares = new Set<AnyMiddleware>()
43
48
  guards = new Set<AnyGuard>()
@@ -166,6 +171,16 @@ export class ApplicationWorkerRuntime extends BaseWorkerRuntime {
166
171
  this.routers.set(kRootRouter, router)
167
172
  this.registerRouter(router, [])
168
173
 
174
+ if (router.default) {
175
+ if (!isProcedure(router.default)) {
176
+ throw new Error('Root router default must be a procedure')
177
+ }
178
+ this.procedures.set(kDefaultProcedure, {
179
+ procedure: router.default,
180
+ path: [router],
181
+ })
182
+ }
183
+
169
184
  for (const filter of filters) this.filters.add(filter)
170
185
  for (const middleware of middlewares) this.middlewares.add(middleware)
171
186
  for (const guard of guards) this.guards.add(guard)
@@ -17,11 +17,17 @@ import { buildPlugins } from '../plugins.ts'
17
17
  import { createBroadcastChannel } from '../runners/worker.ts'
18
18
  import { createServer } from '../server.ts'
19
19
 
20
+ export type WorkerServerEventMap = {
21
+ worker: [Worker]
22
+ 'worker-error': [unknown]
23
+ 'worker-ready': [unknown]
24
+ }
25
+
20
26
  export async function createWorkerServer(
21
27
  options: ViteConfigOptions,
22
28
  mode: 'development' | 'production',
23
29
  neemataConfig: NeemataConfig,
24
- events: EventEmitter<{ worker: [Worker] }>,
30
+ events: EventEmitter<WorkerServerEventMap>,
25
31
  ): Promise<ViteDevServer> {
26
32
  const config = createConfig(options)
27
33
  const applicationEntries = Object.values(options.applicationImports).map(
@@ -94,6 +100,36 @@ export async function createWorkerServer(
94
100
  },
95
101
  }
96
102
 
103
+ let lastError: any
104
+
105
+ events.on('worker-error', (payload: any) => {
106
+ lastError = payload?.error ?? payload
107
+ const error =
108
+ lastError instanceof Error
109
+ ? lastError
110
+ : new Error(String(lastError))
111
+ const message = {
112
+ type: 'error',
113
+ err: {
114
+ message: error.message,
115
+ stack: error.stack ?? '',
116
+ plugin: 'neemata:worker',
117
+ },
118
+ } satisfies HotPayload
119
+ for (const client of clients.values()) {
120
+ client.send(message)
121
+ }
122
+ })
123
+
124
+ events.on('worker-ready', () => {
125
+ if (!lastError) return
126
+ lastError = undefined
127
+ const message: HotPayload = { type: 'full-reload' }
128
+ for (const client of clients.values()) {
129
+ client.send(message)
130
+ }
131
+ })
132
+
97
133
  const environment = new DevEnvironment(name, config, {
98
134
  hot: mode === 'development',
99
135
  transport,