@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 CHANGED
@@ -1,16 +1,267 @@
1
- # l-etabli/events
1
+ # @l-etabli/events
2
2
 
3
- ${project_description}
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
- This project is a template for creating typscript libraries.
7
+ ## Installation
7
8
 
8
- It uses :
9
+ ```bash
10
+ pnpm add @l-etabli/events
11
+ ```
9
12
 
10
- - Typescript for type checking
11
- - Bun for running tests
12
- - Bun as a package manager
13
- - Biome as a formatter
14
- - Biome as a linter
15
- - Lefthook for pre-commit hooks
16
- - GitHub Actions for CI, which will run typecheck, format, lint, test and than deploy the package to npm
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;AAC5B,UAAM,SAAS,cAAc;AAAA,EAC/B;AACA,SAAO,EAAE,QAAQ;AACnB;","names":[]}
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;AAC5B,UAAM,SAAS,cAAc;AAAA,EAC/B;AACA,SAAO,EAAE,QAAQ;AACnB;","names":[]}
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<void>,\n options?: WithEventsUowOptions,\n) => Promise<void>;\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
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<void>, options?: WithEventsUowOptions) => Promise<void>;
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<void>, options?: WithEventsUowOptions) => Promise<void>;
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.3.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<void>,
78
+ > = <T>(
79
+ fn: (uow: EventsUnitOfWork<Event>) => Promise<T>,
79
80
  options?: WithEventsUowOptions,
80
- ) => Promise<void>;
81
+ ) => Promise<T>;