@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 +5 -0
- package/README.md +227 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/index.ts +222 -0
package/CHANGELOG.md
ADDED
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|