nmtjs 0.15.0-beta.2 → 0.15.0-beta.21

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 +113 -0
  92. package/dist/runtime/jobs/manager.js +210 -0
  93. package/dist/runtime/jobs/manager.js.map +1 -0
  94. package/dist/runtime/jobs/router.d.ts +266 -0
  95. package/dist/runtime/jobs/router.js +432 -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 +23 -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 +348 -0
  207. package/src/runtime/jobs/router.ts +896 -0
  208. package/src/runtime/jobs/runner.ts +320 -0
  209. package/src/runtime/jobs/step.ts +66 -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,320 @@
1
+ import type { Container, Logger, LoggingOptions } from '@nmtjs/core'
2
+ import type { Job } from 'bullmq'
3
+ import { anyAbortSignal } from '@nmtjs/common'
4
+ import { Scope } from '@nmtjs/core'
5
+ import { UnrecoverableError } from 'bullmq'
6
+
7
+ import type { LifecycleHooks } from '../core/hooks.ts'
8
+ import type { AnyJob } from './job.ts'
9
+ import type { AnyJobStep } from './step.ts'
10
+ import { LifecycleHook } from '../enums.ts'
11
+ import { jobAbortSignal } from '../injectables.ts'
12
+
13
+ export type JobRunnerOptions = { logging?: LoggingOptions }
14
+
15
+ export interface StepResultEntry {
16
+ data: Record<string, unknown> | null
17
+ duration: number
18
+ }
19
+
20
+ export interface JobRunnerRunOptions {
21
+ signal: AbortSignal
22
+ result: Record<string, unknown>
23
+ stepResults: StepResultEntry[]
24
+ currentStepIndex: number
25
+ progress: Record<string, unknown>
26
+ }
27
+
28
+ export interface JobRunnerRunBeforeStepParams<
29
+ Options extends JobRunnerRunOptions,
30
+ > {
31
+ job: AnyJob
32
+ step: AnyJobStep
33
+ stepIndex: number
34
+ result: Record<string, unknown>
35
+ stepResults: StepResultEntry[]
36
+ options: Options
37
+ }
38
+
39
+ export interface JobRunnerRunAfterStepParams<
40
+ Options extends JobRunnerRunOptions,
41
+ > extends JobRunnerRunBeforeStepParams<Options> {
42
+ stepResult: StepResultEntry
43
+ }
44
+
45
+ export class JobRunner<
46
+ RunOptions extends JobRunnerRunOptions = JobRunnerRunOptions,
47
+ > {
48
+ logger: Logger
49
+
50
+ constructor(
51
+ protected runtime: {
52
+ logger: Logger
53
+ container: Container
54
+ lifecycleHooks: LifecycleHooks
55
+ },
56
+ ) {
57
+ this.logger = runtime.logger.child({ $group: JobRunner.name })
58
+ }
59
+
60
+ get container() {
61
+ return this.runtime.container
62
+ }
63
+
64
+ async runJob<T extends AnyJob>(
65
+ job: T,
66
+ data: any,
67
+ options: Partial<RunOptions> = {},
68
+ ): Promise<T['_']['output']> {
69
+ const {
70
+ signal: runSignal,
71
+ result: runResult = {},
72
+ stepResults: runStepResults = [] as RunOptions['stepResults'],
73
+ progress: runProgress = {},
74
+ currentStepIndex = 0,
75
+ ...rest
76
+ } = options
77
+
78
+ const { input, output, steps } = job
79
+
80
+ const result: Record<string, unknown> = { ...runResult }
81
+ const decodedInput = input.decode(data)
82
+
83
+ // Initialize progress: decode from checkpoint or start fresh
84
+ const progress: Record<string, unknown> = job.progress
85
+ ? job.progress.decode(runProgress)
86
+ : { ...runProgress }
87
+
88
+ using stopListener = this.runtime.lifecycleHooks.once(
89
+ LifecycleHook.BeforeDispose,
90
+ )
91
+ const signal = anyAbortSignal(runSignal, stopListener.signal)
92
+ await using container = this.container.fork(Scope.Global)
93
+ await container.provide(jobAbortSignal, signal)
94
+
95
+ const jobDependencyContext = await container.createContext(job.dependencies)
96
+ const jobData = job.options.data
97
+ ? await job.options.data(jobDependencyContext, decodedInput, progress)
98
+ : undefined
99
+
100
+ const stepResults: StepResultEntry[] = Array.from({ length: steps.length })
101
+
102
+ // Restore previous step results and reconstruct accumulated result
103
+ for (let stepIndex = 0; stepIndex < runStepResults.length; stepIndex++) {
104
+ const entry = runStepResults[stepIndex]
105
+ stepResults[stepIndex] = entry
106
+ if (entry?.data) Object.assign(result, entry.data)
107
+ }
108
+
109
+ // @ts-expect-error
110
+ const runOptions: RunOptions = {
111
+ signal,
112
+ result,
113
+ stepResults,
114
+ currentStepIndex: currentStepIndex,
115
+ progress,
116
+ ...rest,
117
+ } satisfies JobRunnerRunOptions
118
+
119
+ for (
120
+ let stepIndex = currentStepIndex;
121
+ stepIndex < steps.length;
122
+ stepIndex++
123
+ ) {
124
+ const step = steps[stepIndex]
125
+ const resultSnapshot = Object.freeze(
126
+ Object.assign({}, decodedInput, result),
127
+ )
128
+
129
+ try {
130
+ if (signal.aborted) {
131
+ const { reason } = signal
132
+ if (reason instanceof UnrecoverableError) throw reason
133
+ throw new UnrecoverableError('Job cancelled')
134
+ }
135
+
136
+ const condition = job.conditions.get(stepIndex)
137
+ if (condition) {
138
+ const shouldRun = await condition({
139
+ context: jobDependencyContext,
140
+ data: jobData,
141
+ input: decodedInput,
142
+ result: resultSnapshot,
143
+ progress,
144
+ })
145
+ if (!shouldRun) {
146
+ stepResults[stepIndex] = { data: null, duration: 0 }
147
+ continue
148
+ }
149
+ }
150
+
151
+ const stepStartTime = Date.now()
152
+
153
+ await this.beforeStep({
154
+ job,
155
+ step,
156
+ stepIndex,
157
+ result,
158
+ options: runOptions,
159
+ stepResults,
160
+ })
161
+
162
+ const stepContext = await container.createContext(step.dependencies)
163
+ const stepInput = step.input.decode(resultSnapshot)
164
+
165
+ await job.beforeEachHandler?.({
166
+ context: jobDependencyContext,
167
+ data: jobData,
168
+ input: decodedInput,
169
+ result: resultSnapshot,
170
+ progress,
171
+ step,
172
+ stepIndex,
173
+ })
174
+
175
+ const handlerReturn = await step.handler(
176
+ stepContext,
177
+ stepInput,
178
+ jobData,
179
+ )
180
+
181
+ const produced = step.output.encode(handlerReturn ?? {})
182
+ const duration = Date.now() - stepStartTime
183
+
184
+ stepResults[stepIndex] = { data: produced, duration }
185
+ Object.assign(result, produced)
186
+
187
+ await job.afterEachHandler?.({
188
+ context: jobDependencyContext,
189
+ data: jobData,
190
+ input: decodedInput,
191
+ result,
192
+ progress,
193
+ step,
194
+ stepIndex,
195
+ })
196
+
197
+ await this.afterStep({
198
+ job,
199
+ step,
200
+ stepIndex,
201
+ result,
202
+ stepResult: stepResults[stepIndex],
203
+ stepResults,
204
+ options: runOptions,
205
+ })
206
+ } catch (error) {
207
+ const wrapped = new Error(`Error during step [${stepIndex}]`, {
208
+ cause: error,
209
+ })
210
+ this.logger.error(wrapped)
211
+
212
+ const allowRetry = await job.onErrorHandler?.({
213
+ context: jobDependencyContext,
214
+ data: jobData,
215
+ input: decodedInput,
216
+ result: result,
217
+ progress,
218
+ step,
219
+ stepIndex,
220
+ error,
221
+ })
222
+
223
+ if (allowRetry === false) {
224
+ throw new UnrecoverableError('Job failed (unrecoverable)')
225
+ }
226
+
227
+ throw wrapped
228
+ }
229
+ }
230
+
231
+ const finalPayload = await job.returnHandler!({
232
+ context: jobDependencyContext,
233
+ data: jobData,
234
+ input: decodedInput,
235
+ result,
236
+ progress,
237
+ })
238
+
239
+ return output.encode(finalPayload)
240
+ }
241
+
242
+ protected async beforeStep(
243
+ params: JobRunnerRunBeforeStepParams<RunOptions>,
244
+ ): Promise<void> {
245
+ this.logger.debug(
246
+ {
247
+ job: params.job.name,
248
+ step: params.step.label || params.stepIndex + 1,
249
+ stepIndex: params.stepIndex,
250
+ },
251
+ 'Executing job step',
252
+ )
253
+ }
254
+
255
+ protected async afterStep(
256
+ params: JobRunnerRunAfterStepParams<RunOptions>,
257
+ ): Promise<void> {
258
+ this.logger.debug(
259
+ {
260
+ job: params.job.name,
261
+ step: params.step.label || params.stepIndex + 1,
262
+ stepIndex: params.stepIndex,
263
+ },
264
+ 'Completed job step',
265
+ )
266
+ }
267
+ }
268
+
269
+ export class ApplicationWorkerJobRunner extends JobRunner<
270
+ JobRunnerRunOptions & { queueJob: Job }
271
+ > {
272
+ constructor(
273
+ protected runtime: {
274
+ logger: Logger
275
+ container: Container
276
+ lifecycleHooks: LifecycleHooks
277
+ },
278
+ ) {
279
+ super(runtime)
280
+ }
281
+
282
+ protected async afterStep(
283
+ params: JobRunnerRunAfterStepParams<
284
+ JobRunnerRunOptions & { queueJob: Job }
285
+ >,
286
+ ): Promise<void> {
287
+ await super.afterStep(params)
288
+ const {
289
+ job,
290
+ step,
291
+ result,
292
+ stepResult,
293
+ stepResults,
294
+ stepIndex,
295
+ options: { queueJob, progress },
296
+ } = params
297
+ const nextStepIndex = stepIndex + 1
298
+ const totalSteps = job.steps.length
299
+ const percentage = Math.round((nextStepIndex / totalSteps) * 100)
300
+
301
+ // Encode progress before persisting if schema is defined
302
+ const encodedProgress = job.progress
303
+ ? job.progress.encode(progress)
304
+ : progress
305
+
306
+ await Promise.all([
307
+ queueJob.log(
308
+ `Step ${step.label || nextStepIndex} completed in ${(stepResult.duration / 1000).toFixed(3)}s`,
309
+ ),
310
+ queueJob.updateProgress({
311
+ stepIndex: nextStepIndex,
312
+ stepLabel: step.label,
313
+ result,
314
+ stepResults,
315
+ progress: encodedProgress,
316
+ percentage,
317
+ }),
318
+ ])
319
+ }
320
+ }
@@ -0,0 +1,66 @@
1
+ import type { MaybePromise } from '@nmtjs/common'
2
+ import type { Dependant, Dependencies, DependencyContext } from '@nmtjs/core'
3
+ import type { AnyObjectLikeType, ObjectType } from '@nmtjs/type/object'
4
+ import { tryCaptureStackTrace } from '@nmtjs/common'
5
+ import { t } from '@nmtjs/type'
6
+
7
+ import { kJobStepKey } from '../constants.ts'
8
+
9
+ export type AnyJobStep = JobStep<any, any, any, any, any>
10
+
11
+ export type JobStepHandler<
12
+ Deps extends Dependencies,
13
+ Input extends AnyObjectLikeType,
14
+ Output extends AnyObjectLikeType,
15
+ Return,
16
+ Data = any,
17
+ > = (
18
+ context: DependencyContext<Deps>,
19
+ input: t.infer.decode.output<Input>,
20
+ data: Data,
21
+ ) => MaybePromise<null extends Return ? t.infer.encode.input<Output> : Return>
22
+
23
+ export interface JobStep<
24
+ Input extends AnyObjectLikeType = AnyObjectLikeType,
25
+ Output extends AnyObjectLikeType = AnyObjectLikeType,
26
+ Deps extends Dependencies = Dependencies,
27
+ Return = unknown,
28
+ Data = any,
29
+ > extends Dependant {
30
+ [kJobStepKey]: any
31
+ label?: string
32
+ input: Input
33
+ output: Output
34
+ dependencies: Deps
35
+ handler: JobStepHandler<Deps, Input, Output, Return, Data>
36
+ }
37
+
38
+ export function createStep<
39
+ Input extends AnyObjectLikeType,
40
+ Output extends AnyObjectLikeType = ObjectType<{}>,
41
+ Deps extends Dependencies = {},
42
+ Return = unknown,
43
+ Data = any,
44
+ >(step: {
45
+ label?: string
46
+ input: Input
47
+ output?: Output
48
+ dependencies?: Deps
49
+ handler: JobStepHandler<Deps, Input, Output, Return, Data>
50
+ }): JobStep<Input, Output, Deps, Return, Data> {
51
+ return Object.freeze({
52
+ [kJobStepKey]: true,
53
+ output: t.object({}) as unknown as Output,
54
+ dependencies: {} as Deps,
55
+ stack: tryCaptureStackTrace(),
56
+ ...step,
57
+ })
58
+ }
59
+
60
+ export function isJobStep(value: unknown): value is AnyJobStep {
61
+ return (
62
+ typeof value === 'object' &&
63
+ value !== null &&
64
+ (value as AnyJobStep)[kJobStepKey] === true
65
+ )
66
+ }
@@ -0,0 +1,21 @@
1
+ import { createServer } from 'node:http'
2
+
3
+ import type { Queue } from 'bullmq'
4
+ import { createBullBoard } from '@bull-board/api'
5
+ import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'
6
+ import { H3Adapter } from '@bull-board/h3'
7
+ import { createApp, toNodeListener } from 'h3'
8
+
9
+ export function createJobsUI(queues: Queue[]) {
10
+ const app = createApp()
11
+ const serverAdapter = new H3Adapter()
12
+ createBullBoard({
13
+ queues: queues.map((q) => new BullMQAdapter(q, { readOnlyMode: true })),
14
+ serverAdapter,
15
+ })
16
+ const router = serverAdapter.registerHandlers()
17
+ app.use(router)
18
+ return createServer(toNodeListener(app))
19
+ }
20
+
21
+ export type JobsUI = ReturnType<typeof createJobsUI>
@@ -0,0 +1,211 @@
1
+ import assert from 'node:assert'
2
+ import { createHash } from 'node:crypto'
3
+ import { PassThrough, Readable } from 'node:stream'
4
+
5
+ import type {
6
+ SubcriptionOptions,
7
+ TAnyEventContract,
8
+ TAnySubscriptionContract,
9
+ } from '@nmtjs/contract'
10
+ import type { Container, Logger } from '@nmtjs/core'
11
+ import type { t } from '@nmtjs/type'
12
+ import { isAbortError } from '@nmtjs/common'
13
+
14
+ import { pubSubAdapter } from '../injectables.ts'
15
+
16
+ export type PubSubAdapterEvent = { channel: string; payload: any }
17
+
18
+ export interface PubSubAdapterType {
19
+ publish(channel: string, payload: any): Promise<boolean>
20
+ subscribe(
21
+ channel: string,
22
+ signal?: AbortSignal,
23
+ ): AsyncGenerator<PubSubAdapterEvent>
24
+ initialize(): Promise<void>
25
+ dispose(): Promise<void>
26
+ }
27
+
28
+ export type PubSubChannel = {
29
+ stream: Readable
30
+ subscription: TAnySubscriptionContract
31
+ event: TAnyEventContract
32
+ }
33
+
34
+ export type PubSubSubscribe = <
35
+ Contract extends TAnySubscriptionContract,
36
+ Events extends {
37
+ [K in keyof Contract['events']]?: true
38
+ },
39
+ >(
40
+ subscription: Contract,
41
+ events: Events,
42
+ options: Contract['options'],
43
+ signal?: AbortSignal,
44
+ ) => Omit<Readable, typeof Symbol.asyncIterator> & {
45
+ [Symbol.asyncIterator]: () => AsyncIterator<
46
+ {} extends Events
47
+ ? {
48
+ [K in keyof Contract['events']]: {
49
+ event: K
50
+ data: t.infer.decode.output<Contract['events'][K]['payload']>
51
+ }
52
+ }[keyof Contract['events']]
53
+ : {
54
+ [K in keyof Events]: K extends keyof Contract['events']
55
+ ? {
56
+ event: K
57
+ data: t.infer.decode.output<Contract['events'][K]['payload']>
58
+ }
59
+ : never
60
+ }[keyof Events]
61
+ >
62
+ }
63
+
64
+ export type PubSubPublish = <
65
+ S extends TAnySubscriptionContract,
66
+ E extends S['events'][keyof S['events']],
67
+ >(
68
+ event: E,
69
+ options: S['options'],
70
+ data: t.infer.decode.input<E['payload']>,
71
+ ) => Promise<boolean>
72
+
73
+ export type PubSubManagerOptions = { logger: Logger; container: Container }
74
+
75
+ export class PubSubManager {
76
+ readonly subscriptions = new Map<string, PubSubChannel>()
77
+
78
+ constructor(protected readonly options: PubSubManagerOptions) {}
79
+
80
+ protected get adapter() {
81
+ return this.options.container.get(pubSubAdapter)
82
+ }
83
+
84
+ subscribe: PubSubSubscribe = (subscription, events, options, signal) => {
85
+ assert(this.adapter, 'PubSub adapter is not configured')
86
+
87
+ const eventKeys =
88
+ Object.keys(events).length === 0
89
+ ? Object.keys(subscription.events)
90
+ : Object.keys(events)
91
+
92
+ const streams = Array(eventKeys.length)
93
+
94
+ for (const index in eventKeys) {
95
+ const event = subscription.events[eventKeys[index]]
96
+ const channel = getChannelName(event, options)
97
+ if (this.subscriptions.has(channel)) {
98
+ streams[index] = this.subscriptions.get(channel)!.stream
99
+ } else {
100
+ const iterable = this.adapter.subscribe(channel, signal)
101
+ const stream = this.createEventStream(iterable)
102
+ stream.on('close', () => this.subscriptions.delete(channel))
103
+ streams[index] = stream
104
+ this.subscriptions.set(channel, { subscription, event, stream })
105
+ }
106
+ }
107
+
108
+ return mergeEventStreams(streams, signal)
109
+ }
110
+
111
+ publish: PubSubPublish = async (event, options, data) => {
112
+ assert(this.adapter, 'PubSub adapter is not configured')
113
+
114
+ const channel = getChannelName(event, options)
115
+
116
+ try {
117
+ const payload = event.payload.encode(data)
118
+ return await this.adapter.publish(channel, payload)
119
+ } catch (error: any) {
120
+ this.options.logger.error(
121
+ `Failed to publish event "${event.name}" on channel "${channel}": ${error.message}`,
122
+ )
123
+ return Promise.reject(error)
124
+ }
125
+ }
126
+
127
+ private createEventStream(
128
+ iterable: AsyncGenerator<PubSubAdapterEvent>,
129
+ ): Readable {
130
+ const { subscriptions } = this
131
+ return new Readable({
132
+ objectMode: true,
133
+ read() {
134
+ iterable.next().then(
135
+ ({ value, done }) => {
136
+ if (done) {
137
+ this.push(null)
138
+ } else {
139
+ const subscription = subscriptions.get(value.channel)
140
+ if (subscription) {
141
+ const { event } = subscription
142
+ try {
143
+ const data = event.payload.decode(value.payload)
144
+ this.push({ event: event.name, data })
145
+ } catch (error: any) {
146
+ this.destroy(error)
147
+ }
148
+ }
149
+ }
150
+ },
151
+ (error) => {
152
+ if (isAbortError(error)) {
153
+ this.push(null)
154
+ } else {
155
+ this.destroy(error)
156
+ }
157
+ },
158
+ )
159
+ },
160
+ })
161
+ }
162
+ }
163
+
164
+ function concat(...args: any) {
165
+ return args.filter(Boolean).join('/')
166
+ }
167
+
168
+ function getChannelName<T extends TAnyEventContract>(
169
+ contract: T,
170
+ options: T['options'],
171
+ ) {
172
+ const key = options ? serializerOptions(options) : ''
173
+ assert(contract.name, 'Event contract must have a name')
174
+ return concat(contract.name, key)
175
+ }
176
+
177
+ function serializerOptions(options: Exclude<SubcriptionOptions, null>): string {
178
+ const hash = createHash('sha1')
179
+ const serialized = Object.entries(options)
180
+ .sort((a, b) => a[0].localeCompare(b[0]))
181
+ .map(([key, value]) => `${key}=${value}`)
182
+ .join(';')
183
+ hash.update(serialized)
184
+ return hash.digest('base64url')
185
+ }
186
+
187
+ function mergeEventStreams(
188
+ streams: Readable[],
189
+ signal?: AbortSignal,
190
+ ): Readable {
191
+ const destination = new PassThrough({
192
+ signal,
193
+ objectMode: true,
194
+ readableObjectMode: true,
195
+ writableObjectMode: true,
196
+ })
197
+
198
+ let ended = 0
199
+
200
+ for (const source of streams) {
201
+ source.pipe(destination, { end: false })
202
+ source.once('end', () => {
203
+ ended++
204
+ if (ended === streams.length) {
205
+ destination.end()
206
+ }
207
+ })
208
+ }
209
+
210
+ return destination
211
+ }