@nest-batch/core 0.2.0 → 0.2.1
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/README.md +5 -3
- package/dist/src/adapters/in-process.adapter.d.ts +16 -13
- package/dist/src/adapters/in-process.adapter.d.ts.map +1 -1
- package/dist/src/adapters/in-process.adapter.js +2 -0
- package/dist/src/adapters/in-process.adapter.js.map +1 -1
- package/dist/src/compiler/definition-compiler.d.ts.map +1 -1
- package/dist/src/compiler/definition-compiler.js +3 -0
- package/dist/src/compiler/definition-compiler.js.map +1 -1
- package/dist/src/core/ir/listener-definition.d.ts +2 -0
- package/dist/src/core/ir/listener-definition.d.ts.map +1 -1
- package/dist/src/core/item/interfaces.d.ts +14 -4
- package/dist/src/core/item/interfaces.d.ts.map +1 -1
- package/dist/src/decorators/listener.decorators.d.ts +4 -3
- package/dist/src/decorators/listener.decorators.d.ts.map +1 -1
- package/dist/src/decorators/listener.decorators.js +6 -3
- package/dist/src/decorators/listener.decorators.js.map +1 -1
- package/dist/src/execution/chunk-step-executor.d.ts +7 -1
- package/dist/src/execution/chunk-step-executor.d.ts.map +1 -1
- package/dist/src/execution/chunk-step-executor.js +104 -13
- package/dist/src/execution/chunk-step-executor.js.map +1 -1
- package/dist/src/execution/in-process-schedule.d.ts +25 -0
- package/dist/src/execution/in-process-schedule.d.ts.map +1 -0
- package/dist/src/execution/in-process-schedule.js +129 -0
- package/dist/src/execution/in-process-schedule.js.map +1 -0
- package/dist/src/execution/index.d.ts +1 -0
- package/dist/src/execution/index.d.ts.map +1 -1
- package/dist/src/execution/index.js +1 -0
- package/dist/src/execution/index.js.map +1 -1
- package/dist/src/execution/job-executor.d.ts.map +1 -1
- package/dist/src/execution/job-executor.js +14 -8
- package/dist/src/execution/job-executor.js.map +1 -1
- package/dist/src/execution/listener-invoker.d.ts +25 -9
- package/dist/src/execution/listener-invoker.d.ts.map +1 -1
- package/dist/src/execution/listener-invoker.js +70 -14
- package/dist/src/execution/listener-invoker.js.map +1 -1
- package/dist/src/execution/tasklet-step-executor.d.ts +4 -1
- package/dist/src/execution/tasklet-step-executor.d.ts.map +1 -1
- package/dist/src/execution/tasklet-step-executor.js +20 -16
- package/dist/src/execution/tasklet-step-executor.js.map +1 -1
- package/dist/src/explorer/batch-explorer.d.ts +2 -1
- package/dist/src/explorer/batch-explorer.d.ts.map +1 -1
- package/dist/src/explorer/batch-explorer.js +3 -0
- package/dist/src/explorer/batch-explorer.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/module/batch-schedule-registry.d.ts +13 -14
- package/dist/src/module/batch-schedule-registry.d.ts.map +1 -1
- package/dist/src/module/batch-schedule-registry.js +0 -0
- package/dist/src/module/batch-schedule-registry.js.map +1 -1
- package/dist/src/module/nest-batch.module.d.ts +4 -3
- package/dist/src/module/nest-batch.module.d.ts.map +1 -1
- package/dist/src/module/nest-batch.module.js +3 -2
- package/dist/src/module/nest-batch.module.js.map +1 -1
- package/dist/src/module/tokens.d.ts +5 -6
- package/dist/src/module/tokens.d.ts.map +1 -1
- package/dist/src/module/tokens.js.map +1 -1
- package/dist/src/partition-helpers.d.ts +3 -3
- package/dist/src/partition-helpers.d.ts.map +1 -1
- package/dist/src/partition-helpers.js +3 -3
- package/dist/src/partition-helpers.js.map +1 -1
- package/dist/src/scheduling/batch-scheduled.d.ts +9 -11
- package/dist/src/scheduling/batch-scheduled.d.ts.map +1 -1
- package/dist/src/scheduling/batch-scheduled.js +12 -20
- package/dist/src/scheduling/batch-scheduled.js.map +1 -1
- package/package.json +4 -1
- package/src/adapters/in-process.adapter.ts +18 -13
- package/src/compiler/definition-compiler.ts +12 -5
- package/src/core/ir/listener-definition.ts +2 -0
- package/src/core/item/interfaces.ts +15 -4
- package/src/decorators/listener.decorators.ts +11 -13
- package/src/execution/chunk-step-executor.ts +212 -18
- package/src/execution/in-process-schedule.ts +143 -0
- package/src/execution/index.ts +1 -0
- package/src/execution/job-executor.ts +30 -21
- package/src/execution/listener-invoker.ts +105 -27
- package/src/execution/tasklet-step-executor.ts +40 -16
- package/src/explorer/batch-explorer.ts +10 -4
- package/src/index.ts +1 -0
- package/src/module/batch-schedule-registry.ts +0 -0
- package/src/module/nest-batch.module.ts +21 -42
- package/src/module/tokens.ts +8 -15
- package/src/partition-helpers.ts +13 -17
- package/src/scheduling/batch-scheduled.ts +22 -32
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Injectable, Logger, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
|
|
2
|
+
import { CronJob } from 'cron';
|
|
3
|
+
|
|
4
|
+
import { type BatchScheduleEntry, BatchScheduleRegistry } from '../module/batch-schedule-registry';
|
|
5
|
+
import { JobLauncher } from './job-launcher';
|
|
6
|
+
|
|
7
|
+
interface ScheduleState {
|
|
8
|
+
readonly entry: BatchScheduleEntry;
|
|
9
|
+
readonly job: CronJob;
|
|
10
|
+
runningCount: number;
|
|
11
|
+
queued: boolean;
|
|
12
|
+
queuedAt: Date | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function scheduleKey(entry: BatchScheduleEntry): string {
|
|
16
|
+
return `${entry.jobId}::${entry.scheduleName}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* In-process scheduler for `@BatchScheduled` jobs.
|
|
21
|
+
*
|
|
22
|
+
* This is deliberately part of the in-process transport, not the
|
|
23
|
+
* `@BatchScheduled` decorator. The decorator remains metadata-only;
|
|
24
|
+
* this provider consumes the `BatchScheduleRegistry` and turns matching
|
|
25
|
+
* cron ticks into `JobLauncher.launch(...)` calls inside the same server
|
|
26
|
+
* process.
|
|
27
|
+
*/
|
|
28
|
+
@Injectable()
|
|
29
|
+
export class InProcessSchedule implements OnApplicationBootstrap, OnApplicationShutdown {
|
|
30
|
+
private readonly logger = new Logger(InProcessSchedule.name);
|
|
31
|
+
private readonly states = new Map<string, ScheduleState>();
|
|
32
|
+
private stopped = true;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly scheduleRegistry: BatchScheduleRegistry,
|
|
36
|
+
private readonly launcher: JobLauncher,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
onApplicationBootstrap(): void {
|
|
40
|
+
this.stopped = false;
|
|
41
|
+
for (const entry of this.scheduleRegistry.getAll()) {
|
|
42
|
+
if (entry.inert) {
|
|
43
|
+
this.logger.log(`Skipping inert schedule: ${entry.jobId}::${entry.scheduleName}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
let state!: ScheduleState;
|
|
49
|
+
const job = CronJob.from({
|
|
50
|
+
cronTime: entry.cron,
|
|
51
|
+
timeZone: entry.timezone,
|
|
52
|
+
start: false,
|
|
53
|
+
unrefTimeout: true,
|
|
54
|
+
name: scheduleKey(entry),
|
|
55
|
+
errorHandler: (err) => {
|
|
56
|
+
this.logger.warn(
|
|
57
|
+
`In-process schedule ${scheduleKey(entry)} callback failed: ${
|
|
58
|
+
err instanceof Error ? err.message : String(err)
|
|
59
|
+
}`,
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
onTick: () => {
|
|
63
|
+
if (this.stopped) return;
|
|
64
|
+
this.dispatch(state, state.job.lastDate() ?? new Date());
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
state = {
|
|
68
|
+
entry,
|
|
69
|
+
job,
|
|
70
|
+
runningCount: 0,
|
|
71
|
+
queued: false,
|
|
72
|
+
queuedAt: null,
|
|
73
|
+
};
|
|
74
|
+
this.states.set(scheduleKey(entry), state);
|
|
75
|
+
job.start();
|
|
76
|
+
} catch (err) {
|
|
77
|
+
this.logger.warn(
|
|
78
|
+
`Failed to install in-process schedule ${entry.jobId}::${entry.scheduleName}: ${
|
|
79
|
+
err instanceof Error ? err.message : String(err)
|
|
80
|
+
}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.states.size === 0) return;
|
|
86
|
+
|
|
87
|
+
this.logger.log(`InProcessSchedule started: schedules=${this.states.size}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onApplicationShutdown(): void {
|
|
91
|
+
this.stopped = true;
|
|
92
|
+
for (const state of this.states.values()) {
|
|
93
|
+
void state.job.stop();
|
|
94
|
+
}
|
|
95
|
+
this.states.clear();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private dispatch(state: ScheduleState, scheduledAt: Date): void {
|
|
99
|
+
if (this.stopped) return;
|
|
100
|
+
const overlap = state.entry.overlap ?? 'skip';
|
|
101
|
+
if (state.runningCount > 0) {
|
|
102
|
+
if (overlap === 'skip') return;
|
|
103
|
+
if (overlap === 'queue') {
|
|
104
|
+
state.queued = true;
|
|
105
|
+
state.queuedAt ??= scheduledAt;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
state.runningCount += 1;
|
|
111
|
+
void this.launch(state, scheduledAt).finally(() => {
|
|
112
|
+
state.runningCount -= 1;
|
|
113
|
+
if (this.stopped) return;
|
|
114
|
+
if (state.runningCount === 0 && state.queued) {
|
|
115
|
+
state.queued = false;
|
|
116
|
+
const queuedAt = state.queuedAt ?? new Date();
|
|
117
|
+
state.queuedAt = null;
|
|
118
|
+
this.dispatch(state, queuedAt);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async launch(state: ScheduleState, scheduledAt: Date): Promise<void> {
|
|
124
|
+
const { entry } = state;
|
|
125
|
+
try {
|
|
126
|
+
const execution = await this.launcher.launch(entry.jobId, {
|
|
127
|
+
scheduled: true,
|
|
128
|
+
scheduleName: entry.scheduleName,
|
|
129
|
+
scheduledAt: scheduledAt.toISOString(),
|
|
130
|
+
});
|
|
131
|
+
this.logger.log(
|
|
132
|
+
`Fired schedule ${entry.jobId}::${entry.scheduleName} -> ` +
|
|
133
|
+
`execution=${execution.id} status=${execution.status}`,
|
|
134
|
+
);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.logger.warn(
|
|
137
|
+
`Failed to fire schedule ${entry.jobId}::${entry.scheduleName}: ${
|
|
138
|
+
err instanceof Error ? err.message : String(err)
|
|
139
|
+
}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/execution/index.ts
CHANGED
|
@@ -7,11 +7,7 @@ import { StepStatus, JobStatus, FlowExecutionStatus } from '../core/status';
|
|
|
7
7
|
import { JobNotRestartableError } from '../core/errors';
|
|
8
8
|
import { TaskletStepExecutor, type StepExecutionResult } from './tasklet-step-executor';
|
|
9
9
|
import { ChunkStepExecutor, type ChunkExecutionResult } from './chunk-step-executor';
|
|
10
|
-
import {
|
|
11
|
-
ListenerInvoker,
|
|
12
|
-
type ResolverMap,
|
|
13
|
-
type ListenerResolver,
|
|
14
|
-
} from './listener-invoker';
|
|
10
|
+
import { ListenerInvoker, type ResolverMap, type ListenerResolver } from './listener-invoker';
|
|
15
11
|
import { FlowEvaluator } from '../flow/flow-evaluator';
|
|
16
12
|
import {
|
|
17
13
|
BATCH_EVENT,
|
|
@@ -137,6 +133,7 @@ export class JobExecutor {
|
|
|
137
133
|
await this.listenerInvoker.invokeBefore(jobResolvers, 'job', {
|
|
138
134
|
jobExecutionId: execution.id,
|
|
139
135
|
stepExecutionId: '<job>',
|
|
136
|
+
jobParameters: execution.params,
|
|
140
137
|
});
|
|
141
138
|
|
|
142
139
|
// Cache the step order once. `Object.keys` returns insertion order
|
|
@@ -196,6 +193,9 @@ export class JobExecutor {
|
|
|
196
193
|
if (step.kind === 'tasklet') {
|
|
197
194
|
result = await this.taskletExecutor.execute(step, {
|
|
198
195
|
jobExecutionId: execution.id,
|
|
196
|
+
stepExecutionId: stepExecution.id,
|
|
197
|
+
stepName: step.id,
|
|
198
|
+
jobParameters: execution.params,
|
|
199
199
|
jobRepository: this.repository,
|
|
200
200
|
transactionManager: this.transactionManager,
|
|
201
201
|
listenerInvoker: this.listenerInvoker,
|
|
@@ -217,9 +217,12 @@ export class JobExecutor {
|
|
|
217
217
|
result = await this.chunkExecutor.execute(step, {
|
|
218
218
|
jobExecutionId: execution.id,
|
|
219
219
|
stepExecutionId: stepExecution.id,
|
|
220
|
+
stepName: step.id,
|
|
221
|
+
jobParameters: execution.params,
|
|
220
222
|
jobRepository: this.repository,
|
|
221
223
|
transactionManager: this.transactionManager,
|
|
222
224
|
listenerInvoker: this.listenerInvoker,
|
|
225
|
+
listenerResolvers: jobResolvers,
|
|
223
226
|
jobExecutionId2: execution.id,
|
|
224
227
|
resolvers: new Map(),
|
|
225
228
|
...(resumeFromChunkIndex !== undefined ? { resumeFromChunkIndex } : {}),
|
|
@@ -281,18 +284,14 @@ export class JobExecutor {
|
|
|
281
284
|
: result.status === StepStatus.FAILED
|
|
282
285
|
? FlowExecutionStatus.FAILED
|
|
283
286
|
: FlowExecutionStatus.UNKNOWN;
|
|
284
|
-
const deciderExitStatus = await this.resolveDeciderExitStatus(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
exitCode: result.exitCode,
|
|
293
|
-
exitMessage: result.exitMessage,
|
|
294
|
-
},
|
|
295
|
-
);
|
|
287
|
+
const deciderExitStatus = await this.resolveDeciderExitStatus(jobDef, currentStepId, {
|
|
288
|
+
jobExecution: execution,
|
|
289
|
+
stepId: step.id,
|
|
290
|
+
stepExecutionId: stepExecution.id,
|
|
291
|
+
stepStatus: result.status,
|
|
292
|
+
exitCode: result.exitCode,
|
|
293
|
+
exitMessage: result.exitMessage,
|
|
294
|
+
});
|
|
296
295
|
const flowExitStatus = deciderExitStatus ?? (result.exitCode || flowStatus);
|
|
297
296
|
|
|
298
297
|
let evaluatorResult = await this.flowEvaluator.evaluate(
|
|
@@ -373,8 +372,12 @@ export class JobExecutor {
|
|
|
373
372
|
await this.listenerInvoker.invokeAfter(
|
|
374
373
|
jobResolvers,
|
|
375
374
|
'job',
|
|
376
|
-
{
|
|
377
|
-
|
|
375
|
+
{
|
|
376
|
+
jobExecutionId: execution.id,
|
|
377
|
+
stepExecutionId: '<job>',
|
|
378
|
+
jobParameters: execution.params,
|
|
379
|
+
},
|
|
380
|
+
{ status: finalStatus },
|
|
378
381
|
);
|
|
379
382
|
|
|
380
383
|
await this.emit({
|
|
@@ -423,7 +426,10 @@ export class JobExecutor {
|
|
|
423
426
|
const name = this.resolveListenerName(def.ref, lambdaCounter);
|
|
424
427
|
if (def.ref.kind === RefKind.BuilderLambda) lambdaCounter += 1;
|
|
425
428
|
|
|
426
|
-
const key =
|
|
429
|
+
const key =
|
|
430
|
+
def.kind === 'skip' && def.skipKind !== undefined
|
|
431
|
+
? `on-skip:${def.skipKind}:${name}`
|
|
432
|
+
: `${def.phase}:${def.kind}:${name}`;
|
|
427
433
|
resolvers.set(key, {
|
|
428
434
|
fn,
|
|
429
435
|
...(def.nonCritical !== undefined ? { nonCritical: def.nonCritical } : {}),
|
|
@@ -497,7 +503,10 @@ export class JobExecutor {
|
|
|
497
503
|
} else if (key.startsWith('after:step:')) {
|
|
498
504
|
legacy.set(`after-step:${key.slice('after:step:'.length)}`, entry.fn as ListenerResolver);
|
|
499
505
|
} else if (key.startsWith('on-error:step:')) {
|
|
500
|
-
legacy.set(
|
|
506
|
+
legacy.set(
|
|
507
|
+
`on-step-error:${key.slice('on-error:step:'.length)}`,
|
|
508
|
+
entry.fn as ListenerResolver,
|
|
509
|
+
);
|
|
501
510
|
}
|
|
502
511
|
}
|
|
503
512
|
return legacy;
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* Registration order is preserved (Map iteration is insertion-ordered in JS).
|
|
27
27
|
*/
|
|
28
28
|
import { Injectable, Logger } from '@nestjs/common';
|
|
29
|
-
import type { ExecutionContext } from '../core/repository';
|
|
29
|
+
import type { ExecutionContext, JobParameters } from '../core/repository';
|
|
30
|
+
import type { SkipSubKind } from '../core/ir/listener-definition';
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Public types
|
|
@@ -122,8 +123,6 @@ export type OnErrorKind = 'job' | 'step' | 'chunk';
|
|
|
122
123
|
* Sub-kinds for the `on-skip` phase. The resolver key looks like
|
|
123
124
|
* `on-skip:${SkipSubKind}:${name}` — for example `on-skip:read:MySkipListener`.
|
|
124
125
|
*/
|
|
125
|
-
export type SkipSubKind = 'read' | 'process' | 'write';
|
|
126
|
-
|
|
127
126
|
// ---------------------------------------------------------------------------
|
|
128
127
|
// Implementation
|
|
129
128
|
// ---------------------------------------------------------------------------
|
|
@@ -140,11 +139,10 @@ export class ListenerInvoker {
|
|
|
140
139
|
* Invoke every `before:<kind>:<name>` resolver, in registration order.
|
|
141
140
|
*
|
|
142
141
|
* Listener signature depends on `<kind>`:
|
|
143
|
-
* - `job` / `chunk`
|
|
144
|
-
* - `
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* convention)
|
|
142
|
+
* - `job` / `step` / `chunk` — `fn(ctx, result?)`
|
|
143
|
+
* - `item-read` / `item-process` / `item-write` — `fn(item, ctx)` for the
|
|
144
|
+
* legacy generic path. Prefer the explicit item helpers below for exact
|
|
145
|
+
* read/process/write signatures.
|
|
148
146
|
*/
|
|
149
147
|
async invokeBefore(
|
|
150
148
|
resolvers: ResolverMap,
|
|
@@ -183,30 +181,115 @@ export class ListenerInvoker {
|
|
|
183
181
|
await this.invokeMatching(resolvers, `${LISTENER_PHASE.OnError}:${kind}:`, [ctx, err]);
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
/** Invoke every `
|
|
187
|
-
async
|
|
184
|
+
/** Invoke every `before:item-read:<name>` resolver. Listener signature: `fn(ctx)`. */
|
|
185
|
+
async invokeBeforeRead(resolvers: ResolverMap, ctx: ListenerContext): Promise<void> {
|
|
186
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.Before}:item-read:`, [ctx]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Invoke every `after:item-read:<name>` resolver. Listener signature: `fn(item, ctx)`. */
|
|
190
|
+
async invokeAfterRead(
|
|
191
|
+
resolvers: ResolverMap,
|
|
192
|
+
item: unknown,
|
|
193
|
+
ctx: ListenerContext,
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.After}:item-read:`, [item, ctx]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Invoke every `on-error:item-read:<name>` resolver. Listener signature: `fn(err, ctx)`. */
|
|
199
|
+
async invokeOnReadError(
|
|
188
200
|
resolvers: ResolverMap,
|
|
189
201
|
err: unknown,
|
|
202
|
+
ctx: ListenerContext,
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.OnError}:item-read:`, [err, ctx]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Invoke every `before:item-process:<name>` resolver. Listener signature: `fn(item, ctx)`. */
|
|
208
|
+
async invokeBeforeProcess(
|
|
209
|
+
resolvers: ResolverMap,
|
|
190
210
|
item: unknown,
|
|
211
|
+
ctx: ListenerContext,
|
|
191
212
|
): Promise<void> {
|
|
192
|
-
await this.invokeMatching(resolvers, `${LISTENER_PHASE.
|
|
213
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.Before}:item-process:`, [item, ctx]);
|
|
193
214
|
}
|
|
194
215
|
|
|
195
|
-
/** Invoke every `
|
|
196
|
-
async
|
|
216
|
+
/** Invoke every `after:item-process:<name>` resolver. Listener signature: `fn(item, result, ctx)`. */
|
|
217
|
+
async invokeAfterProcess(
|
|
218
|
+
resolvers: ResolverMap,
|
|
219
|
+
item: unknown,
|
|
220
|
+
result: unknown,
|
|
221
|
+
ctx: ListenerContext,
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.After}:item-process:`, [
|
|
224
|
+
item,
|
|
225
|
+
result,
|
|
226
|
+
ctx,
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Invoke every `on-error:item-process:<name>` resolver. Listener signature: `fn(item, err, ctx)`. */
|
|
231
|
+
async invokeOnProcessError(
|
|
197
232
|
resolvers: ResolverMap,
|
|
198
233
|
item: unknown,
|
|
199
234
|
err: unknown,
|
|
235
|
+
ctx: ListenerContext,
|
|
200
236
|
): Promise<void> {
|
|
201
|
-
await this.invokeMatching(resolvers, `${LISTENER_PHASE.
|
|
237
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.OnError}:item-process:`, [
|
|
238
|
+
item,
|
|
239
|
+
err,
|
|
240
|
+
ctx,
|
|
241
|
+
]);
|
|
202
242
|
}
|
|
203
243
|
|
|
204
|
-
/** Invoke every `
|
|
205
|
-
async
|
|
244
|
+
/** Invoke every `before:item-write:<name>` resolver. Listener signature: `fn(items, ctx)`. */
|
|
245
|
+
async invokeBeforeWrite(
|
|
246
|
+
resolvers: ResolverMap,
|
|
247
|
+
items: unknown[],
|
|
248
|
+
ctx: ListenerContext,
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.Before}:item-write:`, [items, ctx]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Invoke every `after:item-write:<name>` resolver. Listener signature: `fn(items, result, ctx)`. */
|
|
254
|
+
async invokeAfterWrite(
|
|
255
|
+
resolvers: ResolverMap,
|
|
256
|
+
items: unknown[],
|
|
257
|
+
result: unknown,
|
|
258
|
+
ctx: ListenerContext,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.After}:item-write:`, [
|
|
261
|
+
items,
|
|
262
|
+
result,
|
|
263
|
+
ctx,
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Invoke every `on-error:item-write:<name>` resolver. Listener signature: `fn(items, err, ctx)`. */
|
|
268
|
+
async invokeOnWriteError(
|
|
206
269
|
resolvers: ResolverMap,
|
|
207
270
|
items: unknown[],
|
|
208
271
|
err: unknown,
|
|
272
|
+
ctx: ListenerContext,
|
|
209
273
|
): Promise<void> {
|
|
274
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.OnError}:item-write:`, [
|
|
275
|
+
items,
|
|
276
|
+
err,
|
|
277
|
+
ctx,
|
|
278
|
+
]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Invoke every `on-skip:read:<name>` resolver. Listener signature: `fn(err, item)`. */
|
|
282
|
+
async invokeOnSkipRead(resolvers: ResolverMap, err: unknown, item: unknown): Promise<void> {
|
|
283
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.OnSkip}:read:`, [err, item]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Invoke every `on-skip:process:<name>` resolver. Listener signature: `fn(item, err)`. */
|
|
287
|
+
async invokeOnSkipProcess(resolvers: ResolverMap, item: unknown, err: unknown): Promise<void> {
|
|
288
|
+
await this.invokeMatching(resolvers, `${LISTENER_PHASE.OnSkip}:process:`, [item, err]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Invoke every `on-skip:write:<name>` resolver. Listener signature: `fn(items, err)`. */
|
|
292
|
+
async invokeOnSkipWrite(resolvers: ResolverMap, items: unknown[], err: unknown): Promise<void> {
|
|
210
293
|
await this.invokeMatching(resolvers, `${LISTENER_PHASE.OnSkip}:write:`, [items, err]);
|
|
211
294
|
}
|
|
212
295
|
|
|
@@ -309,27 +392,21 @@ export class ListenerInvoker {
|
|
|
309
392
|
* Compute the positional argument list to forward to a before/after
|
|
310
393
|
* listener, based on the listener's kind.
|
|
311
394
|
*
|
|
312
|
-
* - `job` / `chunk`
|
|
313
|
-
* - `step` → `[ctx, args]` (args is the result)
|
|
395
|
+
* - `job` / `step` / `chunk` → `[ctx]` or `[ctx, args]`
|
|
314
396
|
* - `item-read` /
|
|
315
397
|
* `item-process` /
|
|
316
398
|
* `item-write` → `[args, ctx]` (args is the item, leading position)
|
|
317
399
|
*/
|
|
318
|
-
private buildCallArgs(
|
|
319
|
-
kind: LifecyclePhaseKind,
|
|
320
|
-
ctx: ListenerContext,
|
|
321
|
-
args: unknown,
|
|
322
|
-
): unknown[] {
|
|
400
|
+
private buildCallArgs(kind: LifecyclePhaseKind, ctx: ListenerContext, args: unknown): unknown[] {
|
|
323
401
|
switch (kind) {
|
|
324
402
|
case 'item-read':
|
|
325
403
|
case 'item-process':
|
|
326
404
|
case 'item-write':
|
|
327
|
-
return [args, ctx];
|
|
328
|
-
case 'step':
|
|
329
|
-
return [ctx, args];
|
|
405
|
+
return args === undefined ? [ctx] : [args, ctx];
|
|
330
406
|
case 'job':
|
|
407
|
+
case 'step':
|
|
331
408
|
case 'chunk':
|
|
332
|
-
return [ctx];
|
|
409
|
+
return args === undefined ? [ctx] : [ctx, args];
|
|
333
410
|
default: {
|
|
334
411
|
// exhaustive guard
|
|
335
412
|
const _exhaustive: never = kind;
|
|
@@ -380,6 +457,7 @@ export interface ListenerContext {
|
|
|
380
457
|
jobExecutionId: string;
|
|
381
458
|
stepExecutionId?: string;
|
|
382
459
|
stepName?: string;
|
|
460
|
+
jobParameters?: JobParameters;
|
|
383
461
|
/** Arbitrary, executor-supplied metadata (transaction context, etc.). */
|
|
384
462
|
[extra: string]: unknown;
|
|
385
463
|
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ExecutionContext,
|
|
8
8
|
StepExecution,
|
|
9
9
|
ExecutionScope,
|
|
10
|
+
JobParameters,
|
|
10
11
|
} from '../core/repository';
|
|
11
12
|
import type { TransactionManager } from '../core/transaction';
|
|
12
13
|
import { StepStatus, JobStatus } from '../core/status';
|
|
@@ -19,6 +20,9 @@ import { resolveProviderToken, type ProviderResolvers } from './ref-resolver';
|
|
|
19
20
|
*/
|
|
20
21
|
export interface TaskletExecutionContext {
|
|
21
22
|
jobExecutionId: string;
|
|
23
|
+
stepExecutionId?: string;
|
|
24
|
+
stepName?: string;
|
|
25
|
+
jobParameters?: JobParameters;
|
|
22
26
|
jobRepository: JobRepository;
|
|
23
27
|
transactionManager: TransactionManager;
|
|
24
28
|
listenerInvoker: ListenerInvoker;
|
|
@@ -76,26 +80,26 @@ export class TaskletStepExecutor {
|
|
|
76
80
|
): Promise<StepExecutionResult> {
|
|
77
81
|
// Build the TaskletContext the tasklet will see.
|
|
78
82
|
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
// `getExecutionContext` / `saveExecutionContext` are also stubbed here;
|
|
84
|
-
// they become real once the JobExecutor wires the stepExecutionId in.
|
|
85
|
-
// (For Wave 3 tests we do not exercise these methods.)
|
|
83
|
+
const stepExecutionId = context.stepExecutionId ?? '<pending>';
|
|
84
|
+
const stepName = context.stepName ?? step.id;
|
|
85
|
+
const jobParameters = context.jobParameters ?? {};
|
|
86
86
|
const taskletCtx: TaskletContext = {
|
|
87
87
|
jobExecutionId: context.jobExecutionId,
|
|
88
|
-
stepExecutionId
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
stepExecutionId,
|
|
89
|
+
jobParameters,
|
|
90
|
+
getExecutionContext: async () =>
|
|
91
|
+
context.jobRepository.getExecutionContext({ stepExecutionId }),
|
|
92
|
+
saveExecutionContext: async (ctx: ExecutionContext) => {
|
|
93
|
+
await context.jobRepository.saveExecutionContext({ stepExecutionId }, ctx);
|
|
92
94
|
},
|
|
93
95
|
};
|
|
94
96
|
|
|
95
97
|
// 1. before-step listeners
|
|
96
98
|
await context.listenerInvoker.invokeBeforeStep(context.listenerResolvers, {
|
|
97
99
|
jobExecutionId: context.jobExecutionId,
|
|
98
|
-
stepExecutionId
|
|
100
|
+
stepExecutionId,
|
|
101
|
+
stepName,
|
|
102
|
+
jobParameters,
|
|
99
103
|
});
|
|
100
104
|
|
|
101
105
|
let result: StepExecutionResult;
|
|
@@ -117,7 +121,12 @@ export class TaskletStepExecutor {
|
|
|
117
121
|
// 3. on-step-error listeners (best-effort: rethrow their failures too)
|
|
118
122
|
await context.listenerInvoker.invokeOnErrorStep(
|
|
119
123
|
context.listenerResolvers,
|
|
120
|
-
{
|
|
124
|
+
{
|
|
125
|
+
jobExecutionId: context.jobExecutionId,
|
|
126
|
+
stepExecutionId,
|
|
127
|
+
stepName,
|
|
128
|
+
jobParameters,
|
|
129
|
+
},
|
|
121
130
|
err,
|
|
122
131
|
);
|
|
123
132
|
result = {
|
|
@@ -136,7 +145,12 @@ export class TaskletStepExecutor {
|
|
|
136
145
|
// different branch — transition evaluation runs AFTER this call.
|
|
137
146
|
await context.listenerInvoker.invokeAfterStep(
|
|
138
147
|
context.listenerResolvers,
|
|
139
|
-
{
|
|
148
|
+
{
|
|
149
|
+
jobExecutionId: context.jobExecutionId,
|
|
150
|
+
stepExecutionId,
|
|
151
|
+
stepName,
|
|
152
|
+
jobParameters,
|
|
153
|
+
},
|
|
140
154
|
result,
|
|
141
155
|
);
|
|
142
156
|
|
|
@@ -156,7 +170,13 @@ export class TaskletStepExecutor {
|
|
|
156
170
|
* `builder-lambda` ref before reaching this executor.
|
|
157
171
|
*/
|
|
158
172
|
private resolveTasklet(
|
|
159
|
-
taskletRef: {
|
|
173
|
+
taskletRef: {
|
|
174
|
+
kind: string;
|
|
175
|
+
token?: string;
|
|
176
|
+
fn?: ListenerResolver;
|
|
177
|
+
classToken?: string;
|
|
178
|
+
methodName?: string;
|
|
179
|
+
},
|
|
160
180
|
context: TaskletExecutionContext,
|
|
161
181
|
): Tasklet {
|
|
162
182
|
if (taskletRef.kind === RefKind.BuilderLambda && taskletRef.fn) {
|
|
@@ -164,7 +184,11 @@ export class TaskletStepExecutor {
|
|
|
164
184
|
if (typeof result === 'function') {
|
|
165
185
|
return { execute: result as Tasklet['execute'] };
|
|
166
186
|
}
|
|
167
|
-
if (
|
|
187
|
+
if (
|
|
188
|
+
result !== null &&
|
|
189
|
+
typeof result === 'object' &&
|
|
190
|
+
typeof (result as Tasklet).execute === 'function'
|
|
191
|
+
) {
|
|
168
192
|
return result as Tasklet;
|
|
169
193
|
}
|
|
170
194
|
return { execute: taskletRef.fn as Tasklet['execute'] };
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
BATCH_TRANSITION_METADATA,
|
|
11
11
|
} from '../decorators/constants';
|
|
12
12
|
import type { JobableOptions, StepableOptions } from '../decorators';
|
|
13
|
-
import type { ListenerKind, ListenerPhase } from '../core/ir/listener-definition';
|
|
13
|
+
import type { ListenerKind, ListenerPhase, SkipSubKind } from '../core/ir/listener-definition';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Raw shape of a discovered batch job, as it appears immediately after the
|
|
@@ -50,6 +50,7 @@ export interface DiscoveredListener {
|
|
|
50
50
|
methodName: string;
|
|
51
51
|
kind: ListenerKind;
|
|
52
52
|
phase: ListenerPhase;
|
|
53
|
+
skipKind?: SkipSubKind;
|
|
53
54
|
nonCritical?: boolean;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -164,8 +165,7 @@ export class BatchExplorer implements OnModuleInit {
|
|
|
164
165
|
| StepableOptions
|
|
165
166
|
| undefined;
|
|
166
167
|
if (!opts) continue;
|
|
167
|
-
const isTasklet =
|
|
168
|
-
Reflect.getMetadata(BATCH_TASKLET_METADATA, prototype, name) === true;
|
|
168
|
+
const isTasklet = Reflect.getMetadata(BATCH_TASKLET_METADATA, prototype, name) === true;
|
|
169
169
|
result.push({ methodName: name, options: opts, isTasklet });
|
|
170
170
|
}
|
|
171
171
|
return result;
|
|
@@ -186,13 +186,19 @@ export class BatchExplorer implements OnModuleInit {
|
|
|
186
186
|
const result: DiscoveredListener[] = [];
|
|
187
187
|
for (const name of this.allMethodNames(prototype)) {
|
|
188
188
|
const opts = Reflect.getMetadata(BATCH_LISTENER_METADATA, prototype, name) as
|
|
189
|
-
| {
|
|
189
|
+
| {
|
|
190
|
+
kind: ListenerKind;
|
|
191
|
+
phase: ListenerPhase;
|
|
192
|
+
skipKind?: SkipSubKind;
|
|
193
|
+
nonCritical?: boolean;
|
|
194
|
+
}
|
|
190
195
|
| undefined;
|
|
191
196
|
if (!opts) continue;
|
|
192
197
|
result.push({
|
|
193
198
|
methodName: name,
|
|
194
199
|
kind: opts.kind,
|
|
195
200
|
phase: opts.phase,
|
|
201
|
+
...(opts.skipKind !== undefined ? { skipKind: opts.skipKind } : {}),
|
|
196
202
|
nonCritical: opts.nonCritical,
|
|
197
203
|
});
|
|
198
204
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './transaction';
|
|
|
14
14
|
export * from './repository';
|
|
15
15
|
export * as BatchDecorators from './decorators';
|
|
16
16
|
export * from './module';
|
|
17
|
+
export * from './scheduling/batch-scheduled';
|
|
17
18
|
export * from './builder';
|
|
18
19
|
export * from './explorer';
|
|
19
20
|
export * from './listeners';
|
|
Binary file
|