@nest-batch/core 0.2.0 → 0.2.2
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 +7 -5
- 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
|
@@ -86,6 +86,7 @@ export class DefinitionCompiler {
|
|
|
86
86
|
const listenerDefs: ListenerDefinition[] = discovered.listenerMethods.map((l) => ({
|
|
87
87
|
kind: l.kind,
|
|
88
88
|
phase: l.phase,
|
|
89
|
+
...(l.skipKind !== undefined ? { skipKind: l.skipKind } : {}),
|
|
89
90
|
nonCritical: l.nonCritical,
|
|
90
91
|
ref: this.buildListenerRef(discovered, classToken, l.methodName),
|
|
91
92
|
}));
|
|
@@ -252,7 +253,11 @@ export class DefinitionCompiler {
|
|
|
252
253
|
throw new InvalidFlowGraphError(
|
|
253
254
|
'INVALID_PARTITIONS',
|
|
254
255
|
`Step "${step.options.id}" has invalid partitions: ${err.message}`,
|
|
255
|
-
{
|
|
256
|
+
{
|
|
257
|
+
jobId: discovered.jobOptions.id,
|
|
258
|
+
stepId: step.options.id,
|
|
259
|
+
partitions: step.options.partitions,
|
|
260
|
+
},
|
|
256
261
|
);
|
|
257
262
|
}
|
|
258
263
|
throw err;
|
|
@@ -267,12 +272,14 @@ export class DefinitionCompiler {
|
|
|
267
272
|
skipPolicy: step.options.skipPolicy,
|
|
268
273
|
retryPolicy: step.options.retryPolicy,
|
|
269
274
|
listeners: [],
|
|
270
|
-
...(step.options.partitions !== undefined
|
|
271
|
-
? { partitions: step.options.partitions }
|
|
272
|
-
: {}),
|
|
275
|
+
...(step.options.partitions !== undefined ? { partitions: step.options.partitions } : {}),
|
|
273
276
|
...(processor
|
|
274
277
|
? {
|
|
275
|
-
processor: this.buildItemMethodRef(
|
|
278
|
+
processor: this.buildItemMethodRef(
|
|
279
|
+
discovered,
|
|
280
|
+
classToken,
|
|
281
|
+
processor,
|
|
282
|
+
) satisfies ProcessorRef,
|
|
276
283
|
}
|
|
277
284
|
: {}),
|
|
278
285
|
};
|
|
@@ -10,10 +10,12 @@ export type ListenerKind =
|
|
|
10
10
|
| 'skip'
|
|
11
11
|
| 'transition';
|
|
12
12
|
export type ListenerPhase = 'before' | 'after' | 'on-error';
|
|
13
|
+
export type SkipSubKind = 'read' | 'process' | 'write';
|
|
13
14
|
|
|
14
15
|
export interface ListenerDefinition {
|
|
15
16
|
kind: ListenerKind;
|
|
16
17
|
ref: ListenerRef;
|
|
17
18
|
phase: ListenerPhase;
|
|
19
|
+
skipKind?: SkipSubKind;
|
|
18
20
|
nonCritical?: boolean;
|
|
19
21
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecutionContext, ExecutionScope } from '../repository/types';
|
|
1
|
+
import type { ExecutionContext, ExecutionScope, JobParameters } from '../repository/types';
|
|
2
2
|
|
|
3
3
|
type MaybePromise<T> = T | Promise<T>;
|
|
4
4
|
|
|
@@ -6,8 +6,18 @@ type MaybePromise<T> = T | Promise<T>;
|
|
|
6
6
|
* Reads one item at a time. Returns `null` to signal EOF.
|
|
7
7
|
* For async iteration, use `AsyncIterable.read()` (not yet supported in MVP).
|
|
8
8
|
*/
|
|
9
|
+
export interface ItemExecutionContext {
|
|
10
|
+
readonly jobExecutionId: string;
|
|
11
|
+
readonly stepExecutionId: string;
|
|
12
|
+
readonly stepName: string;
|
|
13
|
+
readonly jobParameters: JobParameters;
|
|
14
|
+
readonly chunkIndex?: number;
|
|
15
|
+
getExecutionContext(): Promise<ExecutionContext>;
|
|
16
|
+
saveExecutionContext(ctx: ExecutionContext): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export interface ItemReader<T = unknown> {
|
|
10
|
-
read(): Promise<T | null>;
|
|
20
|
+
read(ctx?: ItemExecutionContext): Promise<T | null>;
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
/**
|
|
@@ -15,7 +25,7 @@ export interface ItemReader<T = unknown> {
|
|
|
15
25
|
* Throws to indicate the item should be skipped (via SkipPolicy) or retried (via RetryPolicy).
|
|
16
26
|
*/
|
|
17
27
|
export interface ItemProcessor<I = unknown, O = unknown> {
|
|
18
|
-
process(item: I): Promise<O | null | undefined>;
|
|
28
|
+
process(item: I, ctx?: ItemExecutionContext): Promise<O | null | undefined>;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
/**
|
|
@@ -28,7 +38,7 @@ export interface ItemProcessor<I = unknown, O = unknown> {
|
|
|
28
38
|
* count and assumes no per-item skips.
|
|
29
39
|
*/
|
|
30
40
|
export interface ItemWriter<T = unknown> {
|
|
31
|
-
write(items: T[]): Promise<WriterResult | void>;
|
|
41
|
+
write(items: T[], ctx?: ItemExecutionContext): Promise<WriterResult | void>;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
export interface WriterResult {
|
|
@@ -57,6 +67,7 @@ export interface ItemStream {
|
|
|
57
67
|
export interface TaskletContext {
|
|
58
68
|
readonly jobExecutionId: string;
|
|
59
69
|
readonly stepExecutionId: string;
|
|
70
|
+
readonly jobParameters: JobParameters;
|
|
60
71
|
getExecutionContext(): Promise<ExecutionContext>;
|
|
61
72
|
saveExecutionContext(ctx: ExecutionContext): Promise<void>;
|
|
62
73
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { BATCH_LISTENER_METADATA } from './constants';
|
|
3
|
-
import type { ListenerKind, ListenerPhase } from '../core/ir/listener-definition';
|
|
3
|
+
import type { ListenerKind, ListenerPhase, SkipSubKind } from '../core/ir/listener-definition';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Stored under `BATCH_LISTENER_METADATA` for each listener method.
|
|
@@ -9,12 +9,13 @@ import type { ListenerKind, ListenerPhase } from '../core/ir/listener-definition
|
|
|
9
9
|
*
|
|
10
10
|
* The `skip` kind is special: it has no before/after/on-error phase,
|
|
11
11
|
* it is a single fire-and-forget callback per skip event. We record
|
|
12
|
-
* `phase: 'after'` as a placeholder so the metadata shape stays uniform
|
|
13
|
-
*
|
|
12
|
+
* `phase: 'after'` as a placeholder so the metadata shape stays uniform,
|
|
13
|
+
* plus `skipKind` so read/process/write skip events remain distinguishable.
|
|
14
14
|
*/
|
|
15
15
|
export interface ListenerOptions {
|
|
16
16
|
kind: ListenerKind;
|
|
17
17
|
phase: ListenerPhase;
|
|
18
|
+
skipKind?: SkipSubKind;
|
|
18
19
|
nonCritical?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -37,12 +38,10 @@ function listenerDecorator(options: ListenerOptions): MethodDecorator {
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
38
39
|
|
|
39
40
|
/** Fires before a job execution starts. */
|
|
40
|
-
export const BeforeJob = (): MethodDecorator =>
|
|
41
|
-
listenerDecorator({ kind: 'job', phase: 'before' });
|
|
41
|
+
export const BeforeJob = (): MethodDecorator => listenerDecorator({ kind: 'job', phase: 'before' });
|
|
42
42
|
|
|
43
43
|
/** Fires after a job execution finishes (regardless of status). */
|
|
44
|
-
export const AfterJob = (): MethodDecorator =>
|
|
45
|
-
listenerDecorator({ kind: 'job', phase: 'after' });
|
|
44
|
+
export const AfterJob = (): MethodDecorator => listenerDecorator({ kind: 'job', phase: 'after' });
|
|
46
45
|
|
|
47
46
|
// ---------------------------------------------------------------------------
|
|
48
47
|
// Step-level listeners (2)
|
|
@@ -53,8 +52,7 @@ export const BeforeStep = (): MethodDecorator =>
|
|
|
53
52
|
listenerDecorator({ kind: 'step', phase: 'before' });
|
|
54
53
|
|
|
55
54
|
/** Fires after a step execution finishes (regardless of status). */
|
|
56
|
-
export const AfterStep = (): MethodDecorator =>
|
|
57
|
-
listenerDecorator({ kind: 'step', phase: 'after' });
|
|
55
|
+
export const AfterStep = (): MethodDecorator => listenerDecorator({ kind: 'step', phase: 'after' });
|
|
58
56
|
|
|
59
57
|
// ---------------------------------------------------------------------------
|
|
60
58
|
// Chunk-level listeners (3)
|
|
@@ -126,17 +124,17 @@ export const OnWriteError = (): MethodDecorator =>
|
|
|
126
124
|
// Skip listeners are not phase-based — each kind handles a distinct skip
|
|
127
125
|
// event emitted by the corresponding read/process/write. We store them
|
|
128
126
|
// under `kind: 'skip'` with `phase: 'after'` as a placeholder so the
|
|
129
|
-
// metadata shape is uniform
|
|
127
|
+
// metadata shape is uniform, plus `skipKind` for runtime dispatch.
|
|
130
128
|
// ---------------------------------------------------------------------------
|
|
131
129
|
|
|
132
130
|
/** Fires when a read is skipped (after the skip policy decides to skip). */
|
|
133
131
|
export const OnSkipRead = (): MethodDecorator =>
|
|
134
|
-
listenerDecorator({ kind: 'skip', phase: 'after' });
|
|
132
|
+
listenerDecorator({ kind: 'skip', phase: 'after', skipKind: 'read' });
|
|
135
133
|
|
|
136
134
|
/** Fires when a processed item is skipped. */
|
|
137
135
|
export const OnSkipProcess = (): MethodDecorator =>
|
|
138
|
-
listenerDecorator({ kind: 'skip', phase: 'after' });
|
|
136
|
+
listenerDecorator({ kind: 'skip', phase: 'after', skipKind: 'process' });
|
|
139
137
|
|
|
140
138
|
/** Fires when a write is skipped. */
|
|
141
139
|
export const OnSkipWrite = (): MethodDecorator =>
|
|
142
|
-
listenerDecorator({ kind: 'skip', phase: 'after' });
|
|
140
|
+
listenerDecorator({ kind: 'skip', phase: 'after', skipKind: 'write' });
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
import type { ChunkStepDefinition, ReaderRef, ProcessorRef, WriterRef } from '../core/ir';
|
|
3
3
|
import { RefKind } from '../core/ir';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
4
|
+
import type {
|
|
5
|
+
ItemExecutionContext,
|
|
6
|
+
ItemReader,
|
|
7
|
+
ItemProcessor,
|
|
8
|
+
ItemStream,
|
|
9
|
+
ItemWriter,
|
|
10
|
+
} from '../core/item';
|
|
11
|
+
import type { ExecutionContext, JobParameters, JobRepository } from '../core/repository';
|
|
6
12
|
import type { TransactionManager } from '../core/transaction';
|
|
7
13
|
import { StepStatus } from '../core/status';
|
|
8
14
|
import type { SkipPolicy, SkipContext } from '../policies/skip-policy';
|
|
@@ -20,9 +26,13 @@ export interface ChunkExecutionContext {
|
|
|
20
26
|
/** Step execution id, used to scope the chunk-progress checkpoint in the
|
|
21
27
|
* step's ExecutionContext (saved as `{ lastChunkIndex }`). */
|
|
22
28
|
stepExecutionId: string;
|
|
29
|
+
stepName?: string;
|
|
30
|
+
jobParameters?: JobParameters;
|
|
23
31
|
jobRepository: JobRepository;
|
|
24
32
|
transactionManager: TransactionManager;
|
|
25
33
|
listenerInvoker: ListenerInvoker;
|
|
34
|
+
/** Full listener resolver map keyed by `${phase}:${kind}:${name}`. */
|
|
35
|
+
listenerResolvers?: ResolverMap;
|
|
26
36
|
/** Map of resolved reader/processor/writer functions by name. */
|
|
27
37
|
resolvers: Map<string, (...args: unknown[]) => unknown | Promise<unknown>>;
|
|
28
38
|
jobExecutionId2: string; // unique key for listener resolver namespacing
|
|
@@ -90,7 +100,7 @@ type PhaseResult<T> = { kind: 'ok'; value: T } | { kind: 'skipped' };
|
|
|
90
100
|
* live `skipCount` accumulator so the policy's budget check is consistent
|
|
91
101
|
* with the accounting in the outer loop.
|
|
92
102
|
*/
|
|
93
|
-
interface RunPhaseOptions {
|
|
103
|
+
interface RunPhaseOptions<T = unknown> {
|
|
94
104
|
phase: Phase;
|
|
95
105
|
item: unknown;
|
|
96
106
|
skipPolicy: SkipPolicy | null;
|
|
@@ -102,6 +112,9 @@ interface RunPhaseOptions {
|
|
|
102
112
|
/** Invoked when an error is actually skipped (within budget). The caller
|
|
103
113
|
* is responsible for incrementing its own skipCount here. */
|
|
104
114
|
onSkip: (err: unknown) => Promise<void>;
|
|
115
|
+
before?: () => Promise<void>;
|
|
116
|
+
after?: (value: T) => Promise<void>;
|
|
117
|
+
onError?: (err: unknown) => Promise<void>;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
function isItemStream(value: unknown): value is ItemStream {
|
|
@@ -135,7 +148,8 @@ export class ChunkStepExecutor {
|
|
|
135
148
|
): Promise<ChunkExecutionResult> {
|
|
136
149
|
const skipPolicy = step.skipPolicy ? compileSkipPolicy(step.skipPolicy) : null;
|
|
137
150
|
const retryPolicy = step.retryPolicy ? compileRetryPolicy(step.retryPolicy) : null;
|
|
138
|
-
const
|
|
151
|
+
const listenerResolvers: ResolverMap = context.listenerResolvers ?? new Map();
|
|
152
|
+
const skipResolvers: ResolverMap = context.skipListenerResolvers ?? listenerResolvers;
|
|
139
153
|
|
|
140
154
|
const skipLimit = step.skipPolicy?.limit ?? 0;
|
|
141
155
|
const retryLimit = step.retryPolicy?.limit ?? 0;
|
|
@@ -190,6 +204,12 @@ export class ChunkStepExecutor {
|
|
|
190
204
|
let openedStreams: ItemStream[] = [];
|
|
191
205
|
let streamContext: ExecutionContext | null = null;
|
|
192
206
|
|
|
207
|
+
await context.listenerInvoker.invokeBefore(
|
|
208
|
+
listenerResolvers,
|
|
209
|
+
'step',
|
|
210
|
+
this.buildListenerContext(step, context),
|
|
211
|
+
);
|
|
212
|
+
|
|
193
213
|
try {
|
|
194
214
|
// Resolve inside the try block so a missing provider-token ref
|
|
195
215
|
// surfaces as FAILED/{exitMessage: <err>}, matching the tasklet
|
|
@@ -233,8 +253,9 @@ export class ChunkStepExecutor {
|
|
|
233
253
|
? Math.max(0, partition.to - partition.from - readCount)
|
|
234
254
|
: step.chunkSize;
|
|
235
255
|
if (skipCap === 0) break;
|
|
256
|
+
const itemContext = this.buildItemContext(step, context, chunkIndex);
|
|
236
257
|
for (let i = 0; i < skipCap; i++) {
|
|
237
|
-
const item = await reader.read();
|
|
258
|
+
const item = await reader.read(itemContext);
|
|
238
259
|
if (item == null) break;
|
|
239
260
|
drained += 1;
|
|
240
261
|
}
|
|
@@ -255,7 +276,9 @@ export class ChunkStepExecutor {
|
|
|
255
276
|
// remaining)` and `0` is a valid "no more items" cap (we
|
|
256
277
|
// exit the outer loop on the next iteration).
|
|
257
278
|
const remaining =
|
|
258
|
-
partition !== null
|
|
279
|
+
partition !== null
|
|
280
|
+
? Math.max(0, partition.to - partition.from - readCount)
|
|
281
|
+
: step.chunkSize;
|
|
259
282
|
if (remaining === 0) {
|
|
260
283
|
// Partition is fully drained. Exit the outer loop so the
|
|
261
284
|
// chunk executor returns COMPLETED with the partition's
|
|
@@ -263,9 +286,14 @@ export class ChunkStepExecutor {
|
|
|
263
286
|
break;
|
|
264
287
|
}
|
|
265
288
|
|
|
289
|
+
const itemContext = this.buildItemContext(step, context, chunkIndex);
|
|
290
|
+
const listenerContext = this.buildListenerContext(step, context, { chunkIndex });
|
|
291
|
+
|
|
292
|
+
await context.listenerInvoker.invokeBefore(listenerResolvers, 'chunk', listenerContext);
|
|
293
|
+
|
|
266
294
|
// ---- READ PHASE: per-item retry+skip ----
|
|
267
295
|
for (let i = 0; i < remaining && !eof; i++) {
|
|
268
|
-
const r = await this.runPhase<unknown>(() => reader.read(), {
|
|
296
|
+
const r = await this.runPhase<unknown>(() => reader.read(itemContext), {
|
|
269
297
|
phase: 'read',
|
|
270
298
|
item: null,
|
|
271
299
|
skipPolicy,
|
|
@@ -277,6 +305,25 @@ export class ChunkStepExecutor {
|
|
|
277
305
|
skipCount += 1;
|
|
278
306
|
await context.listenerInvoker.invokeOnSkipRead(skipResolvers, err, null);
|
|
279
307
|
},
|
|
308
|
+
before: async () => {
|
|
309
|
+
await context.listenerInvoker.invokeBeforeRead(listenerResolvers, listenerContext);
|
|
310
|
+
},
|
|
311
|
+
after: async (item) => {
|
|
312
|
+
if (item !== null && item !== undefined) {
|
|
313
|
+
await context.listenerInvoker.invokeAfterRead(
|
|
314
|
+
listenerResolvers,
|
|
315
|
+
item,
|
|
316
|
+
listenerContext,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
onError: async (err) => {
|
|
321
|
+
await context.listenerInvoker.invokeOnReadError(
|
|
322
|
+
listenerResolvers,
|
|
323
|
+
err,
|
|
324
|
+
listenerContext,
|
|
325
|
+
);
|
|
326
|
+
},
|
|
280
327
|
});
|
|
281
328
|
if (r.kind === 'skipped') continue;
|
|
282
329
|
if (r.value == null) {
|
|
@@ -287,7 +334,16 @@ export class ChunkStepExecutor {
|
|
|
287
334
|
items.push(r.value);
|
|
288
335
|
readCount += 1;
|
|
289
336
|
}
|
|
290
|
-
if (items.length === 0)
|
|
337
|
+
if (items.length === 0) {
|
|
338
|
+
await context.listenerInvoker.invokeAfter(listenerResolvers, 'chunk', listenerContext, {
|
|
339
|
+
status: StepStatus.COMPLETED,
|
|
340
|
+
readCount,
|
|
341
|
+
writeCount,
|
|
342
|
+
skipCount,
|
|
343
|
+
commitCount,
|
|
344
|
+
});
|
|
345
|
+
break; // EOF (either before first read or after skips)
|
|
346
|
+
}
|
|
291
347
|
|
|
292
348
|
// ---- PROCESS PHASE: per-item retry+skip ----
|
|
293
349
|
const processed: unknown[] = [];
|
|
@@ -296,7 +352,7 @@ export class ChunkStepExecutor {
|
|
|
296
352
|
processed.push(item);
|
|
297
353
|
continue;
|
|
298
354
|
}
|
|
299
|
-
const r = await this.runPhase<unknown>(() => processor.process(item), {
|
|
355
|
+
const r = await this.runPhase<unknown>(() => processor.process(item, itemContext), {
|
|
300
356
|
phase: 'process',
|
|
301
357
|
item,
|
|
302
358
|
skipPolicy,
|
|
@@ -308,6 +364,29 @@ export class ChunkStepExecutor {
|
|
|
308
364
|
skipCount += 1;
|
|
309
365
|
await context.listenerInvoker.invokeOnSkipProcess(skipResolvers, item, err);
|
|
310
366
|
},
|
|
367
|
+
before: async () => {
|
|
368
|
+
await context.listenerInvoker.invokeBeforeProcess(
|
|
369
|
+
listenerResolvers,
|
|
370
|
+
item,
|
|
371
|
+
listenerContext,
|
|
372
|
+
);
|
|
373
|
+
},
|
|
374
|
+
after: async (value) => {
|
|
375
|
+
await context.listenerInvoker.invokeAfterProcess(
|
|
376
|
+
listenerResolvers,
|
|
377
|
+
item,
|
|
378
|
+
value,
|
|
379
|
+
listenerContext,
|
|
380
|
+
);
|
|
381
|
+
},
|
|
382
|
+
onError: async (err) => {
|
|
383
|
+
await context.listenerInvoker.invokeOnProcessError(
|
|
384
|
+
listenerResolvers,
|
|
385
|
+
item,
|
|
386
|
+
err,
|
|
387
|
+
listenerContext,
|
|
388
|
+
);
|
|
389
|
+
},
|
|
311
390
|
});
|
|
312
391
|
if (r.kind === 'skipped') continue;
|
|
313
392
|
if (r.value !== null && r.value !== undefined) {
|
|
@@ -324,7 +403,7 @@ export class ChunkStepExecutor {
|
|
|
324
403
|
const r = await this.runPhase<{ written: number; skipped: number } | void>(
|
|
325
404
|
() =>
|
|
326
405
|
context.transactionManager.withTransaction(async () => {
|
|
327
|
-
return writer.write(processed);
|
|
406
|
+
return writer.write(processed, itemContext);
|
|
328
407
|
}),
|
|
329
408
|
{
|
|
330
409
|
phase: 'write',
|
|
@@ -338,6 +417,29 @@ export class ChunkStepExecutor {
|
|
|
338
417
|
skipCount += 1;
|
|
339
418
|
await context.listenerInvoker.invokeOnSkipWrite(skipResolvers, processed, err);
|
|
340
419
|
},
|
|
420
|
+
before: async () => {
|
|
421
|
+
await context.listenerInvoker.invokeBeforeWrite(
|
|
422
|
+
listenerResolvers,
|
|
423
|
+
processed,
|
|
424
|
+
listenerContext,
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
after: async (value) => {
|
|
428
|
+
await context.listenerInvoker.invokeAfterWrite(
|
|
429
|
+
listenerResolvers,
|
|
430
|
+
processed,
|
|
431
|
+
value,
|
|
432
|
+
listenerContext,
|
|
433
|
+
);
|
|
434
|
+
},
|
|
435
|
+
onError: async (err) => {
|
|
436
|
+
await context.listenerInvoker.invokeOnWriteError(
|
|
437
|
+
listenerResolvers,
|
|
438
|
+
processed,
|
|
439
|
+
err,
|
|
440
|
+
listenerContext,
|
|
441
|
+
);
|
|
442
|
+
},
|
|
341
443
|
},
|
|
342
444
|
);
|
|
343
445
|
if (r.kind === 'ok') {
|
|
@@ -370,6 +472,14 @@ export class ChunkStepExecutor {
|
|
|
370
472
|
stepExecutionId: context.stepExecutionId,
|
|
371
473
|
});
|
|
372
474
|
|
|
475
|
+
await context.listenerInvoker.invokeAfter(listenerResolvers, 'chunk', listenerContext, {
|
|
476
|
+
status: StepStatus.COMPLETED,
|
|
477
|
+
readCount,
|
|
478
|
+
writeCount,
|
|
479
|
+
skipCount,
|
|
480
|
+
commitCount: commitCount + 1,
|
|
481
|
+
});
|
|
482
|
+
|
|
373
483
|
commitCount += 1;
|
|
374
484
|
chunkIndex += 1;
|
|
375
485
|
}
|
|
@@ -378,7 +488,7 @@ export class ChunkStepExecutor {
|
|
|
378
488
|
openedStreams = [];
|
|
379
489
|
await this.closeStreams(streamsToClose);
|
|
380
490
|
|
|
381
|
-
|
|
491
|
+
const result: ChunkExecutionResult = {
|
|
382
492
|
status: StepStatus.COMPLETED,
|
|
383
493
|
exitCode: 'COMPLETED',
|
|
384
494
|
exitMessage: '',
|
|
@@ -387,6 +497,13 @@ export class ChunkStepExecutor {
|
|
|
387
497
|
skipCount,
|
|
388
498
|
commitCount,
|
|
389
499
|
};
|
|
500
|
+
await context.listenerInvoker.invokeAfter(
|
|
501
|
+
listenerResolvers,
|
|
502
|
+
'step',
|
|
503
|
+
this.buildListenerContext(step, context),
|
|
504
|
+
result,
|
|
505
|
+
);
|
|
506
|
+
return result;
|
|
390
507
|
} catch (err) {
|
|
391
508
|
let finalError = err;
|
|
392
509
|
const streamsToClose = openedStreams;
|
|
@@ -396,7 +513,13 @@ export class ChunkStepExecutor {
|
|
|
396
513
|
} catch (closeErr) {
|
|
397
514
|
finalError = closeErr;
|
|
398
515
|
}
|
|
399
|
-
|
|
516
|
+
await context.listenerInvoker.invokeOnError(
|
|
517
|
+
listenerResolvers,
|
|
518
|
+
'chunk',
|
|
519
|
+
this.buildListenerContext(step, context, { chunkIndex }),
|
|
520
|
+
finalError,
|
|
521
|
+
);
|
|
522
|
+
const result: ChunkExecutionResult = {
|
|
400
523
|
status: StepStatus.FAILED,
|
|
401
524
|
exitCode: 'FAILED',
|
|
402
525
|
exitMessage: finalError instanceof Error ? finalError.message : String(finalError),
|
|
@@ -405,9 +528,64 @@ export class ChunkStepExecutor {
|
|
|
405
528
|
skipCount,
|
|
406
529
|
commitCount,
|
|
407
530
|
};
|
|
531
|
+
await context.listenerInvoker.invokeOnError(
|
|
532
|
+
listenerResolvers,
|
|
533
|
+
'step',
|
|
534
|
+
this.buildListenerContext(step, context),
|
|
535
|
+
finalError,
|
|
536
|
+
);
|
|
537
|
+
await context.listenerInvoker.invokeAfter(
|
|
538
|
+
listenerResolvers,
|
|
539
|
+
'step',
|
|
540
|
+
this.buildListenerContext(step, context),
|
|
541
|
+
result,
|
|
542
|
+
);
|
|
543
|
+
return result;
|
|
408
544
|
}
|
|
409
545
|
}
|
|
410
546
|
|
|
547
|
+
private buildListenerContext(
|
|
548
|
+
step: ChunkStepDefinition,
|
|
549
|
+
context: ChunkExecutionContext,
|
|
550
|
+
extra: Record<string, unknown> = {},
|
|
551
|
+
): {
|
|
552
|
+
jobExecutionId: string;
|
|
553
|
+
stepExecutionId: string;
|
|
554
|
+
stepName: string;
|
|
555
|
+
jobParameters: JobParameters;
|
|
556
|
+
[extra: string]: unknown;
|
|
557
|
+
} {
|
|
558
|
+
return {
|
|
559
|
+
jobExecutionId: context.jobExecutionId,
|
|
560
|
+
stepExecutionId: context.stepExecutionId,
|
|
561
|
+
stepName: context.stepName ?? step.id,
|
|
562
|
+
jobParameters: context.jobParameters ?? {},
|
|
563
|
+
...extra,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private buildItemContext(
|
|
568
|
+
step: ChunkStepDefinition,
|
|
569
|
+
context: ChunkExecutionContext,
|
|
570
|
+
chunkIndex: number,
|
|
571
|
+
): ItemExecutionContext {
|
|
572
|
+
return {
|
|
573
|
+
jobExecutionId: context.jobExecutionId,
|
|
574
|
+
stepExecutionId: context.stepExecutionId,
|
|
575
|
+
stepName: context.stepName ?? step.id,
|
|
576
|
+
jobParameters: context.jobParameters ?? {},
|
|
577
|
+
chunkIndex,
|
|
578
|
+
getExecutionContext: async () =>
|
|
579
|
+
context.jobRepository.getExecutionContext({ stepExecutionId: context.stepExecutionId }),
|
|
580
|
+
saveExecutionContext: async (ctx: ExecutionContext) => {
|
|
581
|
+
await context.jobRepository.saveExecutionContext(
|
|
582
|
+
{ stepExecutionId: context.stepExecutionId },
|
|
583
|
+
ctx,
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
411
589
|
private async updateStreams(
|
|
412
590
|
streams: readonly ItemStream[],
|
|
413
591
|
ctx: ExecutionContext,
|
|
@@ -443,7 +621,7 @@ export class ChunkStepExecutor {
|
|
|
443
621
|
*/
|
|
444
622
|
private async runPhase<T>(
|
|
445
623
|
op: () => Promise<T>,
|
|
446
|
-
options: RunPhaseOptions
|
|
624
|
+
options: RunPhaseOptions<T>,
|
|
447
625
|
): Promise<PhaseResult<T>> {
|
|
448
626
|
let attempt = 1;
|
|
449
627
|
// Outer safety cap: when a retry policy exists, allow many iterations;
|
|
@@ -452,10 +630,12 @@ export class ChunkStepExecutor {
|
|
|
452
630
|
const outerCap = options.retryPolicy ? 999 : 1;
|
|
453
631
|
|
|
454
632
|
while (attempt <= outerCap) {
|
|
633
|
+
if (options.before) await options.before();
|
|
634
|
+
let value: T;
|
|
455
635
|
try {
|
|
456
|
-
|
|
457
|
-
return { kind: 'ok', value };
|
|
636
|
+
value = await op();
|
|
458
637
|
} catch (err) {
|
|
638
|
+
if (options.onError) await options.onError(err);
|
|
459
639
|
// 1) Skip consultation: is this error skippable, and is there budget?
|
|
460
640
|
if (options.skipPolicy) {
|
|
461
641
|
// Use the policy's `shouldSkip` with `skipCount: 0` to get a pure
|
|
@@ -513,6 +693,8 @@ export class ChunkStepExecutor {
|
|
|
513
693
|
// 3) Neither skippable nor retryable: re-throw the original error.
|
|
514
694
|
throw err;
|
|
515
695
|
}
|
|
696
|
+
if (options.after) await options.after(value);
|
|
697
|
+
return { kind: 'ok', value };
|
|
516
698
|
}
|
|
517
699
|
|
|
518
700
|
// Defensive: the outer cap should never be reached when a retry policy
|
|
@@ -529,7 +711,11 @@ export class ChunkStepExecutor {
|
|
|
529
711
|
if (typeof result === 'function') {
|
|
530
712
|
return { read: result as ItemReader['read'] };
|
|
531
713
|
}
|
|
532
|
-
if (
|
|
714
|
+
if (
|
|
715
|
+
result !== null &&
|
|
716
|
+
typeof result === 'object' &&
|
|
717
|
+
typeof (result as ItemReader).read === 'function'
|
|
718
|
+
) {
|
|
533
719
|
return result as ItemReader;
|
|
534
720
|
}
|
|
535
721
|
return { read: ref.fn as ItemReader['read'] };
|
|
@@ -552,7 +738,11 @@ export class ChunkStepExecutor {
|
|
|
552
738
|
if (typeof result === 'function') {
|
|
553
739
|
return { process: result as ItemProcessor['process'] };
|
|
554
740
|
}
|
|
555
|
-
if (
|
|
741
|
+
if (
|
|
742
|
+
result !== null &&
|
|
743
|
+
typeof result === 'object' &&
|
|
744
|
+
typeof (result as ItemProcessor).process === 'function'
|
|
745
|
+
) {
|
|
556
746
|
return result as ItemProcessor;
|
|
557
747
|
}
|
|
558
748
|
return { process: ref.fn as ItemProcessor['process'] };
|
|
@@ -575,7 +765,11 @@ export class ChunkStepExecutor {
|
|
|
575
765
|
if (typeof result === 'function') {
|
|
576
766
|
return { write: result as ItemWriter['write'] };
|
|
577
767
|
}
|
|
578
|
-
if (
|
|
768
|
+
if (
|
|
769
|
+
result !== null &&
|
|
770
|
+
typeof result === 'object' &&
|
|
771
|
+
typeof (result as ItemWriter).write === 'function'
|
|
772
|
+
) {
|
|
579
773
|
return result as ItemWriter;
|
|
580
774
|
}
|
|
581
775
|
return { write: ref.fn as ItemWriter['write'] };
|