@beignet/provider-event-bus-memory 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/provider-event-bus-memory
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
package/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # @beignet/provider-event-bus-memory
2
+
3
+ In-memory `EventBusPort` adapter for Beignet applications.
4
+
5
+ Use it for tests, local development, and single-process apps. Distributed
6
+ systems should adapt a queue, stream, outbox, or message broker behind the same
7
+ `EventBusPort` interface.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add @beignet/provider-event-bus-memory
13
+ ```
14
+
15
+ ## Direct setup
16
+
17
+ ```typescript
18
+ import { defineEvent } from "@beignet/core/events";
19
+ import { createInMemoryEventBus } from "@beignet/provider-event-bus-memory";
20
+ import { z } from "zod";
21
+
22
+ // Define your domain events
23
+ const UserRegistered = defineEvent("user.registered", {
24
+ payload: z.object({
25
+ userId: z.string(),
26
+ email: z.string().email(),
27
+ }),
28
+ });
29
+
30
+ // Create the event bus
31
+ const eventBus = createInMemoryEventBus();
32
+
33
+ // Subscribe to events
34
+ const unsubscribe = eventBus.subscribe(UserRegistered, (payload) => {
35
+ console.log(`User registered: ${payload.email}`);
36
+ // Send welcome email, update analytics, etc.
37
+ });
38
+
39
+ // Publish events
40
+ await eventBus.publish(UserRegistered, {
41
+ userId: "123",
42
+ email: "user@example.com",
43
+ });
44
+
45
+ // Unsubscribe when done
46
+ unsubscribe();
47
+ ```
48
+
49
+ ## Framework setup
50
+
51
+ ```typescript
52
+ import { createNextServer } from "@beignet/next";
53
+ import { createInMemoryEventBusProvider } from "@beignet/provider-event-bus-memory";
54
+ import { appPorts } from "@/infra/app-ports";
55
+ import { routes } from "@/server/routes";
56
+
57
+ export const server = await createNextServer({
58
+ ports: appPorts,
59
+ providers: [createInMemoryEventBusProvider()],
60
+ createContext: ({ ports }) => ({
61
+ ports,
62
+ }),
63
+ routes,
64
+ });
65
+ ```
66
+
67
+ Use `createInMemoryEventBus()` directly when you want to manually assign an
68
+ event bus under `ports`.
69
+
70
+ ## Instrumentation
71
+
72
+ Pass a provider instrumentation target when creating the direct event bus to
73
+ record published events under the `eventBus` watcher:
74
+
75
+ ```typescript
76
+ const eventBus = createInMemoryEventBus({
77
+ instrumentation: ports,
78
+ });
79
+ ```
80
+
81
+ ## Using in use cases
82
+
83
+ ```typescript
84
+ import { defineEvent } from "@beignet/core/events";
85
+ import { z } from "zod";
86
+
87
+ const OrderPlaced = defineEvent("order.placed", {
88
+ payload: z.object({
89
+ orderId: z.string(),
90
+ total: z.number(),
91
+ }),
92
+ });
93
+
94
+ // Subscribe to events in your application setup
95
+ ctx.ports.eventBus.subscribe(OrderPlaced, async (payload) => {
96
+ // Send order confirmation email
97
+ await ctx.ports.mailer.send({
98
+ to: customer.email,
99
+ subject: "Order Confirmation",
100
+ text: `Your order ${payload.orderId} has been placed!`,
101
+ });
102
+ });
103
+
104
+ const placeOrder = useCase
105
+ .command("orders.place")
106
+ .input(PlaceOrderInput)
107
+ .output(OrderOutput)
108
+ .emits([OrderPlaced])
109
+ .run(async ({ ctx, input, events }) => {
110
+ return ctx.ports.uow.transaction(async (tx) => {
111
+ const order = await tx.orders.create(input);
112
+
113
+ await events.record(tx.events, OrderPlaced, {
114
+ orderId: order.id,
115
+ total: order.total,
116
+ });
117
+
118
+ return order;
119
+ });
120
+ });
121
+ ```
122
+
123
+ ## EventBus port API
124
+
125
+ ### `publish<E>(event: E, payload: InferEventPayload<E>): Promise<void> | void`
126
+
127
+ Publish a domain event with a typed payload.
128
+
129
+ ```typescript
130
+ await eventBus.publish(UserRegistered, {
131
+ userId: "123",
132
+ email: "user@example.com",
133
+ });
134
+ ```
135
+
136
+ By default, the in-memory bus awaits handlers so local development and tests are
137
+ deterministic. Handler errors are rethrown unless `onHandlerError` is provided.
138
+ Use `delivery: "fire-and-forget"` when you intentionally want detached
139
+ in-process delivery.
140
+
141
+ ### `subscribe<E>(event: E, handler: (payload) => void | Promise<void>): () => void`
142
+
143
+ Subscribe to a domain event. Returns an unsubscribe function.
144
+
145
+ ```typescript
146
+ const unsubscribe = eventBus.subscribe(UserRegistered, (payload) => {
147
+ console.log(`New user: ${payload.email}`);
148
+ });
149
+
150
+ // Later, when you want to stop listening:
151
+ unsubscribe();
152
+ ```
153
+
154
+ ## TypeScript support
155
+
156
+ The event bus provides full type safety:
157
+
158
+ ```typescript
159
+ import type { EventBusPort } from "@beignet/core/ports";
160
+ import { definePorts } from "@beignet/core/ports";
161
+
162
+ // Type-safe ports definition
163
+ const appPorts = definePorts({
164
+ eventBus: createInMemoryEventBus() as EventBusPort,
165
+ // ... other ports
166
+ });
167
+
168
+ type AppPorts = typeof appPorts;
169
+ ```
170
+
171
+ ## Testing
172
+
173
+ The in-memory event bus is perfect for testing:
174
+
175
+ ```typescript
176
+ import { describe, expect, it, mock } from "bun:test";
177
+
178
+ describe("User Registration", () => {
179
+ it("should publish UserRegistered event", async () => {
180
+ const eventBus = createInMemoryEventBus();
181
+ const handler = mock(() => {});
182
+
183
+ eventBus.subscribe(UserRegistered, handler);
184
+
185
+ // Perform registration
186
+ await registerUser(ctx, { email: "test@example.com" });
187
+
188
+ expect(handler).toHaveBeenCalledWith({
189
+ userId: expect.any(String),
190
+ email: "test@example.com",
191
+ });
192
+ });
193
+ });
194
+ ```
195
+
196
+ ## Behavior
197
+
198
+ - **Awaited by default**: `publish(...)` waits for subscribed handlers unless
199
+ `delivery: "fire-and-forget"` is configured
200
+ - **In-process**: Events are only delivered within the same process
201
+ - **Memory-only**: No persistence - events are lost if the process crashes
202
+ - **Order**: Handlers are called in the order they were subscribed
203
+ - **Multiple handlers**: Multiple handlers can subscribe to the same event
204
+ - **Error handling**: Handler errors reject `publish(...)` unless
205
+ `onHandlerError` is configured
206
+
207
+ ## When to use
208
+
209
+ Good for:
210
+
211
+ - Single-process applications
212
+ - Development and testing
213
+ - Simple event-driven workflows
214
+ - Decoupling application components
215
+
216
+ Not suitable for:
217
+
218
+ - Distributed systems
219
+ - Event persistence requirements
220
+ - Guaranteed delivery needs
221
+ - Cross-service communication
222
+
223
+ For production distributed systems, implement `EventBusPort` with a proper message broker.
224
+
225
+ ## License
226
+
227
+ MIT
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @beignet/provider-event-bus-memory
3
+ *
4
+ * In-memory event bus provider that implements EventBusPort.
5
+ * Suitable for single-process applications or testing.
6
+ *
7
+ * For distributed systems, consider implementing EventBusPort with
8
+ * a message broker like RabbitMQ, Kafka, or Redis Pub/Sub.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createInMemoryEventBus } from "@beignet/provider-event-bus-memory";
13
+ *
14
+ * const eventBus = createInMemoryEventBus();
15
+ *
16
+ * // Subscribe to an event
17
+ * const unsubscribe = eventBus.subscribe(UserRegistered, (payload) => {
18
+ * console.log(`User registered: ${payload.email}`);
19
+ * });
20
+ *
21
+ * // Publish an event
22
+ * await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
23
+ *
24
+ * // Unsubscribe when done
25
+ * unsubscribe();
26
+ * ```
27
+ */
28
+ import type { EventBusPort } from "@beignet/core/ports";
29
+ import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
30
+ /**
31
+ * Options for the in-memory event bus.
32
+ */
33
+ export type InMemoryEventBusDelivery = "await-handlers" | "fire-and-forget";
34
+ export interface InMemoryEventBusOptions {
35
+ /**
36
+ * Optional instrumentation target. Providers pass existing app ports here
37
+ * automatically; direct factory users can pass an instrumentation port.
38
+ */
39
+ instrumentation?: ProviderInstrumentationTarget;
40
+ /**
41
+ * How publish calls deliver events to subscribed handlers.
42
+ *
43
+ * Defaults to "await-handlers" so local development and tests observe handler
44
+ * work deterministically. Use "fire-and-forget" for detached in-process
45
+ * delivery when the caller should not wait for listeners.
46
+ */
47
+ delivery?: InMemoryEventBusDelivery;
48
+ /**
49
+ * Called when an async event handler throws an error.
50
+ *
51
+ * In "await-handlers" mode, handler errors are rethrown when this callback is
52
+ * not provided. In "fire-and-forget" mode, handler errors are swallowed when
53
+ * this callback is not provided so detached handlers do not create unhandled
54
+ * rejections.
55
+ *
56
+ * @param error The error thrown by the handler
57
+ * @param eventName The name of the event being handled
58
+ * @param payload The event payload that was passed to the handler
59
+ */
60
+ onHandlerError?: (error: unknown, eventName: string, payload: unknown) => void;
61
+ }
62
+ export interface InMemoryEventBusProviderOptions extends Omit<InMemoryEventBusOptions, "instrumentation"> {
63
+ /**
64
+ * Provider name. Defaults to "event-bus-memory".
65
+ */
66
+ name?: string;
67
+ }
68
+ export interface InMemoryEventBusProviderPorts {
69
+ eventBus: EventBusPort;
70
+ }
71
+ /**
72
+ * Create an in-memory event bus implementation.
73
+ *
74
+ * This is suitable for single-process applications or testing.
75
+ * For distributed systems, you would implement EventBusPort with
76
+ * a message broker like RabbitMQ, Kafka, or Redis Pub/Sub.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const eventBus = createInMemoryEventBus();
81
+ *
82
+ * eventBus.subscribe(UserRegistered, (payload) => {
83
+ * console.log(`User registered: ${payload.email}`);
84
+ * });
85
+ *
86
+ * await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
87
+ * ```
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * // With error handling
92
+ * const eventBus = createInMemoryEventBus({
93
+ * onHandlerError: (error, eventName, payload) => {
94
+ * logger.error(`Event handler failed for ${eventName}`, { error, payload });
95
+ * },
96
+ * });
97
+ * ```
98
+ */
99
+ export declare function createInMemoryEventBus(options?: InMemoryEventBusOptions): EventBusPort;
100
+ export declare function createInMemoryEventBusProvider(options?: InMemoryEventBusProviderOptions): import("@beignet/core/providers").ServiceProvider<unknown, import("@standard-schema/spec").StandardSchemaV1<void, void>, {
101
+ eventBus: EventBusPort;
102
+ }>;
103
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,yBAAyB,CAAC;AAEjC;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAE5E,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,eAAe,CAAC,EAAE,6BAA6B,CAAC;IAEhD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,wBAAwB,CAAC;IAEpC;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,CACf,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,KACb,IAAI,CAAC;CACX;AAED,MAAM,WAAW,+BACf,SAAQ,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC;IACxD;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,uBAA4B,GACpC,YAAY,CAoFd;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,GAAE,+BAAoC;;GAiB9C"}
package/dist/index.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @beignet/provider-event-bus-memory
3
+ *
4
+ * In-memory event bus provider that implements EventBusPort.
5
+ * Suitable for single-process applications or testing.
6
+ *
7
+ * For distributed systems, consider implementing EventBusPort with
8
+ * a message broker like RabbitMQ, Kafka, or Redis Pub/Sub.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createInMemoryEventBus } from "@beignet/provider-event-bus-memory";
13
+ *
14
+ * const eventBus = createInMemoryEventBus();
15
+ *
16
+ * // Subscribe to an event
17
+ * const unsubscribe = eventBus.subscribe(UserRegistered, (payload) => {
18
+ * console.log(`User registered: ${payload.email}`);
19
+ * });
20
+ *
21
+ * // Publish an event
22
+ * await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
23
+ *
24
+ * // Unsubscribe when done
25
+ * unsubscribe();
26
+ * ```
27
+ */
28
+ import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
29
+ /**
30
+ * Create an in-memory event bus implementation.
31
+ *
32
+ * This is suitable for single-process applications or testing.
33
+ * For distributed systems, you would implement EventBusPort with
34
+ * a message broker like RabbitMQ, Kafka, or Redis Pub/Sub.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const eventBus = createInMemoryEventBus();
39
+ *
40
+ * eventBus.subscribe(UserRegistered, (payload) => {
41
+ * console.log(`User registered: ${payload.email}`);
42
+ * });
43
+ *
44
+ * await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
45
+ * ```
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * // With error handling
50
+ * const eventBus = createInMemoryEventBus({
51
+ * onHandlerError: (error, eventName, payload) => {
52
+ * logger.error(`Event handler failed for ${eventName}`, { error, payload });
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ export function createInMemoryEventBus(options = {}) {
58
+ const onHandlerError = options?.onHandlerError;
59
+ const delivery = options.delivery ?? "await-handlers";
60
+ const instrumentation = createProviderInstrumentation(options.instrumentation, {
61
+ providerName: "event-bus-memory",
62
+ watcher: "eventBus",
63
+ });
64
+ const handlers = new Map();
65
+ const reportHandlerError = (error, eventName, payload) => {
66
+ try {
67
+ onHandlerError?.(error, eventName, payload);
68
+ }
69
+ catch {
70
+ // Handler error callbacks should not make publish throw or create unhandled rejections.
71
+ }
72
+ };
73
+ return {
74
+ publish(event, payload) {
75
+ instrumentation.record({
76
+ type: "eventBus",
77
+ eventName: event.name,
78
+ });
79
+ const subs = handlers.get(event.name);
80
+ if (!subs || subs.size === 0)
81
+ return;
82
+ const subscribers = [...subs];
83
+ if (delivery === "fire-and-forget") {
84
+ for (const handler of subscribers) {
85
+ try {
86
+ const result = handler(payload);
87
+ Promise.resolve(result).catch((error) => {
88
+ reportHandlerError(error, event.name, payload);
89
+ });
90
+ }
91
+ catch (error) {
92
+ reportHandlerError(error, event.name, payload);
93
+ }
94
+ }
95
+ return;
96
+ }
97
+ return (async () => {
98
+ for (const handler of subscribers) {
99
+ try {
100
+ await handler(payload);
101
+ }
102
+ catch (error) {
103
+ reportHandlerError(error, event.name, payload);
104
+ if (!onHandlerError)
105
+ throw error;
106
+ }
107
+ }
108
+ })();
109
+ },
110
+ subscribe(event, handler) {
111
+ let subs = handlers.get(event.name);
112
+ if (!subs) {
113
+ subs = new Set();
114
+ handlers.set(event.name, subs);
115
+ }
116
+ subs.add(handler);
117
+ return () => {
118
+ subs.delete(handler);
119
+ if (subs.size === 0) {
120
+ handlers.delete(event.name);
121
+ }
122
+ };
123
+ },
124
+ };
125
+ }
126
+ export function createInMemoryEventBusProvider(options = {}) {
127
+ const { name = "event-bus-memory", ...eventBusOptions } = options;
128
+ return createProvider({
129
+ name,
130
+ setup({ ports }) {
131
+ return {
132
+ ports: {
133
+ eventBus: createInMemoryEventBus({
134
+ ...eventBusOptions,
135
+ instrumentation: ports,
136
+ }),
137
+ },
138
+ };
139
+ },
140
+ });
141
+ }
142
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@beignet/provider-event-bus-memory",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "In-memory event bus provider for Beignet - implements EventBusPort for single-process applications",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/*.test-d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "clean": "rm -rf dist coverage .turbo",
27
+ "test": "bun test",
28
+ "test:coverage": "bun test --coverage",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "beignet",
33
+ "event-bus",
34
+ "provider",
35
+ "in-memory",
36
+ "ports"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/taylorbryant/beignet.git",
42
+ "directory": "packages/provider-event-bus-memory"
43
+ },
44
+ "author": "Taylor Bryant",
45
+ "homepage": "https://github.com/taylorbryant/beignet#readme",
46
+ "bugs": "https://github.com/taylorbryant/beignet/issues",
47
+ "sideEffects": false,
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "dependencies": {
55
+ "@beignet/core": "*"
56
+ },
57
+ "devDependencies": {
58
+ "@beignet/devtools": "*",
59
+ "@types/bun": "^1.3.13",
60
+ "@types/node": "^20.10.0",
61
+ "typescript": "^5.3.0",
62
+ "zod": "^4.0.0"
63
+ }
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @beignet/provider-event-bus-memory
3
+ *
4
+ * In-memory event bus provider that implements EventBusPort.
5
+ * Suitable for single-process applications or testing.
6
+ *
7
+ * For distributed systems, consider implementing EventBusPort with
8
+ * a message broker like RabbitMQ, Kafka, or Redis Pub/Sub.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createInMemoryEventBus } from "@beignet/provider-event-bus-memory";
13
+ *
14
+ * const eventBus = createInMemoryEventBus();
15
+ *
16
+ * // Subscribe to an event
17
+ * const unsubscribe = eventBus.subscribe(UserRegistered, (payload) => {
18
+ * console.log(`User registered: ${payload.email}`);
19
+ * });
20
+ *
21
+ * // Publish an event
22
+ * await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
23
+ *
24
+ * // Unsubscribe when done
25
+ * unsubscribe();
26
+ * ```
27
+ */
28
+
29
+ import type { EventBusPort } from "@beignet/core/ports";
30
+ import {
31
+ createProvider,
32
+ createProviderInstrumentation,
33
+ type ProviderInstrumentationTarget,
34
+ } from "@beignet/core/providers";
35
+
36
+ /**
37
+ * Options for the in-memory event bus.
38
+ */
39
+ export type InMemoryEventBusDelivery = "await-handlers" | "fire-and-forget";
40
+
41
+ export interface InMemoryEventBusOptions {
42
+ /**
43
+ * Optional instrumentation target. Providers pass existing app ports here
44
+ * automatically; direct factory users can pass an instrumentation port.
45
+ */
46
+ instrumentation?: ProviderInstrumentationTarget;
47
+
48
+ /**
49
+ * How publish calls deliver events to subscribed handlers.
50
+ *
51
+ * Defaults to "await-handlers" so local development and tests observe handler
52
+ * work deterministically. Use "fire-and-forget" for detached in-process
53
+ * delivery when the caller should not wait for listeners.
54
+ */
55
+ delivery?: InMemoryEventBusDelivery;
56
+
57
+ /**
58
+ * Called when an async event handler throws an error.
59
+ *
60
+ * In "await-handlers" mode, handler errors are rethrown when this callback is
61
+ * not provided. In "fire-and-forget" mode, handler errors are swallowed when
62
+ * this callback is not provided so detached handlers do not create unhandled
63
+ * rejections.
64
+ *
65
+ * @param error The error thrown by the handler
66
+ * @param eventName The name of the event being handled
67
+ * @param payload The event payload that was passed to the handler
68
+ */
69
+ onHandlerError?: (
70
+ error: unknown,
71
+ eventName: string,
72
+ payload: unknown,
73
+ ) => void;
74
+ }
75
+
76
+ export interface InMemoryEventBusProviderOptions
77
+ extends Omit<InMemoryEventBusOptions, "instrumentation"> {
78
+ /**
79
+ * Provider name. Defaults to "event-bus-memory".
80
+ */
81
+ name?: string;
82
+ }
83
+
84
+ export interface InMemoryEventBusProviderPorts {
85
+ eventBus: EventBusPort;
86
+ }
87
+
88
+ /**
89
+ * Create an in-memory event bus implementation.
90
+ *
91
+ * This is suitable for single-process applications or testing.
92
+ * For distributed systems, you would implement EventBusPort with
93
+ * a message broker like RabbitMQ, Kafka, or Redis Pub/Sub.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const eventBus = createInMemoryEventBus();
98
+ *
99
+ * eventBus.subscribe(UserRegistered, (payload) => {
100
+ * console.log(`User registered: ${payload.email}`);
101
+ * });
102
+ *
103
+ * await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
104
+ * ```
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // With error handling
109
+ * const eventBus = createInMemoryEventBus({
110
+ * onHandlerError: (error, eventName, payload) => {
111
+ * logger.error(`Event handler failed for ${eventName}`, { error, payload });
112
+ * },
113
+ * });
114
+ * ```
115
+ */
116
+ export function createInMemoryEventBus(
117
+ options: InMemoryEventBusOptions = {},
118
+ ): EventBusPort {
119
+ const onHandlerError = options?.onHandlerError;
120
+ const delivery = options.delivery ?? "await-handlers";
121
+ const instrumentation = createProviderInstrumentation(
122
+ options.instrumentation,
123
+ {
124
+ providerName: "event-bus-memory",
125
+ watcher: "eventBus",
126
+ },
127
+ );
128
+
129
+ const handlers = new Map<
130
+ string,
131
+ Set<(payload: unknown) => void | Promise<void>>
132
+ >();
133
+
134
+ const reportHandlerError = (
135
+ error: unknown,
136
+ eventName: string,
137
+ payload: unknown,
138
+ ) => {
139
+ try {
140
+ onHandlerError?.(error, eventName, payload);
141
+ } catch {
142
+ // Handler error callbacks should not make publish throw or create unhandled rejections.
143
+ }
144
+ };
145
+
146
+ return {
147
+ publish(event, payload) {
148
+ instrumentation.record({
149
+ type: "eventBus",
150
+ eventName: event.name,
151
+ });
152
+
153
+ const subs = handlers.get(event.name);
154
+ if (!subs || subs.size === 0) return;
155
+
156
+ const subscribers = [...subs];
157
+
158
+ if (delivery === "fire-and-forget") {
159
+ for (const handler of subscribers) {
160
+ try {
161
+ const result = handler(payload);
162
+ Promise.resolve(result).catch((error: unknown) => {
163
+ reportHandlerError(error, event.name, payload);
164
+ });
165
+ } catch (error) {
166
+ reportHandlerError(error, event.name, payload);
167
+ }
168
+ }
169
+
170
+ return;
171
+ }
172
+
173
+ return (async () => {
174
+ for (const handler of subscribers) {
175
+ try {
176
+ await handler(payload);
177
+ } catch (error) {
178
+ reportHandlerError(error, event.name, payload);
179
+ if (!onHandlerError) throw error;
180
+ }
181
+ }
182
+ })();
183
+ },
184
+
185
+ subscribe(event, handler) {
186
+ let subs = handlers.get(event.name);
187
+ if (!subs) {
188
+ subs = new Set();
189
+ handlers.set(event.name, subs);
190
+ }
191
+
192
+ subs.add(handler as (payload: unknown) => void | Promise<void>);
193
+
194
+ return () => {
195
+ subs.delete(handler as (payload: unknown) => void | Promise<void>);
196
+ if (subs.size === 0) {
197
+ handlers.delete(event.name);
198
+ }
199
+ };
200
+ },
201
+ };
202
+ }
203
+
204
+ export function createInMemoryEventBusProvider(
205
+ options: InMemoryEventBusProviderOptions = {},
206
+ ) {
207
+ const { name = "event-bus-memory", ...eventBusOptions } = options;
208
+
209
+ return createProvider({
210
+ name,
211
+ setup({ ports }) {
212
+ return {
213
+ ports: {
214
+ eventBus: createInMemoryEventBus({
215
+ ...eventBusOptions,
216
+ instrumentation: ports,
217
+ }),
218
+ } satisfies InMemoryEventBusProviderPorts,
219
+ };
220
+ },
221
+ });
222
+ }