outbox-event-bus 2.0.1 → 2.0.3
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 +146 -148
- package/dist/index.cjs +3 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -4
- package/dist/index.mjs.map +1 -1
- package/docs/API_REFERENCE.md +3 -19
- package/docs/PUBLISHING.md +6 -6
- package/package.json +2 -2
- package/src/bus/outbox-event-bus.ts +2 -4
- package/src/in-memory-outbox/in-memory-outbox.ts +1 -1
- package/src/outboxes/in-memory/in-memory-outbox.ts +1 -1
- package/src/utils/promise-utils.ts +1 -2
package/README.md
CHANGED
|
@@ -34,13 +34,13 @@ npm install outbox-event-bus @outbox-event-bus/postgres-drizzle-outbox drizzle-o
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
```typescript
|
|
37
|
-
import { OutboxEventBus } from 'outbox-event-bus'
|
|
38
|
-
import { PostgresDrizzleOutbox } from '@outbox-event-bus/postgres-drizzle-outbox'
|
|
39
|
-
import { SQSPublisher } from '@outbox-event-bus/sqs-publisher'
|
|
37
|
+
import { OutboxEventBus } from 'outbox-event-bus'
|
|
38
|
+
import { PostgresDrizzleOutbox } from '@outbox-event-bus/postgres-drizzle-outbox'
|
|
39
|
+
import { SQSPublisher } from '@outbox-event-bus/sqs-publisher'
|
|
40
40
|
|
|
41
41
|
// Setup
|
|
42
|
-
const outbox = new PostgresDrizzleOutbox({ db })
|
|
43
|
-
const bus = new OutboxEventBus(outbox, (error: OutboxError) => console.error(error))
|
|
42
|
+
const outbox = new PostgresDrizzleOutbox({ db })
|
|
43
|
+
const bus = new OutboxEventBus(outbox, (error: OutboxError) => console.error(error))
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
// Register handlers
|
|
@@ -49,34 +49,34 @@ bus.on('user.created', async (event) => {
|
|
|
49
49
|
await bus.emitMany([
|
|
50
50
|
{ type: 'send.welcome', payload: event.payload },
|
|
51
51
|
{ type: 'sync-to-sqs', payload: event.payload }
|
|
52
|
-
])
|
|
53
|
-
})
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
54
|
|
|
55
55
|
bus.on('send.welcome', async (event) => {
|
|
56
|
-
await emailService.sendWelcome(event.payload.email)
|
|
57
|
-
})
|
|
56
|
+
await emailService.sendWelcome(event.payload.email)
|
|
57
|
+
})
|
|
58
58
|
|
|
59
59
|
// Middleware
|
|
60
60
|
bus.addHandlerMiddleware(async (ctx, next) => {
|
|
61
|
-
console.log('Processing:', ctx.event.type)
|
|
62
|
-
await next()
|
|
63
|
-
})
|
|
61
|
+
console.log('Processing:', ctx.event.type)
|
|
62
|
+
await next()
|
|
63
|
+
})
|
|
64
64
|
|
|
65
65
|
// Forward messages to SQS
|
|
66
|
-
const sqsClient = new SQSClient({ region: 'us-east-1' })
|
|
67
|
-
const publisher = new SQSPublisher(bus, { queueUrl: '...', sqsClient })
|
|
68
|
-
publisher.subscribe(['sync-to-sqs'])
|
|
66
|
+
const sqsClient = new SQSClient({ region: 'us-east-1' })
|
|
67
|
+
const publisher = new SQSPublisher(bus, { queueUrl: '...', sqsClient })
|
|
68
|
+
publisher.subscribe(['sync-to-sqs'])
|
|
69
69
|
|
|
70
70
|
// Start the bus
|
|
71
|
-
bus.start()
|
|
71
|
+
bus.start()
|
|
72
72
|
|
|
73
73
|
// Emit transactionally
|
|
74
74
|
await db.transaction(async (transaction) => {
|
|
75
|
-
const [user] = await transaction.insert(users).values(newUser).returning()
|
|
75
|
+
const [user] = await transaction.insert(users).values(newUser).returning()
|
|
76
76
|
|
|
77
77
|
// Both operations commit together or rollback together
|
|
78
|
-
await bus.emit({ type: 'user.created', payload: user }, transaction)
|
|
79
|
-
})
|
|
78
|
+
await bus.emit({ type: 'user.created', payload: user }, transaction)
|
|
79
|
+
})
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
<br>
|
|
@@ -127,17 +127,17 @@ bus.on('user.created', async (event) => {
|
|
|
127
127
|
{ type: 'send.welcome', payload: event.payload },
|
|
128
128
|
{ type: 'send.analytics', payload: event.payload },
|
|
129
129
|
{ type: 'sync-to-sqs', payload: event.payload }
|
|
130
|
-
])
|
|
131
|
-
})
|
|
130
|
+
])
|
|
131
|
+
})
|
|
132
132
|
|
|
133
133
|
// Specialized handlers (1:1)
|
|
134
134
|
bus.on('send.welcome', async (event) => {
|
|
135
|
-
await emailService.sendWelcome(event.payload.email)
|
|
136
|
-
})
|
|
135
|
+
await emailService.sendWelcome(event.payload.email)
|
|
136
|
+
})
|
|
137
137
|
|
|
138
138
|
bus.on('send.analytics', async (event) => {
|
|
139
|
-
await analyticsService.track(event.payload)
|
|
140
|
-
})
|
|
139
|
+
await analyticsService.track(event.payload)
|
|
140
|
+
})
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
### Event Lifecycle
|
|
@@ -170,7 +170,7 @@ If a worker crashes while processing an event (status: `active`), the event beco
|
|
|
170
170
|
-- Events stuck for more than 30 seconds are reclaimed
|
|
171
171
|
SELECT * FROM outbox_events
|
|
172
172
|
WHERE status = 'active'
|
|
173
|
-
AND keep_alive < NOW() - INTERVAL '30 seconds'
|
|
173
|
+
AND keep_alive < NOW() - INTERVAL '30 seconds'
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
> **Note:** Non-SQL adapters (DynamoDB, Redis, Mongo) implement equivalent recovery mechanisms using their native features (TTL, Sorted Sets, etc).
|
|
@@ -189,15 +189,15 @@ Each `add*` method accepts one or more middleware functions.
|
|
|
189
189
|
|
|
190
190
|
```typescript
|
|
191
191
|
bus.addMiddleware(async (ctx, next) => {
|
|
192
|
-
const prefix = ctx.phase === 'emit' ? '[emit]' : '[handler]'
|
|
193
|
-
console.log(`${prefix} Processing event: ${ctx.event.type}`)
|
|
192
|
+
const prefix = ctx.phase === 'emit' ? '[emit]' : '[handler]'
|
|
193
|
+
console.log(`${prefix} Processing event: ${ctx.event.type}`)
|
|
194
194
|
|
|
195
|
-
const start = Date.now()
|
|
196
|
-
await next()
|
|
197
|
-
const duration = Date.now() - start
|
|
195
|
+
const start = Date.now()
|
|
196
|
+
await next()
|
|
197
|
+
const duration = Date.now() - start
|
|
198
198
|
|
|
199
|
-
console.log(`${prefix} Completed in ${duration}ms`)
|
|
200
|
-
})
|
|
199
|
+
console.log(`${prefix} Completed in ${duration}ms`)
|
|
200
|
+
})
|
|
201
201
|
```
|
|
202
202
|
|
|
203
203
|
### Middleware Phases
|
|
@@ -220,9 +220,9 @@ bus.addEmitMiddleware(async (ctx, next) => {
|
|
|
220
220
|
ctx.event.metadata = {
|
|
221
221
|
...ctx.event.metadata,
|
|
222
222
|
correlationId: ctx.event.metadata?.correlationId || crypto.randomUUID()
|
|
223
|
-
}
|
|
224
|
-
await next()
|
|
225
|
-
})
|
|
223
|
+
}
|
|
224
|
+
await next()
|
|
225
|
+
})
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
### Filtering Events
|
|
@@ -232,11 +232,11 @@ You can explicitly drop an event by passing `{ dropEvent: true }` to `next()`. T
|
|
|
232
232
|
```typescript
|
|
233
233
|
bus.addEmitMiddleware(async (ctx, next) => {
|
|
234
234
|
if (ctx.event.type === 'sensitive.data') {
|
|
235
|
-
await next({ dropEvent: true })
|
|
236
|
-
return
|
|
235
|
+
await next({ dropEvent: true })
|
|
236
|
+
return
|
|
237
237
|
}
|
|
238
|
-
await next()
|
|
239
|
-
})
|
|
238
|
+
await next()
|
|
239
|
+
})
|
|
240
240
|
```
|
|
241
241
|
|
|
242
242
|
<br>
|
|
@@ -251,15 +251,13 @@ These store your events. Choose one that matches your primary database.
|
|
|
251
251
|
|
|
252
252
|
| Database | Adapters | Transaction Support | Concurrency |
|
|
253
253
|
|:---|:---|:---:|:---|
|
|
254
|
-
| **Postgres** | [Prisma](
|
|
255
|
-
| **MongoDB** | [Native Driver](
|
|
256
|
-
| **DynamoDB** | [AWS SDK](
|
|
257
|
-
| **Redis** | [ioredis](
|
|
258
|
-
| **SQLite** | [better-sqlite3](
|
|
254
|
+
| **Postgres** | [Prisma](https://github.com/dunika/outbox-event-bus/tree/main/packages/adapters/postgres-prisma), [Drizzle](https://github.com/dunika/outbox-event-bus/tree/main/packages/adapters/postgres-drizzle) | Full (ACID) | `SKIP LOCKED` |
|
|
255
|
+
| **MongoDB** | [Native Driver](https://github.com/dunika/outbox-event-bus/tree/main/packages/adapters/mongo-mongodb) | Full (Replica Set) | Optimistic Locking |
|
|
256
|
+
| **DynamoDB** | [AWS SDK](https://github.com/dunika/outbox-event-bus/tree/main/packages/adapters/dynamodb-aws-sdk) | Full (TransactWrite) | Optimistic Locking |
|
|
257
|
+
| **Redis** | [ioredis](https://github.com/dunika/outbox-event-bus/tree/main/packages/adapters/redis-ioredis) | Atomic (Multi/Exec) | Distributed Lock |
|
|
258
|
+
| **SQLite** | [better-sqlite3](https://github.com/dunika/outbox-event-bus/tree/main/packages/adapters/sqlite-better-sqlite3) | Full (ACID) | Serialized |
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
- **SKIP LOCKED**: High-performance non-blocking reads for multiple workers
|
|
262
|
-
- **Limited**: Single-document transactions or optimistic locking
|
|
260
|
+
<br>
|
|
263
261
|
|
|
264
262
|
### Publishers
|
|
265
263
|
|
|
@@ -267,12 +265,12 @@ These send your events to the world.
|
|
|
267
265
|
|
|
268
266
|
| Publisher | Target | Batching | Package |
|
|
269
267
|
|:---|:---|:---:|:---|
|
|
270
|
-
| **[AWS SQS](
|
|
271
|
-
| **[AWS SNS](
|
|
272
|
-
| **[EventBridge](
|
|
273
|
-
| **[RabbitMQ](
|
|
274
|
-
| **[Kafka](
|
|
275
|
-
| **[Redis Streams](
|
|
268
|
+
| **[AWS SQS](https://github.com/dunika/outbox-event-bus/tree/main/packages/publishers/sqs)** | Amazon SQS Queues | Yes (10) | `@outbox-event-bus/sqs-publisher` |
|
|
269
|
+
| **[AWS SNS](https://github.com/dunika/outbox-event-bus/tree/main/packages/publishers/sns)** | Amazon SNS Topics | Yes (10) | `@outbox-event-bus/sns-publisher` |
|
|
270
|
+
| **[EventBridge](https://github.com/dunika/outbox-event-bus/tree/main/packages/publishers/eventbridge)** | AWS Event Bus | Yes (10) | `@outbox-event-bus/eventbridge-publisher` |
|
|
271
|
+
| **[RabbitMQ](https://github.com/dunika/outbox-event-bus/tree/main/packages/publishers/rabbitmq)** | AMQP Brokers | Yes (Configurable) | `@outbox-event-bus/rabbitmq-publisher` |
|
|
272
|
+
| **[Kafka](https://github.com/dunika/outbox-event-bus/tree/main/packages/publishers/kafka)** | Streaming | Yes (Configurable) | `@outbox-event-bus/kafka-publisher` |
|
|
273
|
+
| **[Redis Streams](https://github.com/dunika/outbox-event-bus/tree/main/packages/publishers/redis-streams)** | Lightweight Stream | Yes (Configurable) | `@outbox-event-bus/redis-streams-publisher` |
|
|
276
274
|
|
|
277
275
|
<br>
|
|
278
276
|
|
|
@@ -290,30 +288,30 @@ These send your events to the world.
|
|
|
290
288
|
<br>
|
|
291
289
|
|
|
292
290
|
```typescript
|
|
293
|
-
import { PrismaClient } from '@prisma/client'
|
|
294
|
-
import { PostgresPrismaOutbox } from '@outbox-event-bus/postgres-prisma-outbox'
|
|
295
|
-
import { OutboxEventBus } from 'outbox-event-bus'
|
|
291
|
+
import { PrismaClient } from '@prisma/client'
|
|
292
|
+
import { PostgresPrismaOutbox } from '@outbox-event-bus/postgres-prisma-outbox'
|
|
293
|
+
import { OutboxEventBus } from 'outbox-event-bus'
|
|
296
294
|
|
|
297
|
-
const prisma = new PrismaClient()
|
|
298
|
-
const outbox = new PostgresPrismaOutbox({ prisma })
|
|
299
|
-
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
295
|
+
const prisma = new PrismaClient()
|
|
296
|
+
const outbox = new PostgresPrismaOutbox({ prisma })
|
|
297
|
+
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
300
298
|
|
|
301
|
-
bus.start()
|
|
299
|
+
bus.start()
|
|
302
300
|
|
|
303
301
|
// Register handler
|
|
304
302
|
bus.on('user.created', async (event) => {
|
|
305
|
-
await emailService.sendWelcome(event.payload.email)
|
|
306
|
-
})
|
|
303
|
+
await emailService.sendWelcome(event.payload.email)
|
|
304
|
+
})
|
|
307
305
|
|
|
308
306
|
// Emit within a transaction
|
|
309
307
|
async function createUser(userData: any) {
|
|
310
308
|
await prisma.$transaction(async (transaction) => {
|
|
311
309
|
// 1. Create user
|
|
312
|
-
const user = await transaction.user.create({ data: userData })
|
|
310
|
+
const user = await transaction.user.create({ data: userData })
|
|
313
311
|
|
|
314
312
|
// 2. Emit event (will commit with the user creation)
|
|
315
|
-
await bus.emit({ type: 'user.created', payload: user }, transaction)
|
|
316
|
-
})
|
|
313
|
+
await bus.emit({ type: 'user.created', payload: user }, transaction)
|
|
314
|
+
})
|
|
317
315
|
}
|
|
318
316
|
```
|
|
319
317
|
|
|
@@ -322,31 +320,31 @@ async function createUser(userData: any) {
|
|
|
322
320
|
SQLite transactions are synchronous by default. To use `await` with the event bus, use the `withBetterSqlite3Transaction` helper which manages the transaction scope for you.
|
|
323
321
|
|
|
324
322
|
```typescript
|
|
325
|
-
import Database from 'better-sqlite3'
|
|
326
|
-
import { SqliteBetterSqlite3Outbox, withBetterSqlite3Transaction, getBetterSqlite3Transaction } from '@outbox-event-bus/sqlite-better-sqlite3-outbox'
|
|
327
|
-
import { OutboxEventBus } from 'outbox-event-bus'
|
|
323
|
+
import Database from 'better-sqlite3'
|
|
324
|
+
import { SqliteBetterSqlite3Outbox, withBetterSqlite3Transaction, getBetterSqlite3Transaction } from '@outbox-event-bus/sqlite-better-sqlite3-outbox'
|
|
325
|
+
import { OutboxEventBus } from 'outbox-event-bus'
|
|
328
326
|
|
|
329
|
-
const db = new Database('app.db')
|
|
327
|
+
const db = new Database('app.db')
|
|
330
328
|
const outbox = new SqliteBetterSqlite3Outbox({
|
|
331
329
|
dbPath: 'app.db',
|
|
332
330
|
getTransaction: getBetterSqlite3Transaction()
|
|
333
|
-
})
|
|
334
|
-
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
331
|
+
})
|
|
332
|
+
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
335
333
|
|
|
336
|
-
bus.start()
|
|
334
|
+
bus.start()
|
|
337
335
|
|
|
338
336
|
async function createUser(userData: any) {
|
|
339
337
|
return withBetterSqlite3Transaction(db, async (transaction) => {
|
|
340
|
-
const stmt = transaction.prepare('INSERT INTO users (name) VALUES (?)')
|
|
341
|
-
const info = stmt.run(userData.name)
|
|
338
|
+
const stmt = transaction.prepare('INSERT INTO users (name) VALUES (?)')
|
|
339
|
+
const info = stmt.run(userData.name)
|
|
342
340
|
|
|
343
341
|
await bus.emit({
|
|
344
342
|
type: 'user.created',
|
|
345
343
|
payload: { id: info.lastInsertRowid, ...userData }
|
|
346
|
-
})
|
|
344
|
+
})
|
|
347
345
|
|
|
348
|
-
return info
|
|
349
|
-
})
|
|
346
|
+
return info
|
|
347
|
+
})
|
|
350
348
|
}
|
|
351
349
|
```
|
|
352
350
|
|
|
@@ -357,14 +355,14 @@ async function createUser(userData: any) {
|
|
|
357
355
|
### Environment-Specific Adapters
|
|
358
356
|
|
|
359
357
|
```typescript
|
|
360
|
-
import { InMemoryOutbox } from 'outbox-event-bus'
|
|
361
|
-
import { PostgresPrismaOutbox } from '@outbox-event-bus/postgres-prisma-outbox'
|
|
358
|
+
import { InMemoryOutbox } from 'outbox-event-bus'
|
|
359
|
+
import { PostgresPrismaOutbox } from '@outbox-event-bus/postgres-prisma-outbox'
|
|
362
360
|
|
|
363
361
|
const outbox = process.env.NODE_ENV === 'production'
|
|
364
362
|
? new PostgresPrismaOutbox({ prisma })
|
|
365
|
-
: new InMemoryOutbox()
|
|
363
|
+
: new InMemoryOutbox()
|
|
366
364
|
|
|
367
|
-
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
365
|
+
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
368
366
|
```
|
|
369
367
|
|
|
370
368
|
### Testing Event Handlers
|
|
@@ -374,29 +372,29 @@ const bus = new OutboxEventBus(outbox, (error) => console.error(error));
|
|
|
374
372
|
**Solution:** Use `InMemoryOutbox` and `waitFor`:
|
|
375
373
|
|
|
376
374
|
```typescript
|
|
377
|
-
import { describe, it, expect } from 'vitest'
|
|
378
|
-
import { OutboxEventBus, InMemoryOutbox } from 'outbox-event-bus'
|
|
375
|
+
import { describe, it, expect } from 'vitest'
|
|
376
|
+
import { OutboxEventBus, InMemoryOutbox } from 'outbox-event-bus'
|
|
379
377
|
|
|
380
378
|
describe('User Creation', () => {
|
|
381
379
|
it('sends welcome email when user is created', async () => {
|
|
382
|
-
const outbox = new InMemoryOutbox()
|
|
383
|
-
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
380
|
+
const outbox = new InMemoryOutbox()
|
|
381
|
+
const bus = new OutboxEventBus(outbox, (error) => console.error(error))
|
|
384
382
|
|
|
385
|
-
let emailSent = false
|
|
383
|
+
let emailSent = false
|
|
386
384
|
bus.on('user.created', async (event) => {
|
|
387
|
-
emailSent = true
|
|
388
|
-
})
|
|
385
|
+
emailSent = true
|
|
386
|
+
})
|
|
389
387
|
|
|
390
|
-
bus.start()
|
|
388
|
+
bus.start()
|
|
391
389
|
|
|
392
|
-
await bus.emit({ type: 'user.created', payload: { email: 'test@example.com' } })
|
|
393
|
-
await bus.waitFor('user.created')
|
|
390
|
+
await bus.emit({ type: 'user.created', payload: { email: 'test@example.com' } })
|
|
391
|
+
await bus.waitFor('user.created')
|
|
394
392
|
|
|
395
|
-
expect(emailSent).toBe(true)
|
|
393
|
+
expect(emailSent).toBe(true)
|
|
396
394
|
|
|
397
|
-
await bus.stop()
|
|
398
|
-
})
|
|
399
|
-
})
|
|
395
|
+
await bus.stop()
|
|
396
|
+
})
|
|
397
|
+
})
|
|
400
398
|
```
|
|
401
399
|
|
|
402
400
|
### Error Handling
|
|
@@ -410,18 +408,18 @@ The library provides typed errors to help you handle specific failure scenarios
|
|
|
410
408
|
<br>
|
|
411
409
|
|
|
412
410
|
```typescript
|
|
413
|
-
import { OutboxEventBus, MaxRetriesExceededError } from 'outbox-event-bus'
|
|
411
|
+
import { OutboxEventBus, MaxRetriesExceededError } from 'outbox-event-bus'
|
|
414
412
|
|
|
415
413
|
// Simple initialization with error handler
|
|
416
414
|
const bus = new OutboxEventBus(outbox, (error: OutboxError) => {
|
|
417
|
-
console.error('Bus error:', error)
|
|
418
|
-
})
|
|
415
|
+
console.error('Bus error:', error)
|
|
416
|
+
})
|
|
419
417
|
|
|
420
418
|
// Advanced initialization with config object
|
|
421
419
|
const bus = new OutboxEventBus(outbox, {
|
|
422
420
|
onError: (error: OutboxError) => console.error(error),
|
|
423
421
|
middlewareConcurrency: 20 // Custom concurrency for middleware
|
|
424
|
-
})
|
|
422
|
+
})
|
|
425
423
|
```
|
|
426
424
|
|
|
427
425
|
For a complete list and usage examples, see the [API Reference](./docs/API_REFERENCE.md).
|
|
@@ -434,9 +432,9 @@ For a complete list and usage examples, see the [API Reference](./docs/API_REFER
|
|
|
434
432
|
|
|
435
433
|
**Using API:**
|
|
436
434
|
```typescript
|
|
437
|
-
const failedEvents = await bus.getFailedEvents()
|
|
438
|
-
const idsToRetry = failedEvents.map(e => e.id)
|
|
439
|
-
await bus.retryEvents(idsToRetry)
|
|
435
|
+
const failedEvents = await bus.getFailedEvents()
|
|
436
|
+
const idsToRetry = failedEvents.map(e => e.id)
|
|
437
|
+
await bus.retryEvents(idsToRetry)
|
|
440
438
|
```
|
|
441
439
|
|
|
442
440
|
**Using SQL:**
|
|
@@ -444,12 +442,12 @@ await bus.retryEvents(idsToRetry);
|
|
|
444
442
|
-- Reset a specific event
|
|
445
443
|
UPDATE outbox_events
|
|
446
444
|
SET status = 'created', retry_count = 0, last_error = NULL
|
|
447
|
-
WHERE id = 'event-id-here'
|
|
445
|
+
WHERE id = 'event-id-here'
|
|
448
446
|
|
|
449
447
|
-- Reset all failed events of a type
|
|
450
448
|
UPDATE outbox_events
|
|
451
449
|
SET status = 'created', retry_count = 0, last_error = NULL
|
|
452
|
-
WHERE status = 'failed' AND type = 'user.created'
|
|
450
|
+
WHERE status = 'failed' AND type = 'user.created'
|
|
453
451
|
```
|
|
454
452
|
|
|
455
453
|
### Schema Evolution
|
|
@@ -461,18 +459,18 @@ WHERE status = 'failed' AND type = 'user.created';
|
|
|
461
459
|
```typescript
|
|
462
460
|
// Old handler (still processes legacy events)
|
|
463
461
|
bus.on('user.created.v1', async (event) => {
|
|
464
|
-
const { firstName, lastName } = event.payload
|
|
465
|
-
await emailService.send({ name: `${firstName} ${lastName}` })
|
|
466
|
-
})
|
|
462
|
+
const { firstName, lastName } = event.payload
|
|
463
|
+
await emailService.send({ name: `${firstName} ${lastName}` })
|
|
464
|
+
})
|
|
467
465
|
|
|
468
466
|
// New handler (processes new events)
|
|
469
467
|
bus.on('user.created.v2', async (event) => {
|
|
470
|
-
const { fullName } = event.payload
|
|
471
|
-
await emailService.send({ name: fullName })
|
|
472
|
-
})
|
|
468
|
+
const { fullName } = event.payload
|
|
469
|
+
await emailService.send({ name: fullName })
|
|
470
|
+
})
|
|
473
471
|
|
|
474
472
|
// Emit new version
|
|
475
|
-
await bus.emit({ type: 'user.created.v2', payload: { fullName: 'John Doe' } })
|
|
473
|
+
await bus.emit({ type: 'user.created.v2', payload: { fullName: 'John Doe' } })
|
|
476
474
|
```
|
|
477
475
|
|
|
478
476
|
|
|
@@ -507,16 +505,16 @@ The `onError` callback captures unexpected errors and permanent failures.
|
|
|
507
505
|
|
|
508
506
|
```typescript
|
|
509
507
|
const bus = new OutboxEventBus(outbox, (error: OutboxError) => {
|
|
510
|
-
const event = error.context?.event
|
|
508
|
+
const event = error.context?.event
|
|
511
509
|
|
|
512
510
|
// Send to error tracking (e.g. Sentry)
|
|
513
511
|
if (error instanceof MaxRetriesExceededError) {
|
|
514
512
|
Sentry.captureException(error, {
|
|
515
513
|
tags: { eventType: event?.type, retryCount: error.retryCount },
|
|
516
514
|
extra: error.context
|
|
517
|
-
})
|
|
515
|
+
})
|
|
518
516
|
}
|
|
519
|
-
})
|
|
517
|
+
})
|
|
520
518
|
```
|
|
521
519
|
|
|
522
520
|
#### 2. Metrics (Prometheus Example)
|
|
@@ -524,32 +522,32 @@ const bus = new OutboxEventBus(outbox, (error: OutboxError) => {
|
|
|
524
522
|
Use middleware to track event counts and processing duration.
|
|
525
523
|
|
|
526
524
|
```typescript
|
|
527
|
-
import { Counter, Histogram } from 'prom-client'
|
|
525
|
+
import { Counter, Histogram } from 'prom-client'
|
|
528
526
|
|
|
529
527
|
const eventCounter = new Counter({
|
|
530
528
|
name: 'outbox_events_total',
|
|
531
529
|
help: 'Total events processed',
|
|
532
530
|
labelNames: ['type', 'phase', 'status']
|
|
533
|
-
})
|
|
531
|
+
})
|
|
534
532
|
|
|
535
533
|
const processingDuration = new Histogram({
|
|
536
534
|
name: 'outbox_event_duration_seconds',
|
|
537
535
|
help: 'Event processing duration',
|
|
538
536
|
labelNames: ['type']
|
|
539
|
-
})
|
|
537
|
+
})
|
|
540
538
|
|
|
541
539
|
bus.addMiddleware(async (ctx, next) => {
|
|
542
|
-
const start = Date.now()
|
|
540
|
+
const start = Date.now()
|
|
543
541
|
try {
|
|
544
|
-
await next()
|
|
545
|
-
eventCounter.inc({ type: ctx.event.type, phase: ctx.phase, status: 'success' })
|
|
546
|
-
} catch (
|
|
547
|
-
eventCounter.inc({ type: ctx.event.type, phase: ctx.phase, status: 'error' })
|
|
548
|
-
throw
|
|
542
|
+
await next()
|
|
543
|
+
eventCounter.inc({ type: ctx.event.type, phase: ctx.phase, status: 'success' })
|
|
544
|
+
} catch (error) {
|
|
545
|
+
eventCounter.inc({ type: ctx.event.type, phase: ctx.phase, status: 'error' })
|
|
546
|
+
throw error
|
|
549
547
|
} finally {
|
|
550
|
-
processingDuration.observe({ type: ctx.event.type }, (Date.now() - start) / 1000)
|
|
548
|
+
processingDuration.observe({ type: ctx.event.type }, (Date.now() - start) / 1000)
|
|
551
549
|
}
|
|
552
|
-
})
|
|
550
|
+
})
|
|
553
551
|
```
|
|
554
552
|
|
|
555
553
|
#### 3. Tracing (OpenTelemetry Example)
|
|
@@ -557,9 +555,9 @@ bus.addMiddleware(async (ctx, next) => {
|
|
|
557
555
|
Use middleware to start spans for distributed tracing.
|
|
558
556
|
|
|
559
557
|
```typescript
|
|
560
|
-
import { trace } from '@opentelemetry/api'
|
|
558
|
+
import { trace } from '@opentelemetry/api'
|
|
561
559
|
|
|
562
|
-
const tracer = trace.getTracer('outbox-event-bus')
|
|
560
|
+
const tracer = trace.getTracer('outbox-event-bus')
|
|
563
561
|
|
|
564
562
|
bus.addMiddleware(async (ctx, next) => {
|
|
565
563
|
return tracer.startActiveSpan(`${ctx.phase} ${ctx.event.type}`, async (span) => {
|
|
@@ -568,20 +566,20 @@ bus.addMiddleware(async (ctx, next) => {
|
|
|
568
566
|
'messaging.destination': ctx.event.type,
|
|
569
567
|
'messaging.message_id': ctx.event.id,
|
|
570
568
|
'messaging.phase': ctx.phase
|
|
571
|
-
})
|
|
569
|
+
})
|
|
572
570
|
|
|
573
571
|
try {
|
|
574
|
-
await next()
|
|
575
|
-
span.setStatus({ code: 1 })
|
|
572
|
+
await next()
|
|
573
|
+
span.setStatus({ code: 1 }) // OK
|
|
576
574
|
} catch (err) {
|
|
577
|
-
span.recordException(err as Error)
|
|
578
|
-
span.setStatus({ code: 2 })
|
|
579
|
-
throw err
|
|
575
|
+
span.recordException(err as Error)
|
|
576
|
+
span.setStatus({ code: 2 }) // ERROR
|
|
577
|
+
throw err
|
|
580
578
|
} finally {
|
|
581
|
-
span.end()
|
|
579
|
+
span.end()
|
|
582
580
|
}
|
|
583
|
-
})
|
|
584
|
-
})
|
|
581
|
+
})
|
|
582
|
+
})
|
|
585
583
|
```
|
|
586
584
|
|
|
587
585
|
#### 4. Dead Letter Queue (DLQ)
|
|
@@ -590,12 +588,12 @@ Query the database for events that failed after all retries.
|
|
|
590
588
|
|
|
591
589
|
```typescript
|
|
592
590
|
// Get failed events
|
|
593
|
-
const failedEvents = await bus.getFailedEvents()
|
|
591
|
+
const failedEvents = await bus.getFailedEvents()
|
|
594
592
|
```
|
|
595
593
|
|
|
596
594
|
Or via SQL:
|
|
597
595
|
```sql
|
|
598
|
-
SELECT * FROM outbox_events WHERE status = 'failed'
|
|
596
|
+
SELECT * FROM outbox_events WHERE status = 'failed'
|
|
599
597
|
```
|
|
600
598
|
|
|
601
599
|
### Scaling
|
|
@@ -617,7 +615,7 @@ const outbox = new PostgresPrismaOutbox({
|
|
|
617
615
|
prisma,
|
|
618
616
|
batchSize: 100, // Process 100 events per poll
|
|
619
617
|
pollIntervalMs: 500 // Poll every 500ms
|
|
620
|
-
})
|
|
618
|
+
})
|
|
621
619
|
```
|
|
622
620
|
|
|
623
621
|
### Security
|
|
@@ -634,25 +632,25 @@ const outbox = new PostgresPrismaOutbox({
|
|
|
634
632
|
Essential when forwarding events to external systems (SQS, Kafka) or to protect PII stored in the `outbox_events` table.
|
|
635
633
|
|
|
636
634
|
```typescript
|
|
637
|
-
import { encrypt, decrypt } from './crypto'
|
|
635
|
+
import { encrypt, decrypt } from './crypto'
|
|
638
636
|
|
|
639
637
|
// Encryption Middleware (applies to both phases, encryption logic inside handles direction)
|
|
640
638
|
bus.addMiddleware(async (ctx, next) => {
|
|
641
639
|
if (ctx.phase === 'emit') {
|
|
642
|
-
ctx.event.payload = encrypt(ctx.event.payload)
|
|
640
|
+
ctx.event.payload = encrypt(ctx.event.payload)
|
|
643
641
|
} else {
|
|
644
|
-
ctx.event.payload = decrypt(ctx.event.payload)
|
|
642
|
+
ctx.event.payload = decrypt(ctx.event.payload)
|
|
645
643
|
}
|
|
646
|
-
await next()
|
|
647
|
-
})
|
|
644
|
+
await next()
|
|
645
|
+
})
|
|
648
646
|
|
|
649
647
|
// Usage (Transparent encryption/decryption)
|
|
650
|
-
await bus.emit({ type: 'user.created', payload: user })
|
|
648
|
+
await bus.emit({ type: 'user.created', payload: user })
|
|
651
649
|
|
|
652
650
|
bus.on('user.created', async (event) => {
|
|
653
651
|
// event.payload is automatically decrypted
|
|
654
|
-
await emailService.send(event.payload.email)
|
|
655
|
-
})
|
|
652
|
+
await emailService.send(event.payload.email)
|
|
653
|
+
})
|
|
656
654
|
```
|
|
657
655
|
|
|
658
656
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -141,10 +141,9 @@ async function executeMiddleware(middlewares, context) {
|
|
|
141
141
|
async function promiseMap(items, mapper, concurrency) {
|
|
142
142
|
let cursor = 0;
|
|
143
143
|
const results = new Array(items.length);
|
|
144
|
-
const workers = Array.from({ length: concurrency }, async () => {
|
|
144
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
145
145
|
while (cursor < items.length) {
|
|
146
146
|
const index = cursor++;
|
|
147
|
-
if (index >= items.length) break;
|
|
148
147
|
results[index] = await mapper(items[index], index);
|
|
149
148
|
}
|
|
150
149
|
});
|
|
@@ -220,8 +219,8 @@ var OutboxEventBus = class {
|
|
|
220
219
|
return this;
|
|
221
220
|
}
|
|
222
221
|
addMiddleware(...middlewares) {
|
|
223
|
-
this.
|
|
224
|
-
this.
|
|
222
|
+
this.addEmitMiddleware(...middlewares);
|
|
223
|
+
this.addHandlerMiddleware(...middlewares);
|
|
225
224
|
return this;
|
|
226
225
|
}
|
|
227
226
|
async emit(event, transaction) {
|