@l-etabli/events 0.3.0 → 0.4.0
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 +262 -11
- package/dist/adapters/in-memory/InMemoryEventRepository.cjs +2 -1
- package/dist/adapters/in-memory/InMemoryEventRepository.cjs.map +1 -1
- package/dist/adapters/in-memory/InMemoryEventRepository.mjs +2 -1
- package/dist/adapters/in-memory/InMemoryEventRepository.mjs.map +1 -1
- package/dist/ports/EventRepository.cjs.map +1 -1
- package/dist/ports/EventRepository.d.cts +4 -3
- package/dist/ports/EventRepository.d.ts +4 -3
- package/package.json +1 -1
- package/src/adapters/in-memory/InMemoryEventRepository.ts +2 -1
- package/src/ports/EventRepository.ts +6 -5
package/README.md
CHANGED
|
@@ -1,16 +1,267 @@
|
|
|
1
|
-
# l-etabli/events
|
|
1
|
+
# @l-etabli/events
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Event-driven architecture library implementing the **outbox pattern** for TypeScript.
|
|
4
4
|
|
|
5
|
+
Events are persisted in the same transaction as your domain changes, then reliably published asynchronously. No lost events, even on failures.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
## Installation
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @l-etabli/events
|
|
11
|
+
```
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
For Kysely adapter (PostgreSQL):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @l-etabli/events kysely pg
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Define Your Events
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { GenericEvent } from "@l-etabli/events";
|
|
25
|
+
|
|
26
|
+
type MyEvents =
|
|
27
|
+
| GenericEvent<"UserCreated", { userId: string; email: string }>
|
|
28
|
+
| GenericEvent<"OrderPlaced", { orderId: string; amount: number }>;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Setup Event Infrastructure
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import {
|
|
35
|
+
createInMemoryEventBus,
|
|
36
|
+
createInMemoryEventRepositoryAndQueries,
|
|
37
|
+
createEventCrawler,
|
|
38
|
+
} from "@l-etabli/events";
|
|
39
|
+
|
|
40
|
+
const { eventQueries, withUow } = createInMemoryEventRepositoryAndQueries<MyEvents>();
|
|
41
|
+
const { eventBus, createNewEvent } = createInMemoryEventBus<MyEvents>(withUow);
|
|
42
|
+
|
|
43
|
+
const crawler = createEventCrawler({
|
|
44
|
+
withUow,
|
|
45
|
+
eventQueries,
|
|
46
|
+
eventBus,
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Subscribe to Events
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
eventBus.subscribe({
|
|
54
|
+
topic: "OrderPlaced",
|
|
55
|
+
subscriptionId: "send-confirmation-email",
|
|
56
|
+
callBack: async (event) => {
|
|
57
|
+
await emailService.sendOrderConfirmation(event.payload.orderId);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 4. Emit Events (in a use case)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
await withUow(async (uow) => {
|
|
66
|
+
// Save your domain entity
|
|
67
|
+
await orderRepository.save(order);
|
|
68
|
+
|
|
69
|
+
// Emit event in the same transaction
|
|
70
|
+
await uow.eventRepository.saveNewEventsBatch([
|
|
71
|
+
createNewEvent({
|
|
72
|
+
topic: "OrderPlaced",
|
|
73
|
+
payload: { orderId: order.id, amount: order.total },
|
|
74
|
+
triggeredByUserId: currentUserId,
|
|
75
|
+
}),
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 5. Process Events
|
|
81
|
+
|
|
82
|
+
**Traditional server** - start background polling:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
crawler.start();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Serverless** - trigger on-demand after commit:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
await withUow(
|
|
92
|
+
async (uow) => {
|
|
93
|
+
await uow.eventRepository.saveNewEventsBatch([event]);
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
afterCommit: async () => {
|
|
97
|
+
await crawler.triggerProcessing();
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
### Returning Values from Transactions
|
|
103
|
+
|
|
104
|
+
The `withUow` function supports returning values from your transaction callback:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const result = await withUow(async (uow) => {
|
|
108
|
+
const order = await orderRepository.save(newOrder);
|
|
109
|
+
|
|
110
|
+
await uow.eventRepository.saveNewEventsBatch([
|
|
111
|
+
createNewEvent({
|
|
112
|
+
topic: "OrderPlaced",
|
|
113
|
+
payload: { orderId: order.id, amount: order.total },
|
|
114
|
+
triggeredByUserId: currentUserId,
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
return { orderId: order.id, createdAt: order.createdAt };
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.log(result.orderId); // Access the returned value
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Event Lifecycle
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
never-published → in-process → published
|
|
128
|
+
↘ failed-but-will-retry → published
|
|
129
|
+
↘ quarantined (after maxRetries)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `never-published` - New event, not yet processed
|
|
133
|
+
- `in-process` - Currently being published
|
|
134
|
+
- `published` - Successfully delivered to all subscribers
|
|
135
|
+
- `failed-but-will-retry` - Some subscribers failed, will retry
|
|
136
|
+
- `quarantined` - Exceeded max retries, requires manual intervention
|
|
137
|
+
- `to-republish` - Force republish to all subscribers
|
|
138
|
+
|
|
139
|
+
## API Reference
|
|
140
|
+
|
|
141
|
+
### Types
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Define events with topic, payload, and optional context
|
|
145
|
+
type GenericEvent<Topic, Payload, Context?> = {
|
|
146
|
+
id: EventId;
|
|
147
|
+
topic: Topic;
|
|
148
|
+
payload: Payload;
|
|
149
|
+
status: EventStatus;
|
|
150
|
+
occurredAt: Date;
|
|
151
|
+
triggeredByUserId: UserId;
|
|
152
|
+
publications: EventPublication[];
|
|
153
|
+
context?: Context;
|
|
154
|
+
priority?: number;
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `makeCreateNewEvent<Events>(options?)`
|
|
159
|
+
|
|
160
|
+
Creates a type-safe event factory. Payload is validated against topic at compile time.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const createEvent = makeCreateNewEvent<MyEvents>({
|
|
164
|
+
getNow: () => new Date(), // optional, for testing
|
|
165
|
+
generateId: () => crypto.randomUUID(), // optional, for testing
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Type-safe: payload must match topic
|
|
169
|
+
createEvent({ topic: "UserCreated", payload: { userId: "1", email: "a@b.com" }, triggeredByUserId: "u1" });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `createInMemoryEventBus<Events>(withUow, options?)`
|
|
173
|
+
|
|
174
|
+
Creates an in-memory event bus with a typed `createNewEvent` function.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const { eventBus, createNewEvent } = createInMemoryEventBus<MyEvents>(withUow, {
|
|
178
|
+
maxRetries: 3, // default
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `createEventCrawler(config)`
|
|
183
|
+
|
|
184
|
+
Creates a background processor for publishing events.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const crawler = createEventCrawler({
|
|
188
|
+
withUow,
|
|
189
|
+
eventQueries,
|
|
190
|
+
eventBus,
|
|
191
|
+
options: {
|
|
192
|
+
batchSize: 100, // events per batch (default: 100)
|
|
193
|
+
maxParallelProcessing: 1, // parallel publishes (default: 1)
|
|
194
|
+
newEventsIntervalMs: 10000, // polling interval (default: 10s)
|
|
195
|
+
failedEventsIntervalMs: 60000, // retry interval (default: 60s)
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
crawler.start(); // Start background polling
|
|
200
|
+
crawler.processNewEvents(); // Manual: process new events
|
|
201
|
+
crawler.retryFailedEvents(); // Manual: retry failed events
|
|
202
|
+
crawler.triggerProcessing(); // Manual: process new + retry failed
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Database Setup (Kysely/PostgreSQL)
|
|
206
|
+
|
|
207
|
+
### Migration
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import type { Kysely } from "kysely";
|
|
211
|
+
|
|
212
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
213
|
+
await db.schema
|
|
214
|
+
.createTable("events")
|
|
215
|
+
.addColumn("id", "text", (col) => col.primaryKey())
|
|
216
|
+
.addColumn("topic", "text", (col) => col.notNull())
|
|
217
|
+
.addColumn("payload", "jsonb", (col) => col.notNull())
|
|
218
|
+
.addColumn("context", "jsonb")
|
|
219
|
+
.addColumn("status", "text", (col) => col.notNull())
|
|
220
|
+
.addColumn("triggeredByUserId", "text", (col) => col.notNull())
|
|
221
|
+
.addColumn("occurredAt", "timestamptz", (col) => col.notNull())
|
|
222
|
+
.addColumn("publications", "jsonb", (col) => col.notNull().defaultTo("[]"))
|
|
223
|
+
.addColumn("priority", "integer")
|
|
224
|
+
.execute();
|
|
225
|
+
|
|
226
|
+
await db.schema
|
|
227
|
+
.createIndex("events_status_idx")
|
|
228
|
+
.on("events")
|
|
229
|
+
.column("status")
|
|
230
|
+
.execute();
|
|
231
|
+
|
|
232
|
+
await db.schema
|
|
233
|
+
.createIndex("events_topic_idx")
|
|
234
|
+
.on("events")
|
|
235
|
+
.column("topic")
|
|
236
|
+
.execute();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
240
|
+
await db.schema.dropTable("events").execute();
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Usage with Kysely
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { createInMemoryEventBus, createEventCrawler } from "@l-etabli/events";
|
|
248
|
+
import {
|
|
249
|
+
KyselyEventRepository,
|
|
250
|
+
KyselyEventQueries,
|
|
251
|
+
createKyselyMigration,
|
|
252
|
+
} from "@l-etabli/events/kysely";
|
|
253
|
+
|
|
254
|
+
// See examples/kysely/ for complete implementation
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Examples
|
|
258
|
+
|
|
259
|
+
See the [`examples/`](./examples/) directory for complete implementations:
|
|
260
|
+
|
|
261
|
+
- **[kysely-adapter.ts](./examples/kysely/kysely-adapter.ts)** - Kysely adapter with transaction support
|
|
262
|
+
- **[serverless-usage.ts](./examples/kysely/serverless-usage.ts)** - AWS Lambda / serverless deployment
|
|
263
|
+
- **[cascading-events.ts](./examples/kysely/cascading-events.ts)** - Transactional event chains
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT
|
|
@@ -56,8 +56,9 @@ const createInMemoryEventRepository = () => {
|
|
|
56
56
|
};
|
|
57
57
|
const createInMemoryWithUow = (eventRepository) => {
|
|
58
58
|
const withUow = async (fn, options) => {
|
|
59
|
-
await fn({ eventRepository });
|
|
59
|
+
const result = await fn({ eventRepository });
|
|
60
60
|
await options?.afterCommit?.();
|
|
61
|
+
return result;
|
|
61
62
|
};
|
|
62
63
|
return { withUow };
|
|
63
64
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from \"../../ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n // In-memory adapter awaits afterCommit for predictable test behavior\n const withUow: WithEventsUow<Event> = async (fn, options) => {\n await fn({ eventRepository });\n await options?.afterCommit?.();\n };\n return { withUow };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;AAEtC,QAAM,UAAgC,OAAO,IAAI,YAAY;AAC3D,UAAM,GAAG,EAAE,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from \"../../ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n // In-memory adapter awaits afterCommit for predictable test behavior\n const withUow: WithEventsUow<Event> = async (fn, options) => {\n const result = await fn({ eventRepository });\n await options?.afterCommit?.();\n return result;\n };\n return { withUow };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;AAEtC,QAAM,UAAgC,OAAO,IAAI,YAAY;AAC3D,UAAM,SAAS,MAAM,GAAG,EAAE,gBAAgB,CAAC;AAC3C,UAAM,SAAS,cAAc;AAC7B,WAAO;AAAA,EACT;AACA,SAAO,EAAE,QAAQ;AACnB;","names":[]}
|
|
@@ -32,8 +32,9 @@ const createInMemoryEventRepository = () => {
|
|
|
32
32
|
};
|
|
33
33
|
const createInMemoryWithUow = (eventRepository) => {
|
|
34
34
|
const withUow = async (fn, options) => {
|
|
35
|
-
await fn({ eventRepository });
|
|
35
|
+
const result = await fn({ eventRepository });
|
|
36
36
|
await options?.afterCommit?.();
|
|
37
|
+
return result;
|
|
37
38
|
};
|
|
38
39
|
return { withUow };
|
|
39
40
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from '../../ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n // In-memory adapter awaits afterCommit for predictable test behavior\n const withUow: WithEventsUow<Event> = async (fn, options) => {\n await fn({ eventRepository });\n await options?.afterCommit?.();\n };\n return { withUow };\n};\n"],"mappings":"AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;AAEtC,QAAM,UAAgC,OAAO,IAAI,YAAY;AAC3D,UAAM,GAAG,EAAE,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from '../../ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n // In-memory adapter awaits afterCommit for predictable test behavior\n const withUow: WithEventsUow<Event> = async (fn, options) => {\n const result = await fn({ eventRepository });\n await options?.afterCommit?.();\n return result;\n };\n return { withUow };\n};\n"],"mappings":"AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;AAEtC,QAAM,UAAgC,OAAO,IAAI,YAAY;AAC3D,UAAM,SAAS,MAAM,GAAG,EAAE,gBAAgB,CAAC;AAC3C,UAAM,SAAS,cAAc;AAC7B,WAAO;AAAA,EACT;AACA,SAAO,EAAE,QAAQ;AACnB;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ports/EventRepository.ts"],"sourcesContent":["import type { DefaultContext, GenericEvent } from \"../types.ts\";\n\n/**\n * Repository interface for persisting events.\n * Implement this to store events in your database (e.g., PostgreSQL, MongoDB).\n * Events should be saved in the same transaction as your domain changes.\n */\nexport type EventRepository<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Persist a single event (typically after publication status update). */\n save: (event: Event) => Promise<void>;\n /** Persist multiple new events in a batch. */\n saveNewEventsBatch: (events: Event[]) => Promise<void>;\n /** Mark events as \"in-process\" before publishing (prevents duplicate processing). */\n markEventsAsInProcess: (events: Event[]) => Promise<void>;\n};\n\n/**\n * Unit of work containing the event repository.\n * Extend this with your own repositories for transactional consistency.\n */\nexport type EventsUnitOfWork<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n eventRepository: EventRepository<Event>;\n};\n\n/**\n * Options for unit of work execution.\n */\nexport type WithEventsUowOptions = {\n /**\n * Callback executed after successful transaction commit.\n * Useful for triggering event processing in serverless environments.\n *\n * The callback should return a Promise. Whether it's awaited depends on\n * the withUow implementation:\n * - Serverless (Lambda): await to ensure completion before runtime freezes\n * - Long-running servers: fire-and-forget for faster response times\n *\n * @example\n * ```typescript\n * await withUow(async (uow) => {\n * await uow.eventRepository.save(event);\n * }, {\n * afterCommit: async () => {\n * await eventCrawler.triggerProcessing();\n * }\n * });\n * ```\n */\n afterCommit?: () => Promise<void>;\n};\n\n/**\n * Higher-order function that provides a unit of work for transactional operations.\n * Your implementation should handle transaction begin/commit/rollback.\n *\n * @example\n * ```typescript\n * const withUow: WithEventsUow<MyEvent> = async (fn, options) => {\n * const tx = await db.beginTransaction();\n * try {\n * await fn({ eventRepository: createEventRepo(tx) });\n * await tx.commit();\n * options?.afterCommit?.();\n * } catch (e) {\n * await tx.rollback();\n * throw e;\n * }\n * };\n * ```\n */\nexport type WithEventsUow<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = (\n fn: (uow: EventsUnitOfWork<Event>) => Promise<
|
|
1
|
+
{"version":3,"sources":["../../src/ports/EventRepository.ts"],"sourcesContent":["import type { DefaultContext, GenericEvent } from \"../types.ts\";\n\n/**\n * Repository interface for persisting events.\n * Implement this to store events in your database (e.g., PostgreSQL, MongoDB).\n * Events should be saved in the same transaction as your domain changes.\n */\nexport type EventRepository<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Persist a single event (typically after publication status update). */\n save: (event: Event) => Promise<void>;\n /** Persist multiple new events in a batch. */\n saveNewEventsBatch: (events: Event[]) => Promise<void>;\n /** Mark events as \"in-process\" before publishing (prevents duplicate processing). */\n markEventsAsInProcess: (events: Event[]) => Promise<void>;\n};\n\n/**\n * Unit of work containing the event repository.\n * Extend this with your own repositories for transactional consistency.\n */\nexport type EventsUnitOfWork<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n eventRepository: EventRepository<Event>;\n};\n\n/**\n * Options for unit of work execution.\n */\nexport type WithEventsUowOptions = {\n /**\n * Callback executed after successful transaction commit.\n * Useful for triggering event processing in serverless environments.\n *\n * The callback should return a Promise. Whether it's awaited depends on\n * the withUow implementation:\n * - Serverless (Lambda): await to ensure completion before runtime freezes\n * - Long-running servers: fire-and-forget for faster response times\n *\n * @example\n * ```typescript\n * await withUow(async (uow) => {\n * await uow.eventRepository.save(event);\n * }, {\n * afterCommit: async () => {\n * await eventCrawler.triggerProcessing();\n * }\n * });\n * ```\n */\n afterCommit?: () => Promise<void>;\n};\n\n/**\n * Higher-order function that provides a unit of work for transactional operations.\n * Your implementation should handle transaction begin/commit/rollback.\n *\n * @example\n * ```typescript\n * const withUow: WithEventsUow<MyEvent> = async (fn, options) => {\n * const tx = await db.beginTransaction();\n * try {\n * const result = await fn({ eventRepository: createEventRepo(tx) });\n * await tx.commit();\n * await options?.afterCommit?.();\n * return result;\n * } catch (e) {\n * await tx.rollback();\n * throw e;\n * }\n * };\n * ```\n */\nexport type WithEventsUow<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = <T>(\n fn: (uow: EventsUnitOfWork<Event>) => Promise<T>,\n options?: WithEventsUowOptions,\n) => Promise<T>;\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
@@ -55,9 +55,10 @@ type WithEventsUowOptions = {
|
|
|
55
55
|
* const withUow: WithEventsUow<MyEvent> = async (fn, options) => {
|
|
56
56
|
* const tx = await db.beginTransaction();
|
|
57
57
|
* try {
|
|
58
|
-
* await fn({ eventRepository: createEventRepo(tx) });
|
|
58
|
+
* const result = await fn({ eventRepository: createEventRepo(tx) });
|
|
59
59
|
* await tx.commit();
|
|
60
|
-
* options?.afterCommit?.();
|
|
60
|
+
* await options?.afterCommit?.();
|
|
61
|
+
* return result;
|
|
61
62
|
* } catch (e) {
|
|
62
63
|
* await tx.rollback();
|
|
63
64
|
* throw e;
|
|
@@ -65,6 +66,6 @@ type WithEventsUowOptions = {
|
|
|
65
66
|
* };
|
|
66
67
|
* ```
|
|
67
68
|
*/
|
|
68
|
-
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = (fn: (uow: EventsUnitOfWork<Event>) => Promise<
|
|
69
|
+
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = <T>(fn: (uow: EventsUnitOfWork<Event>) => Promise<T>, options?: WithEventsUowOptions) => Promise<T>;
|
|
69
70
|
|
|
70
71
|
export type { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions };
|
|
@@ -55,9 +55,10 @@ type WithEventsUowOptions = {
|
|
|
55
55
|
* const withUow: WithEventsUow<MyEvent> = async (fn, options) => {
|
|
56
56
|
* const tx = await db.beginTransaction();
|
|
57
57
|
* try {
|
|
58
|
-
* await fn({ eventRepository: createEventRepo(tx) });
|
|
58
|
+
* const result = await fn({ eventRepository: createEventRepo(tx) });
|
|
59
59
|
* await tx.commit();
|
|
60
|
-
* options?.afterCommit?.();
|
|
60
|
+
* await options?.afterCommit?.();
|
|
61
|
+
* return result;
|
|
61
62
|
* } catch (e) {
|
|
62
63
|
* await tx.rollback();
|
|
63
64
|
* throw e;
|
|
@@ -65,6 +66,6 @@ type WithEventsUowOptions = {
|
|
|
65
66
|
* };
|
|
66
67
|
* ```
|
|
67
68
|
*/
|
|
68
|
-
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = (fn: (uow: EventsUnitOfWork<Event>) => Promise<
|
|
69
|
+
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = <T>(fn: (uow: EventsUnitOfWork<Event>) => Promise<T>, options?: WithEventsUowOptions) => Promise<T>;
|
|
69
70
|
|
|
70
71
|
export type { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "The purpose of this repository is to make it easy to setup event driven architecture using outbox pattern",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.4.0",
|
|
7
7
|
"main": "./dist/index.mjs",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"files": [
|
|
@@ -56,8 +56,9 @@ export const createInMemoryWithUow = <
|
|
|
56
56
|
): { withUow: WithEventsUow<Event> } => {
|
|
57
57
|
// In-memory adapter awaits afterCommit for predictable test behavior
|
|
58
58
|
const withUow: WithEventsUow<Event> = async (fn, options) => {
|
|
59
|
-
await fn({ eventRepository });
|
|
59
|
+
const result = await fn({ eventRepository });
|
|
60
60
|
await options?.afterCommit?.();
|
|
61
|
+
return result;
|
|
61
62
|
};
|
|
62
63
|
return { withUow };
|
|
63
64
|
};
|
|
@@ -62,9 +62,10 @@ export type WithEventsUowOptions = {
|
|
|
62
62
|
* const withUow: WithEventsUow<MyEvent> = async (fn, options) => {
|
|
63
63
|
* const tx = await db.beginTransaction();
|
|
64
64
|
* try {
|
|
65
|
-
* await fn({ eventRepository: createEventRepo(tx) });
|
|
65
|
+
* const result = await fn({ eventRepository: createEventRepo(tx) });
|
|
66
66
|
* await tx.commit();
|
|
67
|
-
* options?.afterCommit?.();
|
|
67
|
+
* await options?.afterCommit?.();
|
|
68
|
+
* return result;
|
|
68
69
|
* } catch (e) {
|
|
69
70
|
* await tx.rollback();
|
|
70
71
|
* throw e;
|
|
@@ -74,7 +75,7 @@ export type WithEventsUowOptions = {
|
|
|
74
75
|
*/
|
|
75
76
|
export type WithEventsUow<
|
|
76
77
|
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
77
|
-
> = (
|
|
78
|
-
fn: (uow: EventsUnitOfWork<Event>) => Promise<
|
|
78
|
+
> = <T>(
|
|
79
|
+
fn: (uow: EventsUnitOfWork<Event>) => Promise<T>,
|
|
79
80
|
options?: WithEventsUowOptions,
|
|
80
|
-
) => Promise<
|
|
81
|
+
) => Promise<T>;
|