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 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](./adapters/postgres-prisma/README.md), [Drizzle](./adapters/postgres-drizzle/README.md) | Full (ACID) | `SKIP LOCKED` |
255
- | **MongoDB** | [Native Driver](./adapters/mongo-mongodb/README.md) | Full (Replica Set) | Optimistic Locking |
256
- | **DynamoDB** | [AWS SDK](./adapters/dynamodb-aws-sdk/README.md) | Full (TransactWrite) | Optimistic Locking |
257
- | **Redis** | [ioredis](./adapters/redis-ioredis/README.md) | Atomic (Multi/Exec) | Distributed Lock |
258
- | **SQLite** | [better-sqlite3](./adapters/sqlite-better-sqlite3/README.md) | Full (ACID) | Serialized |
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
- - **Full**: ACID transactions with atomicity guarantees
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](./publishers/sqs/README.md)** | Amazon SQS Queues | Yes (10) | `@outbox-event-bus/sqs-publisher` |
271
- | **[AWS SNS](./publishers/sns/README.md)** | Amazon SNS Topics | Yes (10) | `@outbox-event-bus/sns-publisher` |
272
- | **[EventBridge](./publishers/eventbridge/README.md)** | AWS Event Bus | Yes (10) | `@outbox-event-bus/eventbridge-publisher` |
273
- | **[RabbitMQ](./publishers/rabbitmq/README.md)** | AMQP Brokers | Yes (Configurable) | `@outbox-event-bus/rabbitmq-publisher` |
274
- | **[Kafka](./publishers/kafka/README.md)** | Streaming | Yes (Configurable) | `@outbox-event-bus/kafka-publisher` |
275
- | **[Redis Streams](./publishers/redis-streams/README.md)** | Lightweight Stream | Yes (Configurable) | `@outbox-event-bus/redis-streams-publisher` |
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 (err) {
547
- eventCounter.inc({ type: ctx.event.type, phase: ctx.phase, status: 'error' });
548
- throw err;
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 }); // OK
572
+ await next()
573
+ span.setStatus({ code: 1 }) // OK
576
574
  } catch (err) {
577
- span.recordException(err as Error);
578
- span.setStatus({ code: 2 }); // ERROR
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.emitMiddlewares.push(...middlewares);
224
- this.handlerMiddlewares.push(...middlewares);
222
+ this.addEmitMiddleware(...middlewares);
223
+ this.addHandlerMiddleware(...middlewares);
225
224
  return this;
226
225
  }
227
226
  async emit(event, transaction) {