@nest-batch/webhook 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.ko.md +49 -0
- package/README.md +25 -370
- package/package.json +5 -4
package/README.ko.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @nest-batch/webhook
|
|
2
|
+
|
|
3
|
+
`@nest-batch/core` lifecycle event를 외부 URL로 보내는 webhook observer입니다.
|
|
4
|
+
payload는 HMAC-SHA256으로 서명됩니다.
|
|
5
|
+
|
|
6
|
+
English: [README.md](./README.md)
|
|
7
|
+
|
|
8
|
+
## 설치
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @nest-batch/core @nest-batch/webhook
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
패키지는 runtime `fetch`와 `AbortController` API를 사용하므로 Node 20 이상을
|
|
15
|
+
권장합니다.
|
|
16
|
+
|
|
17
|
+
## Public Import
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
WebhookBatchModule,
|
|
22
|
+
WebhookBatchObserver,
|
|
23
|
+
BATCH_EVENT,
|
|
24
|
+
type WebhookBatchModuleOptions,
|
|
25
|
+
} from '@nest-batch/webhook';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Wiring
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { WebhookBatchModule } from '@nest-batch/webhook';
|
|
32
|
+
|
|
33
|
+
@Module({
|
|
34
|
+
imports: [
|
|
35
|
+
NestBatchModule.forRoot({ adapters }),
|
|
36
|
+
WebhookBatchModule.forRoot({
|
|
37
|
+
secret: process.env.WEBHOOK_HMAC_SECRET,
|
|
38
|
+
urls: ['https://hooks.example.com/nest-batch'],
|
|
39
|
+
events: [BATCH_EVENT.JOB_COMPLETED, BATCH_EVENT.JOB_FAILED],
|
|
40
|
+
timeoutMs: 10_000,
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
export class AppModule {}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
기본 subscription은 job completed, job failed, step failed입니다. HTTP 4xx 응답은
|
|
48
|
+
설정 오류로 분류되고, HTTP 5xx, network error, timeout은 제한된 backoff로
|
|
49
|
+
retry됩니다.
|
package/README.md
CHANGED
|
@@ -1,395 +1,50 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nest-batch/webhook
|
|
2
2
|
|
|
3
|
-
Webhook
|
|
4
|
-
|
|
5
|
-
`WebhookBatchObserver` that subscribes to the `BATCH_EVENT.*`
|
|
6
|
-
lifecycle stream and POSTs an HMAC-SHA256-signed JSON envelope
|
|
7
|
-
to one or more URLs, with exponential-backoff retry on
|
|
8
|
-
5xx / network errors, a `logger.warn` dead-letter on final
|
|
9
|
-
failure, and a hard `no retry on 4xx` rule.
|
|
3
|
+
Webhook observer for `@nest-batch/core` lifecycle events. It sends
|
|
4
|
+
HMAC-SHA256-signed JSON envelopes to one or more URLs.
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
> envelopes; it does not care whether the job was driven by
|
|
13
|
-
> BullMQ, Kafka, or the in-process strategy. Any transport
|
|
14
|
-
> package that bridges `QueueEvents` (or equivalent) to the
|
|
15
|
-
> `BatchObserver.onEvent` entry point will deliver events to
|
|
16
|
-
> this observer without any extra wiring on the host's side.
|
|
17
|
-
|
|
18
|
-
The package is a **sibling**, not a replacement. The dependency
|
|
19
|
-
direction is strict and one-way:
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
@nest-batch/webhook ──▶ @nest-batch/core
|
|
23
|
-
│
|
|
24
|
-
└──────────────▶ @nestjs/common, @nestjs/core (peer)
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`@nest-batch/core` does not know this package exists. It
|
|
28
|
-
cannot — the boundary is enforced by
|
|
29
|
-
[`packages/core/tests/core/boundary/no-forbidden-imports.test.ts`](../core/tests/core/boundary/no-forbidden-imports.test.ts),
|
|
30
|
-
which scans the core source tree and fails the build if a
|
|
31
|
-
forbidden package — `bullmq`, `kafkajs`, `mikro-orm`, `typeorm`,
|
|
32
|
-
`drizzle-orm`, `cron` — appears as a core import.
|
|
33
|
-
|
|
34
|
-
The observer uses **native `fetch`** (Node 20+). No HTTP client
|
|
35
|
-
(`undici` / `axios` / `node-fetch`) is added as a peer dep —
|
|
36
|
-
the host does not need to ship a separate HTTP library to
|
|
37
|
-
enable webhook delivery. `AbortController` provides the
|
|
38
|
-
per-attempt timeout.
|
|
39
|
-
|
|
40
|
-
---
|
|
6
|
+
Korean: [README.ko.md](./README.ko.md)
|
|
41
7
|
|
|
42
8
|
## Install
|
|
43
9
|
|
|
44
10
|
```bash
|
|
45
|
-
pnpm add @nest-batch/webhook
|
|
11
|
+
pnpm add @nest-batch/core @nest-batch/webhook
|
|
46
12
|
```
|
|
47
13
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| Package | Range |
|
|
51
|
-
| ------------------ | ------------- |
|
|
52
|
-
| `@nest-batch/core` | `workspace:*` |
|
|
53
|
-
| `@nestjs/common` | `^10 \|\| ^11` |
|
|
54
|
-
| `@nestjs/core` | `^10 \|\| ^11` |
|
|
55
|
-
|
|
56
|
-
Node 20+ is required for the native `fetch` / `AbortController`
|
|
57
|
-
runtime. Older Node versions are not supported.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Peer dependencies
|
|
14
|
+
Node 20 or newer is recommended because the package uses the runtime `fetch`
|
|
15
|
+
and `AbortController` APIs.
|
|
62
16
|
|
|
63
|
-
|
|
64
|
-
| ------------------ | --------------- | -------------------------------------------------------------------------------------- |
|
|
65
|
-
| `@nest-batch/core` | `workspace:*` | The batch engine. The observer only consumes the `BatchObserver` / `BATCH_EVENT` surface. |
|
|
66
|
-
| `@nestjs/common` | `^10 \|\| ^11` | For `@Module` / `Module` / injection tokens. Nest 10 and 11 are both supported. |
|
|
67
|
-
| `@nestjs/core` | `^10 \|\| ^11` | Peer-declared for the dynamic-module surface; not used at runtime. |
|
|
17
|
+
## Public Imports
|
|
68
18
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
WebhookBatchModule,
|
|
22
|
+
WebhookBatchObserver,
|
|
23
|
+
BATCH_EVENT,
|
|
24
|
+
type WebhookBatchModuleOptions,
|
|
25
|
+
} from '@nest-batch/webhook';
|
|
26
|
+
```
|
|
77
27
|
|
|
78
28
|
## Wiring
|
|
79
29
|
|
|
80
30
|
```ts
|
|
81
|
-
import { Module } from '@nestjs/common';
|
|
82
|
-
import { NestBatchModule } from '@nest-batch/core';
|
|
83
|
-
import { BullmqAdapter } from '@nest-batch/bullmq';
|
|
84
31
|
import { WebhookBatchModule } from '@nest-batch/webhook';
|
|
85
32
|
|
|
86
33
|
@Module({
|
|
87
34
|
imports: [
|
|
88
|
-
NestBatchModule.forRoot({
|
|
89
|
-
// ... your persistence + transport adapters
|
|
90
|
-
}),
|
|
91
|
-
BullmqAdapter.forRoot({
|
|
92
|
-
connection: { host: process.env.REDIS_HOST, port: 6379 },
|
|
93
|
-
autoStartWorker: true,
|
|
94
|
-
}),
|
|
35
|
+
NestBatchModule.forRoot({ adapters }),
|
|
95
36
|
WebhookBatchModule.forRoot({
|
|
96
|
-
secret: process.env.WEBHOOK_HMAC_SECRET,
|
|
97
|
-
urls: [
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
],
|
|
37
|
+
secret: process.env.WEBHOOK_HMAC_SECRET,
|
|
38
|
+
urls: ['https://hooks.example.com/nest-batch'],
|
|
39
|
+
events: [BATCH_EVENT.JOB_COMPLETED, BATCH_EVENT.JOB_FAILED],
|
|
40
|
+
timeoutMs: 10_000,
|
|
101
41
|
}),
|
|
102
42
|
],
|
|
103
43
|
})
|
|
104
44
|
export class AppModule {}
|
|
105
45
|
```
|
|
106
46
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
| `secret` | `string` | required (or via env) | — | Host-injected HMAC-SHA256 secret. 32+ bytes of randomness recommended. |
|
|
112
|
-
| `urls` | `string[]` | yes | — | One or more absolute URLs the observer fans out to on every event. |
|
|
113
|
-
| `events` | `BatchEventType[]` | no | `[JOB_COMPLETED, JOB_FAILED, STEP_FAILED]` | Subscription filter. Events not in the set are dropped silently. |
|
|
114
|
-
| `attempts` | `number` | no | `4` | Total POST attempts. Clamped to `[1, 4]`. `1` = no retries. |
|
|
115
|
-
| `timeoutMs` | `number` | no | `10_000` | Per-attempt HTTP timeout in ms. A timeout is treated as a network error. |
|
|
116
|
-
| `logger` | `WebhookLogger` | no | `new Logger('WebhookBatchObserver')` | Nest-`Logger`-compatible surface for the dead-letter `warn` line. |
|
|
117
|
-
|
|
118
|
-
`WebhookBatchModule` is registered as `global: true` (matching
|
|
119
|
-
`NestBatchModule` and the transport adapters) so consumers do
|
|
120
|
-
not need to re-import it in every sub-module. The observer is
|
|
121
|
-
auto-registered against the `BatchObserver` token, so the
|
|
122
|
-
executor / runtime services pick it up via the
|
|
123
|
-
`@Optional() observer: BatchObserver = new NoopBatchObserver()`
|
|
124
|
-
injection path without any extra wiring.
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
## Events
|
|
129
|
-
|
|
130
|
-
The v1 subscription default is three events. A `BatchEvent`
|
|
131
|
-
whose `type` is not in the set is dropped silently — the
|
|
132
|
-
observer never holds the executor back.
|
|
133
|
-
|
|
134
|
-
| Event | Constant | When it fires |
|
|
135
|
-
| ---------------------------------- | ------------------------------ | -------------------------------------------- |
|
|
136
|
-
| `nest-batch.job.completed` | `BATCH_EVENT.JOB_COMPLETED` | A `JobExecution` reached the `COMPLETED` terminal state. |
|
|
137
|
-
| `nest-batch.job.failed` | `BATCH_EVENT.JOB_FAILED` | A `JobExecution` reached the `FAILED` terminal state. |
|
|
138
|
-
| `nest-batch.step.failed` | `BATCH_EVENT.STEP_FAILED` | A `StepExecution` reached the `FAILED` terminal state. |
|
|
139
|
-
|
|
140
|
-
Override the default via the `events` option:
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
WebhookBatchModule.forRoot({
|
|
144
|
-
secret: process.env.WEBHOOK_HMAC_SECRET,
|
|
145
|
-
urls: ['https://hooks.example.com/nest-batch'],
|
|
146
|
-
events: [BATCH_EVENT.JOB_COMPLETED, BATCH_EVENT.JOB_FAILED],
|
|
147
|
-
});
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
A future v2 may widen the default to include `STEP_*` /
|
|
151
|
-
`CHUNK_*` / `ITEM_*` events; in v1 only the three
|
|
152
|
-
terminal-state events trigger a POST.
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## Retry policy
|
|
157
|
-
|
|
158
|
-
The fixed backoff schedule is `[1s, 5s, 25s, 125s]` — four
|
|
159
|
-
attempts total (one initial POST plus three retries). The
|
|
160
|
-
schedule is the v1 contract; the test suite
|
|
161
|
-
([`tests/webhook-observer.test.ts`](./tests/webhook-observer.test.ts),
|
|
162
|
-
T-AC-5) asserts against it.
|
|
163
|
-
|
|
164
|
-
| Outcome | Behavior |
|
|
165
|
-
| --------------- | --------------------------------------------------------------------------------------- |
|
|
166
|
-
| `2xx` | Success. The observer marks the delivery done. |
|
|
167
|
-
| `3xx` | Treated as a redirect. The observer follows the redirect once; if the redirect target returns 4xx / 5xx, that target's status drives the retry decision. |
|
|
168
|
-
| `4xx` | **No retry.** Logged at `warn` level with the URL and the response body. The host is expected to fix the misconfiguration (bad URL, missing auth, malformed payload). |
|
|
169
|
-
| `5xx` | **Retried** through the full 4-attempt budget at the 1s / 5s / 25s / 125s schedule. After the final attempt, dead-letter. |
|
|
170
|
-
| Network error | **Retried** through the full 4-attempt budget. After the final attempt, dead-letter. |
|
|
171
|
-
| Timeout | Treated as a network error. **Retried** through the full 4-attempt budget. |
|
|
172
|
-
|
|
173
|
-
### Fast-mode test override
|
|
174
|
-
|
|
175
|
-
The retry schedule can be overridden to `[1ms, 5ms, 25ms, 125ms]`
|
|
176
|
-
by setting the `WEBHOOK_TEST_FAST=1` env var. The override lets
|
|
177
|
-
the test suite exercise the 4-attempt retry path in <200ms
|
|
178
|
-
instead of the 156-second production schedule. The override is
|
|
179
|
-
gated behind an env var so production cannot trip it by
|
|
180
|
-
accident.
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
WEBHOOK_TEST_FAST=1 pnpm --filter @nest-batch/webhook test -- tests/webhook-observer.test.ts
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Dead-letter
|
|
187
|
-
|
|
188
|
-
After the final failed attempt (4xx not retried, or 5xx /
|
|
189
|
-
network / timeout after all 4 attempts), the observer emits:
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
logger.warn(
|
|
193
|
-
`[WebhookBatchObserver] dead-letter url=${url} attempts=${attempts} ` +
|
|
194
|
-
`lastStatus=${lastStatus ?? 'n/a'} lastError=${lastError} ` +
|
|
195
|
-
`type=${event.type} jobExecutionId=${event.jobExecutionId} ` +
|
|
196
|
-
`secret_sha256=${fingerprint}`,
|
|
197
|
-
);
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
The dead-letter is a log line, not a database row. The host
|
|
201
|
-
ships its log aggregator (Datadog, CloudWatch, Loki, ...)
|
|
202
|
-
to recover dead-lettered URLs. A future v2 may add a
|
|
203
|
-
`DeadLetterStore` token; v1 ships the log line only.
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## HMAC signature
|
|
208
|
-
|
|
209
|
-
Every outbound POST carries a Stripe-style signature header:
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
X-Nest-Batch-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Where:
|
|
216
|
-
|
|
217
|
-
- `t=<unix>` is the unix-seconds timestamp the signature is
|
|
218
|
-
pinned to. The receiver uses it to enforce a replay window
|
|
219
|
-
(recommended: 5 minutes). The timestamp is also sent as a
|
|
220
|
-
separate `X-Nest-Batch-Timestamp: <unix>` header for
|
|
221
|
-
receivers that prefer parsing HTTP headers to parsing the
|
|
222
|
-
signature header.
|
|
223
|
-
- `v1=<hex>` is the lowercase hex of
|
|
224
|
-
`HMAC_SHA256(secret, "<unix>.<raw-body>")`.
|
|
225
|
-
- The `<raw-body>` is the EXACT JSON-serialized request body
|
|
226
|
-
bytes (not a re-serialization). The receiver MUST HMAC the
|
|
227
|
-
body it received, byte-for-byte; the observer does not
|
|
228
|
-
re-serialize, so a different key order on the receiver
|
|
229
|
-
side will fail verification.
|
|
230
|
-
|
|
231
|
-
### Receiver-side verification
|
|
232
|
-
|
|
233
|
-
The package exports `verifyV1` / `parseSignatureHeader` from
|
|
234
|
-
`@nest-batch/webhook` so a Node receiver can verify the
|
|
235
|
-
signature without re-implementing the crypto:
|
|
236
|
-
|
|
237
|
-
```ts
|
|
238
|
-
import {
|
|
239
|
-
parseSignatureHeader,
|
|
240
|
-
verifyV1,
|
|
241
|
-
SIGNATURE_HEADER_NAME,
|
|
242
|
-
} from '@nest-batch/webhook';
|
|
243
|
-
|
|
244
|
-
app.post('/hook', (req, res) => {
|
|
245
|
-
const header = String(req.headers[SIGNATURE_HEADER_NAME.toLowerCase()]);
|
|
246
|
-
const raw = req.rawBody; // capture in your body parser
|
|
247
|
-
const { timestamp, v1 } = parseSignatureHeader(header);
|
|
248
|
-
if (!verifyV1(process.env.WEBHOOK_HMAC_SECRET, timestamp, raw, v1)) {
|
|
249
|
-
return res.status(401).json({ error: 'invalid signature' });
|
|
250
|
-
}
|
|
251
|
-
// ... accept the envelope
|
|
252
|
-
res.status(200).json({ ok: true });
|
|
253
|
-
});
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
Express users: mount `express.raw({ type: 'application/json' })`
|
|
257
|
-
on the webhook route so `req.body` is a `Buffer` (not the
|
|
258
|
-
default JSON-parsed object). The v1 signature is over the
|
|
259
|
-
**raw bytes**, not the parsed JSON. The example above
|
|
260
|
-
assumes the host mounted a raw-body parser that stashes the
|
|
261
|
-
bytes on `req.rawBody`.
|
|
262
|
-
|
|
263
|
-
### Why v1?
|
|
264
|
-
|
|
265
|
-
The `v1` key is the signature-scheme version, not the
|
|
266
|
-
envelope version. A future v2 may add `v2=`-prefixed schemes
|
|
267
|
-
(e.g. a SHA-512 variant or a multi-rotation scheme);
|
|
268
|
-
receivers MUST reject unknown `vN` keys. The v1 contract is
|
|
269
|
-
HMAC-SHA256 over `<unix>.<raw-body>`, lowercase hex, single
|
|
270
|
-
field.
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## Secret handling
|
|
275
|
-
|
|
276
|
-
The HMAC secret is the single most sensitive value the package
|
|
277
|
-
handles. The contract:
|
|
278
|
-
|
|
279
|
-
- **Host-injection is primary.** Pass `secret` to `forRoot({...})`
|
|
280
|
-
at module-build time. The package binds it to the
|
|
281
|
-
`WebhookBatchObserver` instance via `WEBHOOK_MODULE_OPTIONS`
|
|
282
|
-
(a private `Symbol.for` token); it is not exported, not
|
|
283
|
-
injectable, not reachable from any public API.
|
|
284
|
-
- **Env fallback is secondary.** If `secret` is omitted,
|
|
285
|
-
`forRoot({...})` falls back to `process.env.WEBHOOK_HMAC_SECRET`.
|
|
286
|
-
Env is the safety net for hosts that do not want to thread
|
|
287
|
-
the secret through their config service explicitly. The
|
|
288
|
-
host's `ConfigModule` should set the env var from the
|
|
289
|
-
secret manager.
|
|
290
|
-
- **Neither is the secret on disk.** The package does not read
|
|
291
|
-
a file, does not read a CLI arg, does not read a Vault path.
|
|
292
|
-
The host owns the secret source.
|
|
293
|
-
- **The secret is never logged.** The dead-letter line emits a
|
|
294
|
-
SHA-256 fingerprint (`secret_sha256=abc123...`, first 12 hex
|
|
295
|
-
chars) so operators can correlate dead-letters across
|
|
296
|
-
services without exposing the secret. The full secret value
|
|
297
|
-
is never written to a `logger` call, never serialized into a
|
|
298
|
-
dead-letter body, never returned by any public API, never
|
|
299
|
-
echoed in a stack trace.
|
|
300
|
-
|
|
301
|
-
The pinned acceptance test (T-AC-5) asserts no
|
|
302
|
-
`WEBHOOK_HMAC_SECRET` substring (or, equivalently, the
|
|
303
|
-
`secret` value the test injected) appears in the captured log
|
|
304
|
-
stream across the full 4-attempt retry budget, the 4xx
|
|
305
|
-
dead-letter path, the 5xx dead-letter path, the success path,
|
|
306
|
-
and the debug path.
|
|
307
|
-
|
|
308
|
-
---
|
|
309
|
-
|
|
310
|
-
## Launcher-only deployment — events do NOT fire without a worker
|
|
311
|
-
|
|
312
|
-
`@nest-batch/webhook` is a `BatchObserver` — it consumes the
|
|
313
|
-
event stream the executor / runtime services produce. The
|
|
314
|
-
event stream only fires when the host has wired a transport
|
|
315
|
-
**with** a running consumer:
|
|
316
|
-
|
|
317
|
-
- `BullmqAdapter.forRoot({ autoStartWorker: true })` — events
|
|
318
|
-
fire (the worker drives the lifecycle and the
|
|
319
|
-
`QueueEvents` bridge fans them out).
|
|
320
|
-
- `BullmqAdapter.forRoot({ autoStartWorker: false })` — **no
|
|
321
|
-
events fire.** The launcher enqueues; no worker consumes;
|
|
322
|
-
the lifecycle never reaches `COMPLETED` / `FAILED`; the
|
|
323
|
-
observer never sees anything.
|
|
324
|
-
- `InProcessAdapter.forRoot()` — events fire (the strategy
|
|
325
|
-
runs the lifecycle in-process).
|
|
326
|
-
- `KafkaAdapter.forRoot({ autoStartConsumer: true })` — events
|
|
327
|
-
fire (the consumer drives the lifecycle and the consumer
|
|
328
|
-
bridge fans them out).
|
|
329
|
-
- `KafkaAdapter.forRoot({ autoStartConsumer: false })` — **no
|
|
330
|
-
events fire.** Same reason as the BullMQ launcher-only case.
|
|
331
|
-
|
|
332
|
-
This is the v1 contract. A launcher-only deployment (an API
|
|
333
|
-
service that only enqueues) does NOT need to install
|
|
334
|
-
`@nest-batch/webhook` — the observer would be dead code.
|
|
335
|
-
|
|
336
|
-
---
|
|
337
|
-
|
|
338
|
-
## What is NOT in this package
|
|
339
|
-
|
|
340
|
-
- A persistence adapter. Use `@nest-batch/mikro-orm`,
|
|
341
|
-
`@nest-batch/typeorm`, `@nest-batch/drizzle`, or
|
|
342
|
-
`@nest-batch/prisma` to wire a `JobRepository`. The observer
|
|
343
|
-
is event-stream-only; it does not read or write any
|
|
344
|
-
`JobExecution` rows.
|
|
345
|
-
- A batch engine. Job / Step / Chunk / Tasklet semantics,
|
|
346
|
-
checkpoint, restart, skip, business retry, and the event
|
|
347
|
-
stream itself live in
|
|
348
|
-
[`@nest-batch/core`](../core). The observer is the
|
|
349
|
-
downstream consumer.
|
|
350
|
-
- A transport. Use `@nest-batch/bullmq` or
|
|
351
|
-
`@nest-batch/kafka` to drive the lifecycle. The observer
|
|
352
|
-
does not enqueue or consume.
|
|
353
|
-
- A retry-policy module. The fixed `[1s, 5s, 25s, 125s]`
|
|
354
|
-
backoff is the v1 contract. A future v2 may add a
|
|
355
|
-
`retryDelaysMs` override; v1 ships the fixed schedule only.
|
|
356
|
-
- A dead-letter database. The dead-letter is a `logger.warn`
|
|
357
|
-
line; a future v2 may add a `DeadLetterStore` token.
|
|
358
|
-
- A scheduler. Cron-style scheduling lives in
|
|
359
|
-
`@nest-batch/core`'s `@BatchScheduled` decorator. The
|
|
360
|
-
observer does not fire on a timer; it fires on the
|
|
361
|
-
executor's event stream.
|
|
362
|
-
- An admin UI, metrics backend, or tracing backend. Hook a
|
|
363
|
-
different `BatchObserver` (or extend this one) to ship
|
|
364
|
-
events where you need them.
|
|
365
|
-
- Alternative HTTP transports (e.g. webhook-over-mqtt, gRPC
|
|
366
|
-
webhooks). The observer uses HTTP POST + HMAC-SHA256. A
|
|
367
|
-
future sibling package could ship a webhook-over-mqtt
|
|
368
|
-
observer that implements the same `BatchObserver`
|
|
369
|
-
contract.
|
|
370
|
-
|
|
371
|
-
---
|
|
372
|
-
|
|
373
|
-
## Scripts
|
|
374
|
-
|
|
375
|
-
```bash
|
|
376
|
-
pnpm --filter @nest-batch/webhook build # SWC transpile + tsc declarations
|
|
377
|
-
pnpm --filter @nest-batch/webhook test # vitest run (T-AC-5; see env note below)
|
|
378
|
-
pnpm --filter @nest-batch/webhook test:watch # vitest watch
|
|
379
|
-
pnpm --filter @nest-batch/webhook typecheck # tsc --noEmit
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
The T-AC-5 test
|
|
383
|
-
([`tests/webhook-observer.test.ts`](./tests/webhook-observer.test.ts))
|
|
384
|
-
uses `WEBHOOK_TEST_FAST=1` to override the retry schedule to
|
|
385
|
-
milliseconds. Run it with:
|
|
386
|
-
|
|
387
|
-
```bash
|
|
388
|
-
WEBHOOK_TEST_FAST=1 pnpm --filter @nest-batch/webhook test -- tests/webhook-observer.test.ts --reporter=verbose
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
The full `pnpm test` run uses the same env var (set at the
|
|
392
|
-
top of the test file) so the suite finishes in <1s. The test
|
|
393
|
-
stands up a real `http.createServer().listen(0)` server on a
|
|
394
|
-
random port; no external service (Redis, Postgres) is
|
|
395
|
-
required.
|
|
47
|
+
The default subscription set is job completed, job failed, and step failed.
|
|
48
|
+
HTTP 4xx responses are treated as configuration failures and are not retried.
|
|
49
|
+
HTTP 5xx responses, network errors, and timeouts are retried with bounded
|
|
50
|
+
backoff.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nest-batch/webhook",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Webhook delivery observer for @nest-batch/core. Subscribes to BATCH_EVENT.* and POSTs HMAC-SHA256-signed JSON envelopes to one or more URLs with exponential-backoff retry and dead-letter logging. Uses native fetch (Node 20+); no HTTP client peer dep.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "easdkr",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist/src",
|
|
29
29
|
"src",
|
|
30
|
-
"README.md"
|
|
30
|
+
"README.md",
|
|
31
|
+
"README.ko.md"
|
|
31
32
|
],
|
|
32
33
|
"publishConfig": {
|
|
33
34
|
"access": "public"
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"@nestjs/common": "^10 || ^11",
|
|
37
38
|
"@nestjs/core": "^10 || ^11",
|
|
38
|
-
"@nest-batch/core": "^0.2.
|
|
39
|
+
"@nest-batch/core": "^0.2.4"
|
|
39
40
|
},
|
|
40
41
|
"peerDependenciesMeta": {
|
|
41
42
|
"@nest-batch/core": {
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
"typescript": "^5.5.0",
|
|
59
60
|
"unplugin-swc": "^1.5.0",
|
|
60
61
|
"vitest": "^2.0.0",
|
|
61
|
-
"@nest-batch/core": "0.2.
|
|
62
|
+
"@nest-batch/core": "0.2.4"
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
65
|
"build": "swc src -d dist --config-file ../../.swcrc && tsc --emitDeclarationOnly -p tsconfig.build.json",
|