@nest-batch/core 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.ko.md +95 -0
  2. package/README.md +56 -332
  3. package/package.json +3 -2
package/README.ko.md ADDED
@@ -0,0 +1,95 @@
1
+ # @nest-batch/core
2
+
3
+ NestJS용 batch engine core입니다. job model, decorator, launcher/explorer/operator
4
+ service, in-process transport, 그리고 `@nest-batch/*` 패키지들이 사용하는 adapter
5
+ contract를 제공합니다.
6
+
7
+ English: [README.md](./README.md)
8
+
9
+ ## 설치
10
+
11
+ ```bash
12
+ pnpm add @nest-batch/core reflect-metadata
13
+ ```
14
+
15
+ peer dependency:
16
+
17
+ - `@nestjs/common` `^10 || ^11`
18
+ - `@nestjs/core` `^10 || ^11`
19
+ - `reflect-metadata` `^0.2`
20
+
21
+ ## Public Import
22
+
23
+ ```ts
24
+ import {
25
+ Batch,
26
+ BatchScheduled,
27
+ InProcessAdapter,
28
+ JobExplorer,
29
+ JobLauncher,
30
+ JobOperator,
31
+ NestBatchModule,
32
+ } from '@nest-batch/core';
33
+ ```
34
+
35
+ `Batch.Jobable`, `Batch.Stepable`, `Batch.Tasklet`, `Batch.ItemReader`,
36
+ `Batch.ItemProcessor`, `Batch.ItemWriter`, listener decorator는 `Batch` namespace로
37
+ 사용합니다.
38
+
39
+ ## Module Wiring
40
+
41
+ core에는 persistence adapter 하나와 transport adapter 하나가 필요합니다.
42
+
43
+ ```ts
44
+ import { Module } from '@nestjs/common';
45
+ import { InProcessAdapter, NestBatchModule } from '@nest-batch/core';
46
+ import { MikroOrmAdapter } from '@nest-batch/mikro-orm';
47
+
48
+ @Module({
49
+ imports: [
50
+ NestBatchModule.forRoot({
51
+ adapters: {
52
+ persistence: MikroOrmAdapter.forRoot(),
53
+ transport: InProcessAdapter.forRoot(),
54
+ },
55
+ }),
56
+ ],
57
+ })
58
+ export class AppModule {}
59
+ ```
60
+
61
+ `InProcessAdapter`는 이 패키지에 포함되어 있습니다. 다른 transport adapter는 sibling
62
+ package로 제공합니다.
63
+
64
+ ## Job 정의
65
+
66
+ ```ts
67
+ import { Injectable } from '@nestjs/common';
68
+ import { Batch } from '@nest-batch/core';
69
+
70
+ @Injectable()
71
+ @Batch.Jobable({ id: 'send-digest', restartable: true })
72
+ export class SendDigestJob {
73
+ @Batch.Stepable({ id: 'send' })
74
+ @Batch.Tasklet()
75
+ async send(): Promise<void> {
76
+ await sendDigestEmails();
77
+ }
78
+ }
79
+ ```
80
+
81
+ job class를 Nest provider로 등록하면 application boot 시점에 `NestBatchModule`이
82
+ `@Batch.Jobable` provider를 발견합니다.
83
+
84
+ ## 실행과 조회
85
+
86
+ ```ts
87
+ await jobLauncher.launch('send-digest', { businessDate: '2026-06-25' });
88
+
89
+ const executions = await jobExplorer.listJobExecutions({
90
+ status: 'COMPLETED',
91
+ });
92
+ ```
93
+
94
+ `JobLauncher`는 작업을 시작하고, `JobExplorer`는 durable state를 읽으며,
95
+ `JobOperator`는 stop, restart, abandon, start-next-instance operation을 제공합니다.
package/README.md CHANGED
@@ -1,371 +1,95 @@
1
- # `@nest-batch/core`
1
+ # @nest-batch/core
2
2
 
