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.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -0
- package/dist/entrypoints/cli.d.ts +1 -0
- package/dist/entrypoints/cli.js +1 -0
- package/dist/entrypoints/cli.js.map +1 -0
- package/dist/entrypoints/main.d.ts +5 -0
- package/dist/entrypoints/main.js +83 -15
- package/dist/entrypoints/main.js.map +1 -0
- package/dist/entrypoints/thread.d.ts +14 -0
- package/dist/entrypoints/thread.js +130 -24
- package/dist/entrypoints/thread.js.map +1 -0
- package/dist/entrypoints/worker.d.ts +3 -0
- package/dist/entrypoints/worker.js +4 -3
- package/dist/entrypoints/worker.js.map +1 -0
- package/dist/index.d.ts +69 -0
- package/dist/{_exports/index.js → index.js} +9 -5
- package/dist/index.js.map +1 -0
- package/dist/resolver.d.ts +2 -0
- package/dist/resolver.js +1 -0
- package/dist/resolver.js.map +1 -0
- package/dist/runtime/application/api/api.d.ts +49 -0
- package/dist/runtime/application/api/api.js +193 -0
- package/dist/runtime/application/api/api.js.map +1 -0
- package/dist/runtime/application/api/constants.d.ts +14 -0
- package/dist/runtime/application/api/constants.js +8 -0
- package/dist/runtime/application/api/constants.js.map +1 -0
- package/dist/runtime/application/api/filters.d.ts +14 -0
- package/dist/runtime/application/api/filters.js +11 -0
- package/dist/runtime/application/api/filters.js.map +1 -0
- package/dist/runtime/application/api/guards.d.ts +13 -0
- package/dist/runtime/application/api/guards.js +8 -0
- package/dist/runtime/application/api/guards.js.map +1 -0
- package/dist/runtime/application/api/index.d.ts +8 -0
- package/dist/runtime/application/api/index.js +9 -0
- package/dist/runtime/application/api/index.js.map +1 -0
- package/dist/runtime/application/api/middlewares.d.ts +14 -0
- package/dist/runtime/application/api/middlewares.js +12 -0
- package/dist/runtime/application/api/middlewares.js.map +1 -0
- package/dist/runtime/application/api/procedure.d.ts +67 -0
- package/dist/runtime/application/api/procedure.js +50 -0
- package/dist/runtime/application/api/procedure.js.map +1 -0
- package/dist/runtime/application/api/router.d.ts +71 -0
- package/dist/runtime/application/api/router.js +51 -0
- package/dist/runtime/application/api/router.js.map +1 -0
- package/dist/runtime/application/api/types.d.ts +32 -0
- package/dist/runtime/application/api/types.js +2 -0
- package/dist/runtime/application/api/types.js.map +1 -0
- package/dist/runtime/application/config.d.ts +26 -0
- package/dist/runtime/application/config.js +21 -0
- package/dist/runtime/application/config.js.map +1 -0
- package/dist/runtime/application/constants.d.ts +2 -0
- package/dist/runtime/application/constants.js +2 -0
- package/dist/runtime/application/constants.js.map +1 -0
- package/dist/runtime/application/hook.d.ts +19 -0
- package/dist/runtime/application/hook.js +11 -0
- package/dist/runtime/application/hook.js.map +1 -0
- package/dist/runtime/application/hooks.d.ts +3 -0
- package/dist/runtime/application/hooks.js +4 -0
- package/dist/runtime/application/hooks.js.map +1 -0
- package/dist/runtime/application/index.d.ts +5 -0
- package/dist/runtime/application/index.js +6 -0
- package/dist/runtime/application/index.js.map +1 -0
- package/dist/runtime/constants.d.ts +8 -0
- package/dist/runtime/constants.js +5 -0
- package/dist/runtime/constants.js.map +1 -0
- package/dist/runtime/core/hooks.d.ts +4 -0
- package/dist/runtime/core/hooks.js +4 -0
- package/dist/runtime/core/hooks.js.map +1 -0
- package/dist/runtime/core/plugin.d.ts +8 -0
- package/dist/runtime/core/plugin.js +4 -0
- package/dist/runtime/core/plugin.js.map +1 -0
- package/dist/runtime/core/runtime.d.ts +27 -0
- package/dist/runtime/core/runtime.js +81 -0
- package/dist/runtime/core/runtime.js.map +1 -0
- package/dist/runtime/enums.d.ts +21 -0
- package/dist/runtime/enums.js +26 -0
- package/dist/runtime/enums.js.map +1 -0
- package/dist/runtime/index.d.ts +21 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/injectables.d.ts +23 -0
- package/dist/runtime/injectables.js +20 -0
- package/dist/runtime/injectables.js.map +1 -0
- package/dist/runtime/jobs/job.d.ts +132 -0
- package/dist/runtime/jobs/job.js +68 -0
- package/dist/runtime/jobs/job.js.map +1 -0
- package/dist/runtime/jobs/manager.d.ts +113 -0
- package/dist/runtime/jobs/manager.js +210 -0
- package/dist/runtime/jobs/manager.js.map +1 -0
- package/dist/runtime/jobs/router.d.ts +266 -0
- package/dist/runtime/jobs/router.js +432 -0
- package/dist/runtime/jobs/router.js.map +1 -0
- package/dist/runtime/jobs/runner.d.ts +64 -0
- package/dist/runtime/jobs/runner.js +256 -0
- package/dist/runtime/jobs/runner.js.map +1 -0
- package/dist/runtime/jobs/step.d.ts +23 -0
- package/dist/runtime/jobs/step.js +18 -0
- package/dist/runtime/jobs/step.js.map +1 -0
- package/dist/runtime/jobs/ui.d.ts +3 -0
- package/dist/runtime/jobs/ui.js +17 -0
- package/dist/runtime/jobs/ui.js.map +1 -0
- package/dist/runtime/pubsub/manager.d.ts +48 -0
- package/dist/runtime/pubsub/manager.js +119 -0
- package/dist/runtime/pubsub/manager.js.map +1 -0
- package/dist/runtime/pubsub/redis.d.ts +16 -0
- package/dist/runtime/pubsub/redis.js +98 -0
- package/dist/runtime/pubsub/redis.js.map +1 -0
- package/dist/runtime/scheduler/index.d.ts +22 -0
- package/dist/runtime/scheduler/index.js +20 -0
- package/dist/runtime/scheduler/index.js.map +1 -0
- package/dist/runtime/server/applications.d.ts +52 -0
- package/dist/runtime/server/applications.js +133 -0
- package/dist/runtime/server/applications.js.map +1 -0
- package/dist/runtime/server/config.d.ts +121 -0
- package/dist/runtime/server/config.js +33 -0
- package/dist/runtime/server/config.js.map +1 -0
- package/dist/runtime/server/jobs.d.ts +41 -0
- package/dist/runtime/server/jobs.js +181 -0
- package/dist/runtime/server/jobs.js.map +1 -0
- package/dist/runtime/server/pool.d.ts +54 -0
- package/dist/runtime/server/pool.js +194 -0
- package/dist/runtime/server/pool.js.map +1 -0
- package/dist/runtime/server/proxy.d.ts +21 -0
- package/dist/runtime/server/proxy.js +79 -0
- package/dist/runtime/server/proxy.js.map +1 -0
- package/dist/runtime/server/server.d.ts +53 -0
- package/dist/runtime/server/server.js +90 -0
- package/dist/runtime/server/server.js.map +1 -0
- package/dist/runtime/store/index.d.ts +3 -0
- package/dist/runtime/store/index.js +23 -0
- package/dist/runtime/store/index.js.map +1 -0
- package/dist/runtime/types.d.ts +103 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/runtime/workers/application.d.ts +47 -0
- package/dist/runtime/workers/application.js +162 -0
- package/dist/runtime/workers/application.js.map +1 -0
- package/dist/runtime/workers/base.d.ts +16 -0
- package/dist/runtime/workers/base.js +46 -0
- package/dist/runtime/workers/base.js.map +1 -0
- package/dist/runtime/workers/cli.d.ts +1 -0
- package/dist/runtime/workers/cli.js +2 -0
- package/dist/runtime/workers/cli.js.map +1 -0
- package/dist/runtime/workers/job.d.ts +20 -0
- package/dist/runtime/workers/job.js +172 -0
- package/dist/runtime/workers/job.js.map +1 -0
- package/dist/typings.d.ts +5 -0
- package/dist/typings.js +4 -3
- package/dist/typings.js.map +1 -0
- package/dist/vite/builder.d.ts +5 -0
- package/dist/vite/builder.js +5 -1
- package/dist/vite/builder.js.map +1 -0
- package/dist/vite/config.d.ts +28 -0
- package/dist/vite/config.js +1 -0
- package/dist/vite/config.js.map +1 -0
- package/dist/vite/plugins.d.ts +2 -0
- package/dist/vite/plugins.js +1 -0
- package/dist/vite/plugins.js.map +1 -0
- package/dist/vite/runners/worker.d.ts +4 -0
- package/dist/vite/runners/worker.js +1 -0
- package/dist/vite/runners/worker.js.map +1 -0
- package/dist/vite/server.d.ts +3 -0
- package/dist/vite/server.js +6 -1
- package/dist/vite/server.js.map +1 -0
- package/dist/vite/servers/main.d.ts +8 -0
- package/dist/vite/servers/main.js +1 -0
- package/dist/vite/servers/main.js.map +1 -0
- package/dist/vite/servers/worker.d.ts +11 -0
- package/dist/vite/servers/worker.js +28 -0
- package/dist/vite/servers/worker.js.map +1 -0
- package/package.json +31 -18
- package/src/cli.ts +144 -0
- package/src/config.ts +64 -0
- package/src/entrypoints/cli.ts +13 -0
- package/src/entrypoints/main.ts +200 -0
- package/src/entrypoints/thread.ts +184 -0
- package/src/entrypoints/worker.ts +48 -0
- package/src/index.ts +82 -0
- package/src/resolver.ts +16 -0
- package/src/runtime/application/api/api.ts +265 -0
- package/src/runtime/application/api/constants.ts +22 -0
- package/src/runtime/application/api/filters.ts +39 -0
- package/src/runtime/application/api/guards.ts +29 -0
- package/src/runtime/application/api/index.ts +8 -0
- package/src/runtime/application/api/middlewares.ts +37 -0
- package/src/runtime/application/api/procedure.ts +229 -0
- package/src/runtime/application/api/router.ts +193 -0
- package/src/runtime/application/api/types.ts +124 -0
- package/src/runtime/application/config.ts +69 -0
- package/src/runtime/application/constants.ts +4 -0
- package/src/runtime/application/hook.ts +51 -0
- package/src/runtime/application/hooks.ts +3 -0
- package/src/runtime/application/index.ts +5 -0
- package/src/runtime/constants.ts +13 -0
- package/src/runtime/core/hooks.ts +5 -0
- package/src/runtime/core/plugin.ts +13 -0
- package/src/runtime/core/runtime.ts +109 -0
- package/src/runtime/enums.ts +24 -0
- package/src/runtime/index.ts +21 -0
- package/src/runtime/injectables.ts +61 -0
- package/src/runtime/jobs/job.ts +370 -0
- package/src/runtime/jobs/manager.ts +348 -0
- package/src/runtime/jobs/router.ts +896 -0
- package/src/runtime/jobs/runner.ts +320 -0
- package/src/runtime/jobs/step.ts +66 -0
- package/src/runtime/jobs/ui.ts +21 -0
- package/src/runtime/pubsub/manager.ts +211 -0
- package/src/runtime/pubsub/redis.ts +108 -0
- package/src/runtime/scheduler/index.ts +39 -0
- package/src/runtime/server/applications.ts +210 -0
- package/src/runtime/server/config.ts +158 -0
- package/src/runtime/server/jobs.ts +250 -0
- package/src/runtime/server/pool.ts +260 -0
- package/src/runtime/server/proxy.ts +118 -0
- package/src/runtime/server/server.ts +155 -0
- package/src/runtime/store/index.ts +30 -0
- package/src/runtime/types.ts +93 -0
- package/src/runtime/workers/application.ts +209 -0
- package/src/runtime/workers/base.ts +68 -0
- package/src/runtime/workers/cli.ts +0 -0
- package/src/runtime/workers/job.ts +153 -0
- package/src/typings.ts +30 -0
- package/src/vite/builder.ts +122 -0
- package/src/vite/config.ts +45 -0
- package/src/vite/plugins.ts +26 -0
- package/src/vite/runners/worker.ts +57 -0
- package/src/vite/server.ts +39 -0
- package/src/vite/servers/main.ts +34 -0
- package/src/vite/servers/worker.ts +143 -0
- package/dist/_exports/application.js +0 -1
- package/dist/_exports/common.js +0 -1
- package/dist/_exports/contract.js +0 -2
- package/dist/_exports/core.js +0 -1
- package/dist/_exports/gateway.js +0 -1
- package/dist/_exports/http-transport/bun.js +0 -1
- package/dist/_exports/http-transport/deno.js +0 -1
- package/dist/_exports/http-transport/node.js +0 -1
- package/dist/_exports/http-transport.js +0 -1
- package/dist/_exports/json-format.js +0 -1
- package/dist/_exports/protocol/client.js +0 -1
- package/dist/_exports/protocol/server.js +0 -1
- package/dist/_exports/protocol.js +0 -1
- package/dist/_exports/runtime/types.js +0 -1
- package/dist/_exports/runtime.js +0 -1
- package/dist/_exports/type.js +0 -2
- package/dist/_exports/ws-transport/bun.js +0 -1
- package/dist/_exports/ws-transport/deno.js +0 -1
- package/dist/_exports/ws-transport/node.js +0 -1
- package/dist/_exports/ws-transport.js +0 -1
- 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
|
+
}
|