3
- A lightweight, NestJS-coupled batch processing core modelled after Spring
4
- Batch. `@nest-batch/core` owns the **batch engine**: Job/Step/Chunk/Tasklet
5
- semantics, checkpoint, restart, skip, chunk transaction, and business retry.
6
- It does not own persistence, transport, or scheduling. Those live in
7
- [sibling packages](#what-is-not-in-core).
3
+ Core batch engine for NestJS. This package provides the public job model,
4
+ decorators, launcher/explorer/operator services, in-process transport, and the
5
+ adapter contracts used by the `@nest-batch/*` package family.
8
6
 
9
- The package is dependency-light on purpose. It only pulls in
10
- `@nestjs/common`, `@nestjs/core`, and `reflect-metadata`. Anything
11
- specific to a database, a queue, or a scheduler is injected through
12
- tokens at the DI boundary, so a host app can swap persistence and
13
- transport without touching the core.
14
-
15
- ---
7
+ Korean: [README.ko.md](./README.ko.md)
16
8
 
17
9
  ## Install
18
10
 
19
11
  ```bash
20
- pnpm add @nest-batch/core
21
- ```
22
-
23
- Peer dependencies are pulled in by the host app:
24
-
25
- | Package | Range |
26
- | ------------------ | --------- |
27
- | `@nestjs/common` | `^11.0.0` |
28
- | `@nestjs/core` | `^11.0.0` |
29
- | `reflect-metadata` | `^0.2.2` |
30
-
31
- Core supports Nest 10 and 11.
32
-
33
- ---
34
-
35
- ## Conceptual model
36
-
37
- The model is a direct port of Spring Batch's mental model. If you've
38
- written a Spring Batch job, you already know 80% of this.
39
-
40
- ```
41
- Job
42
- └── Step (one or more)
43
- ├── Chunk step (read → process → write in fixed-size chunks)
44
- │ ├── ItemReader<T>
45
- │ ├── ItemProcessor<T, R>
46
- │ └── ItemWriter<R>
47
- └── Tasklet (single-method work unit, no chunking)
48
- ```
49
-
50
- ### Job
51
-
52
- A named unit of work. The host app declares one with the `@Jobable`
53
- decorator or the `BatchBuilder` fluent API. A job has:
54
-
55
- - a unique `id`
56
- - an ordered list of `Step`s
57
- - a `JobParameters` shape (the params that pin a `JobInstance`)
58
-
59
- ### Step
60
-
61
- A step is either a **chunk step** or a **tasklet step**. The compiler
62
- decides which one based on which handler method you provide. Each step
63
- runs inside a `StepExecution` row in the batch meta schema, and the
64
- `StepExecution.id` is the unit of restart/checkpoint.
65
-
66
- ### Chunk step
67
-
68
- A chunk step reads `chunkSize` items, processes them, writes them, and
69
- repeats until the reader is exhausted. The chunk is the unit of
70
- transaction: if any item in the chunk fails, the whole chunk rolls
71
- back. This is the model Spring Batch uses, and we keep it.
72
-
73
- The reader, processor, and writer are plain Nest providers. You can
74
- declare them with `@ItemReader`, `@ItemProcessor`, `@ItemWriter` (under
75
- the `Batch` namespace) or with method references on the job
76
- class itself. Three reference kinds are accepted:
77
-
78
- - `BuilderLambda` — a function value captured by the builder.
79
- - `Method` — a method on the job class.
80
- - `ProviderToken` — a Nest DI token resolved at runtime.
81
-
82
- ### Tasklet step
83
-
84
- A single method that runs to completion. Useful for one-off work that
85
- doesn't fit the read/process/write shape (e.g. "run this SQL and
86
- move on"). A tasklet is the right answer when you don't need chunking,
87
- skip, or restart.
88
-
89
- ### Listeners
90
-
91
- Listeners fire around every transition in the engine. You tag a method
92
- on your provider with one of the listener decorators, and the engine
93
- calls it at the right moment. The full set:
94
-
95
- | Decorator | Fires |
96
- | ----------------- | --------------------------------------------- |
97
- | `@BeforeJob` | Before a job execution starts. |
98
- | `@AfterJob` | After a job execution finishes (any status). |
99
- | `@BeforeStep` | Before a step execution starts. |
100
- | `@AfterStep` | After a step execution finishes (any status). |
101
- | `@BeforeChunk` | Before each chunk (read-process-write cycle). |
102
- | `@AfterChunk` | After each chunk finishes successfully. |
103
- | `@OnChunkError` | When a chunk throws. |
104
- | `@BeforeRead` | Before each item is read. |
105
- | `@AfterRead` | After each item is read. |
106
- | `@OnReadError` | When the reader throws. |
107
- | `@BeforeProcess` | Before each item is processed. |
108
- | `@AfterProcess` | After each item is processed. |
109
- | `@OnProcessError` | When the processor throws. |
110
- | `@BeforeWrite` | Before the writer receives a chunk. |
111
- | `@AfterWrite` | After the writer finishes a chunk. |
112
- | `@OnWriteError` | When the writer throws. |
113
- | `@OnSkipRead` | When a read is skipped by the skip policy. |
114
- | `@OnSkipProcess` | When a processed item is skipped. |
115
- | `@OnSkipWrite` | When a write is skipped. |
116
-
117
- You can mark a listener as `nonCritical: true` via
118
- `@BeforeJob({ nonCritical: true })` if the engine should swallow
119
- exceptions from it. A critical listener that throws fails the
120
- execution.
121
-
122
- ### Skip and retry policies
123
-
124
- Skip and retry are Batch Core concerns, not transport concerns. The
125
- default policy is "fail on first error", and you can swap in:
126
-
127
- - `LimitSkipPolicy` — skip up to N items of a given kind
128
- (read/process/write), then fail.
129
- - `ClassifySkipPolicy` — skip based on the exception class.
130
- - `ExponentialBackoffRetryPolicy` — retry with exponential backoff.
131
- - `FixedDelayRetryPolicy` — retry with a fixed delay.
132
-
133
- The BullMQ package reuses these policies. It does **not** reimplement
134
- them. See "what is NOT in core" below.
135
-
136
- ---
137
-
138
- ## Polymorphic `JobLauncher`
139
-
140
- `JobLauncher` is the public entry point for starting a job. Its
141
- signature is:
142
-
143
- ```ts
144
- launch(jobId: string, params: JobParameters = {}): Promise<JobExecution>
12
+ pnpm add @nest-batch/core reflect-metadata
145
13
  ```
146
14
 
147
- The launcher does:
15
+ Peer dependencies:
148
16
 
149
- 1. Look up the `JobDefinition` from the registry. Missing → `JobNotFoundError`.
150
- 2. Canonicalize `params` into a stable `jobKey` hash. Object key
151
- order, `null/undefined` omission, `Date → ISO` are all normalized
152
- so semantically-identical params yield the same key.
153
- 3. `createExecutionAtomic(jobId, jobKey, params)` — atomic
154
- get-or-create instance + `SELECT ... FOR UPDATE SKIP LOCKED` to
155
- serialize concurrent launches + running-execution check + insert.
156
- 4. Delegate to whatever `IExecutionStrategy` is bound to the
157
- `EXECUTION_STRATEGY` token. The default is the in-process strategy;
158
- `@nest-batch/bullmq` overrides it with a transport strategy.
17
+ - `@nestjs/common` `^10 || ^11`
18
+ - `@nestjs/core` `^10 || ^11`
19
+ - `reflect-metadata` `^0.2`
159
20
 
160
- `IExecutionStrategy` is the polymorphism seam:
21
+ ## Public Imports
161
22
 
162
23
  ```ts
163
- export interface IExecutionStrategy {
164
- readonly name: string;
165
- launch(
166
- job: JobDefinition,
167
- params: JobParameters,
168
- ctx: ExecutionStrategyContext,
169
- ): Promise<LaunchResult>;
170
- }
24
+ import {
25
+ Batch,
26
+ BatchScheduled,
27
+ InProcessAdapter,
28
+ JobExplorer,
29
+ JobLauncher,
30
+ JobOperator,
31
+ NestBatchModule,
32
+ } from '@nest-batch/core';
171
33
  ```
172
34
 
173
- `LaunchResult` is a discriminated union:
174
-
175
- - `{ kind: 'completed', status }` — the strategy ran to a terminal
176
- state in-process. The launcher resolves the persisted
177
- `JobExecution` and returns it.
178
- - `{ kind: 'enqueued', queueJobId }` — the strategy handed off to a
179
- transport. The launcher still resolves the latest persisted
180
- `JobExecution` (which is in `STARTING` / `STARTED` because the
181
- executor has not run yet on the launcher process).
182
-
183
- The `JobLauncher.launch` API is intentionally stable. Strategies
184
- change; the public surface does not.
35
+ Use the `Batch` namespace for decorators such as `Batch.Jobable`,
36
+ `Batch.Stepable`, `Batch.Tasklet`, `Batch.ItemReader`, `Batch.ItemProcessor`,
37
+ `Batch.ItemWriter`, and listener decorators.
185
38
 
186
- ---
39
+ ## Module Wiring
187
40
 
188
- ## Module wiring
41
+ Core requires one persistence adapter and one transport adapter.
189
42
 
190
43
  ```ts
191
44
  import { Module } from '@nestjs/common';
192
- import {
193
- NestBatchModule,
194
- JobRepository,
195
- TransactionManager,
196
- InProcessExecutionStrategy,
197
- IN_PROCESS_EXECUTION_STRATEGY_PROVIDER,
198
- } from '@nest-batch/core';
199
- import { MikroORMJobRepository, MikroORMTransactionManager } from '@nest-batch/mikro-orm';
200
- import { MikroOrmModule } from '@mikro-orm/nestjs';
201
- import { BATCH_META_ENTITIES } from '@nest-batch/mikro-orm';
202
- import { ProductEntity } from './entities/product.entity';
45
+ import { InProcessAdapter, NestBatchModule } from '@nest-batch/core';
46
+ import { MikroOrmAdapter } from '@nest-batch/mikro-orm';
203
47
 
204
48
  @Module({
205
49
  imports: [
206
- MikroOrmModule.forRoot({
207
- entities: [ProductEntity, ...BATCH_META_ENTITIES],
208
- // ...
50
+ NestBatchModule.forRoot({
51
+ adapters: {
52
+ persistence: MikroOrmAdapter.forRoot(),
53
+ transport: InProcessAdapter.forRoot(),
54
+ },
209
55
  }),
210
- NestBatchModule.forRoot(),
211
- ],
212
- providers: [
213
- { provide: JobRepository, useClass: MikroORMJobRepository },
214
- { provide: TransactionManager, useClass: MikroORMTransactionManager },
215
- InProcessExecutionStrategy,
216
- IN_PROCESS_EXECUTION_STRATEGY_PROVIDER,
217
56
  ],
218
57
  })
219
58
  export class AppModule {}
220
59
  ```
221
60
 
222
- `NestBatchModule` is `global: true`, so sub-modules don't have to
223
- import it again. The module exports `JobRegistry`, `DefinitionCompiler`,
224
- `BatchExplorer`, `FlowEvaluator`, and `BatchScheduleRegistry` so
225
- consumers can inject them from outside.
61
+ `InProcessAdapter` is included in this package. Other transport adapters are
62
+ available as sibling packages.
226
63
 
227
- `forRootAsync` is also available when the repository/strategy bindings
228
- need to come from a config service or another async source:
64
+ ## Define a Job
229
65
 
230
66
  ```ts
231
- NestBatchModule.forRootAsync({
232
- imports: [ConfigModule],
233
- inject: [ConfigService],
234
- useFactory: (cfg: ConfigService) => ({
235
- repository: {
236
- provide: JOB_REPOSITORY_TOKEN,
237
- useClass: cfg.get<Type<JobRepository>>('BATCH_REPOSITORY'),
238
- },
239
- }),
240
- });
67
+ import { Injectable } from '@nestjs/common';
68
+ import { Batch } from '@nest-batch/core';
69
+
70
+ @Injectable()
71
+ @Batch.Jobable({ id: 'send-digest', restartable: true })
72
+ export class SendDigestJob {
73
+ @Batch.Stepable({ id: 'send' })
74
+ @Batch.Tasklet()
75
+ async send(): Promise<void> {
76
+ await sendDigestEmails();
77
+ }
78
+ }
241
79
  ```
242
80
 
243
- ---
244
-
245
- ## Listener resolver
81
+ Register the job class as a Nest provider. `NestBatchModule` discovers
82
+ registered `@Batch.Jobable` providers when the application boots.
246
83
 
247
- Listeners are discovered by walking every `@Jobable` class the
248
- `BatchExplorer` finds, reading the `BATCH_LISTENER_METADATA` slot from
249
- each method, and building a per-job, per-step resolver map. The map
250
- is populated once at `OnApplicationBootstrap` (see
251
- `BatchBootstrapper`) and is read on every transition.
252
-
253
- Critical vs non-critical semantics:
254
-
255
- - A **critical** listener that throws fails the execution. The
256
- executor records the failure and the listener exception is part of
257
- the failure context.
258
- - A **non-critical** listener that throws is logged and contained. The
259
- execution continues.
260
-
261
- The two paths are separated on purpose. Critical listener failures
262
- should be loud, non-critical ones should not poison the run.
263
-
264
- ---
265
-
266
- ## Contract suite
267
-
268
- `@nest-batch/core` ships a contract suite that the adapter packages
269
- use to prove they implement the repository and transaction
270
- contracts correctly. It is exposed at
271
- `@nest-batch/core/test-contracts`:
84
+ ## Launch and Inspect
272
85
 
273
86
  ```ts
274
- import {
275
- runJobRepositoryContract,
276
- runTransactionManagerContract,
277
- } from '@nest-batch/core/test-contracts';
278
- ```
279
-
280
- The contract covers:
87
+ await jobLauncher.launch('send-digest', { businessDate: '2026-06-25' });
281
88
 
282
- - `getOrCreateJobInstance` idempotency, concurrent creation.
283
- - `createExecutionAtomic` — atomicity, lock semantics, running-execution guard.
284
- - `updateJobExecution` / `getJobExecution` — round-trip integrity.
285
- - `createStepExecution` / `updateStepExecution` / `getStepExecution` — same for step rows.
286
- - `getExecutionContext` / `saveExecutionContext` — versioning, optimistic concurrency.
287
- - `findLatestStepExecution` — restart/checkpoint lookup; must return
288
- the most recent `StepExecution` for `(jobExecutionId, stepName)`.
289
- - `TransactionManager` — wrap / commit / rollback / nested.
290
-
291
- `@nest-batch/mikro-orm` and `@nest-batch/typeorm` both run this suite
292
- against their implementations. The in-memory reference implementation
293
- in core also passes it. If you write a custom adapter, the suite is
294
- how you prove it satisfies the contract.
295
-
296
- ---
297
-
298
- ## Public API surface
299
-
300
- Everything in `@nest-batch/core` is reachable from the package root.
301
- The barrel re-exports:
302
-
303
- - `./core` — IR (`JobDefinition`, `StepDefinition`, ...), errors, status, execution context, item interfaces, repository/transaction contracts.
304
- - `./compiler` — turns discovered jobs into compiled IR.
305
- - `./registry` — `JobRegistry` and friends.
306
- - `./execution` — `JobLauncher`, `JobExecutor`, `InProcessExecutionStrategy`, `IExecutionStrategy`, `EXECUTION_STRATEGY`, `ChunkStepExecutor`, `TaskletStepExecutor`, `ListenerInvoker`, `RefResolver`.
307
- - `./transaction` — `TransactionManager` token and contract.
308
- - `./repository` — `JobRepository` token, contract, in-memory reference, ID generators.
309
- - `./decorators` — under the `Batch` namespace (`@Jobable`, `@ItemReader`, `@ItemProcessor`, `@ItemWriter`, `@Tasklet`, listener decorators).
310
- - `./scheduling/batch-scheduled` — `@BatchScheduled` and its schedule option/error types are also re-exported directly from the package root.
311
- - `./module` — `NestBatchModule`, tokens, options.
312
- - `./builder` — fluent `BatchBuilder`, `JobBuilder`, `StepBuilder`, `FlowBuilder`.
313
- - `./explorer` — `BatchExplorer` (the metadata scanner).
314
- - `./listeners` — built-in `LoggingListener`, `MetricsListener`, `TimingListener` reference implementations.
315
- - `./policies` — `LimitSkipPolicy`, `ClassifySkipPolicy`, retry policies, backoff helpers.
316
- - `./flow` — `FlowEvaluator` for the `on` / `from` / `end` flow DSL.
317
- - `./observability` — `BatchObserver` contract, `BATCH_EVENT` constants, `NoopBatchObserver` default.
318
-
319
- Decorator names collide with interface names (e.g. `Tasklet` is both a
320
- decorator and an interface). Decorators are re-exported under
321
- `Batch`; interfaces are reachable as bare names from
322
- `./core/item`. `BatchDecorators` remains available as a backward-compatible
323
- alias. This is intentional.
324
-
325
- ---
326
-
327
- ## What is NOT in core
328
-
329
- Core is the engine. The following live in sibling packages and are
330
- injected at the DI boundary:
331
-
332
- | Concern | Package | Why |
333
- | ----------------------------- | ----------------------- | ----------------------------------------------------------------- |
334
- | **Persistence (MikroORM)** | `@nest-batch/mikro-orm` | Exposes the batch meta entities; the host owns migrations. |
335
- | **Persistence (TypeORM 1.0)** | `@nest-batch/typeorm` | Exposes the same table contract as TypeORM 1.0.0 entities. |
336
- | **Transport (BullMQ)** | `@nest-batch/bullmq` | The Redis-backed execution strategy. Owns Queue/Worker lifecycle. |
337
- | **Drizzle** | _not in this release_ | Explicitly excluded and deferred. See `MIGRATION.md`. |
338
-
339
- Core itself does **not** ship:
340
-
341
- - A default `JobRepository` (the choice of DB is the host's).
342
- - A default `TransactionManager` (same).
343
- - A default transport (in-process is the default; siblings override).
344
- - An admin UI.
345
- - A metrics backend (Prometheus, OpenTelemetry, ...).
346
- - A tracing backend.
347
- - A webhook or notification system.
348
- - A job visualization dashboard.
349
- - Multi-tenant routing.
350
-
351
- These are out of scope by design. If you need one, write a
352
- `BatchObserver` adapter that hooks into the event stream, or open an
353
- issue if you think it belongs in core.
354
-
355
- ---
356
-
357
- ## Scripts
358
-
359
- ```bash
360
- pnpm --filter @nest-batch/core build # SWC transpile + tsc declarations
361
- pnpm --filter @nest-batch/core test # vitest run
362
- pnpm --filter @nest-batch/core test:watch # vitest watch
363
- pnpm --filter @nest-batch/core test:e2e # vitest e2e (requires Postgres/Redis)
364
- pnpm --filter @nest-batch/core typecheck # tsc --noEmit
89
+ const executions = await jobExplorer.listJobExecutions({
90
+ status: 'COMPLETED',
91
+ });
365
92
  ```
366
93
 
367
- The boundary test (`tests/core/boundary/no-forbidden-imports.test.ts`)
368
- guards core's dependency-light promise. It fails the build if any
369
- forbidden integration package (`bullmq`, `mikro-orm`, `typeorm`,
370
- `drizzle-orm`) shows up as a core import. The small `cron` dependency
371
- is intentionally allowed for the built-in in-process scheduler bridge.
94
+ `JobLauncher` starts work, `JobExplorer` reads durable state, and `JobOperator`
95
+ provides stop, restart, abandon, and start-next-instance operations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nest-batch/core",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Batch processing engine for NestJS — jobs, steps, chunk-oriented processing, and the persistence/transport contracts the @nest-batch adapters implement.",
5
5
  "license": "MIT",
6
6
  "author": "easdkr",
@@ -28,7 +28,8 @@
28
28
  "dist/src",
29
29
  "dist/tests/contracts",
30
30
  "src",
31
- "README.md"
31
+ "README.md",
32
+ "README.ko.md"
32
33
  ],
33
34
  "exports": {
34
35
  ".": {