@beignet/provider-event-bus-redis 0.0.33
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +328 -0
- package/dist/index.js.map +1 -0
- package/package.json +107 -0
- package/src/index.ts +480 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @beignet/provider-event-bus-redis
|
|
2
|
+
|
|
3
|
+
## 0.0.33
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 308770a: Add opt-in live Redis integration coverage for cross-instance event delivery, unsubscribe behavior, readiness, instrumentation, and failure callbacks.
|
|
8
|
+
- 92af951: Add a Redis Pub/Sub event bus provider and `event-bus-redis` provider preset for cross-process best-effort event delivery.
|
|
9
|
+
|
|
10
|
+
## 0.0.32
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Initial Redis Pub/Sub event bus provider for cross-process best-effort
|
|
15
|
+
`EventBusPort` delivery.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Taylor Bryant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @beignet/provider-event-bus-redis
|
|
2
|
+
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> Beignet is experimental alpha software. The `0.0.x` package line is for early
|
|
5
|
+
> evaluation, and APIs may change between releases while the framework settles.
|
|
6
|
+
|
|
7
|
+
Redis Pub/Sub `EventBusPort` provider for Beignet applications.
|
|
8
|
+
|
|
9
|
+
Use it when a multi-process app needs domain events published by one process to
|
|
10
|
+
reach listeners registered in another process. Delivery is best-effort:
|
|
11
|
+
subscribers must be online, Redis does not persist messages for later replay,
|
|
12
|
+
and the provider does not acknowledge, retry, or dead-letter failed handlers.
|
|
13
|
+
Use Beignet outbox and jobs for durable side effects that need retry,
|
|
14
|
+
idempotency, or recovery after downtime.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add @beignet/provider-event-bus-redis @beignet/core ioredis
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You can also wire it into an existing app with the CLI:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
beignet providers add event-bus-redis
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
32
|
+
import { redisEventBusProvider } from "@beignet/provider-event-bus-redis";
|
|
33
|
+
import { appPorts } from "@/infra/app-ports";
|
|
34
|
+
import { routes } from "@/server/routes";
|
|
35
|
+
|
|
36
|
+
// Set environment variables:
|
|
37
|
+
// REDIS_EVENT_BUS_URL=redis://localhost:6379
|
|
38
|
+
// REDIS_EVENT_BUS_DB=0 (optional)
|
|
39
|
+
|
|
40
|
+
export const getServer = createNextServerLoader(() =>
|
|
41
|
+
createNextServer({
|
|
42
|
+
ports: appPorts,
|
|
43
|
+
providers: [redisEventBusProvider],
|
|
44
|
+
context: ({ ports }) => ({
|
|
45
|
+
ports,
|
|
46
|
+
}),
|
|
47
|
+
routes,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The provider contributes `ctx.ports.eventBus`, the standard Beignet event bus
|
|
53
|
+
port, and `ctx.ports.redisEventBus` as a Redis-specific escape hatch.
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
The provider reads environment variables with the `REDIS_EVENT_BUS_` prefix:
|
|
58
|
+
|
|
59
|
+
| Variable | Required | Description | Example |
|
|
60
|
+
| --- | --- | --- | --- |
|
|
61
|
+
| `REDIS_EVENT_BUS_URL` | Yes | Redis connection URL. | `redis://localhost:6379` |
|
|
62
|
+
| `REDIS_EVENT_BUS_DB` | No | Redis database number. Defaults to `0`. | `0` |
|
|
63
|
+
| `REDIS_EVENT_BUS_CHANNEL_PREFIX` | No | Prefix for Pub/Sub channels. Defaults to `beignet:event-bus:`. | `my-app:events:` |
|
|
64
|
+
| `REDIS_EVENT_BUS_CONNECT_TIMEOUT_MS` | No | Initial connection timeout in milliseconds. Defaults to `5000`. | `2000` |
|
|
65
|
+
| `REDIS_EVENT_BUS_MAX_RETRIES_PER_REQUEST` | No | Per-command retry limit before a command rejects. Defaults to `2`. | `1` |
|
|
66
|
+
| `REDIS_EVENT_BUS_CONNECT_MAX_ATTEMPTS` | No | Connection attempts before startup fails. Defaults to `3`. | `5` |
|
|
67
|
+
|
|
68
|
+
### Factory options
|
|
69
|
+
|
|
70
|
+
Use `createRedisEventBusProvider(options)` when the app should own connection
|
|
71
|
+
settings. Defined options override matching `REDIS_EVENT_BUS_*` environment
|
|
72
|
+
variables:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { createRedisEventBusProvider } from "@beignet/provider-event-bus-redis";
|
|
76
|
+
|
|
77
|
+
const redisEventBusProvider = createRedisEventBusProvider({
|
|
78
|
+
channelPrefix: "my-app:events:",
|
|
79
|
+
connectTimeoutMs: 2000,
|
|
80
|
+
maxRetriesPerRequest: 1,
|
|
81
|
+
db: 1,
|
|
82
|
+
retryStrategy: (times) => Math.min(times * 100, 1000),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The default `redisEventBusProvider` export is
|
|
87
|
+
`createRedisEventBusProvider()` with no options.
|
|
88
|
+
|
|
89
|
+
### Connection behavior
|
|
90
|
+
|
|
91
|
+
During startup the provider creates separate publisher and subscriber clients,
|
|
92
|
+
connects both eagerly, and fails boot with a redacted connection error when
|
|
93
|
+
Redis is unreachable. The default initial connection retry strategy stops
|
|
94
|
+
after `REDIS_EVENT_BUS_CONNECT_MAX_ATTEMPTS`; after a successful connection,
|
|
95
|
+
ioredis reconnects with capped exponential backoff.
|
|
96
|
+
|
|
97
|
+
## Direct adapter
|
|
98
|
+
|
|
99
|
+
Use `createRedisEventBus(...)` when your app owns Redis clients:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import Redis from "ioredis";
|
|
103
|
+
import { createRedisEventBus } from "@beignet/provider-event-bus-redis";
|
|
104
|
+
|
|
105
|
+
const eventBus = createRedisEventBus({
|
|
106
|
+
publisher: new Redis(process.env.REDIS_EVENT_BUS_URL),
|
|
107
|
+
subscriber: new Redis(process.env.REDIS_EVENT_BUS_URL),
|
|
108
|
+
channelPrefix: "my-app:events:",
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Register listeners during startup before handling traffic. The `EventBusPort`
|
|
113
|
+
subscription API is synchronous, while Redis subscription acknowledgement is
|
|
114
|
+
asynchronous, so messages published immediately after a new subscription may be
|
|
115
|
+
missed during startup races.
|
|
116
|
+
|
|
117
|
+
## Delivery semantics
|
|
118
|
+
|
|
119
|
+
- `publish(...)` publishes one Redis Pub/Sub message on
|
|
120
|
+
`<channelPrefix><event.name>`.
|
|
121
|
+
- Only subscribers that are online and currently subscribed receive the event.
|
|
122
|
+
- Payloads are JSON encoded and delivered to handlers without persisting the
|
|
123
|
+
message.
|
|
124
|
+
- Handler errors are reported through `onHandlerError` when provided and are
|
|
125
|
+
also recorded as provider instrumentation events; they do not reject the
|
|
126
|
+
original publisher.
|
|
127
|
+
- Invalid messages and subscription errors are reported through
|
|
128
|
+
`onSubscriberError` when provided.
|
|
129
|
+
- The provider does not retry handlers, store attempts, replay missed events,
|
|
130
|
+
or dead-letter failures.
|
|
131
|
+
|
|
132
|
+
When side effects must survive process restarts or dependency outages, record
|
|
133
|
+
the domain event through the Unit of Work outbox and drain it through jobs or
|
|
134
|
+
outbox-backed listeners.
|
|
135
|
+
|
|
136
|
+
## Escape hatch and health
|
|
137
|
+
|
|
138
|
+
`ctx.ports.redisEventBus` exposes:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const health = await ctx.ports.redisEventBus.checkHealth();
|
|
142
|
+
|
|
143
|
+
ctx.ports.redisEventBus.publisher;
|
|
144
|
+
ctx.ports.redisEventBus.subscriber;
|
|
145
|
+
ctx.ports.redisEventBus.channelPrefix;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`checkHealth()` sends cheap `PING` commands to both Redis clients and returns
|
|
149
|
+
metadata suitable for app-owned readiness endpoints.
|
|
150
|
+
|
|
151
|
+
## Instrumentation
|
|
152
|
+
|
|
153
|
+
The provider records publish events under the `eventBus` watcher. Received
|
|
154
|
+
messages and handler failures are recorded as custom provider events. Payloads
|
|
155
|
+
are not recorded.
|
|
156
|
+
|
|
157
|
+
## Testing
|
|
158
|
+
|
|
159
|
+
Use `@beignet/provider-event-bus-memory` for deterministic unit tests and
|
|
160
|
+
single-process local tests. Use this Redis provider in integration tests when
|
|
161
|
+
the behavior under test specifically depends on cross-process delivery or
|
|
162
|
+
Redis connection behavior.
|
|
163
|
+
|
|
164
|
+
This package includes opt-in live Redis integration tests for cross-instance
|
|
165
|
+
delivery, unsubscribe behavior, readiness, instrumentation, and failure
|
|
166
|
+
callbacks. The default `bun run test` command runs the hermetic unit suite only
|
|
167
|
+
so the live tests cannot share process state with the mocked `ioredis` tests.
|
|
168
|
+
Set `REDIS_EVENT_BUS_TEST_URL` or the shared `REDIS_TEST_URL` before running
|
|
169
|
+
the dedicated live test command:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
REDIS_EVENT_BUS_TEST_URL=redis://localhost:6379 bun run test:live
|
|
173
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-event-bus-redis
|
|
3
|
+
*
|
|
4
|
+
* Redis Pub/Sub event bus provider that implements EventBusPort for
|
|
5
|
+
* cross-process, best-effort domain event delivery.
|
|
6
|
+
*/
|
|
7
|
+
import type { EventBusPort } from "@beignet/core/ports";
|
|
8
|
+
import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
|
|
9
|
+
import { Redis } from "ioredis";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
declare const RedisEventBusConfigSchema: z.ZodObject<{
|
|
12
|
+
URL: z.ZodString;
|
|
13
|
+
DB: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
14
|
+
CHANNEL_PREFIX: z.ZodDefault<z.ZodString>;
|
|
15
|
+
CONNECT_TIMEOUT_MS: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
16
|
+
MAX_RETRIES_PER_REQUEST: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
17
|
+
CONNECT_MAX_ATTEMPTS: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export type RedisEventBusConfig = z.infer<typeof RedisEventBusConfigSchema>;
|
|
20
|
+
export interface RedisEventBusPublisherClient {
|
|
21
|
+
publish(channel: string, message: string): Promise<number> | number;
|
|
22
|
+
ping?(): Promise<string> | string;
|
|
23
|
+
}
|
|
24
|
+
export interface RedisEventBusSubscriberClient {
|
|
25
|
+
subscribe(...channels: string[]): Promise<unknown> | unknown;
|
|
26
|
+
unsubscribe(...channels: string[]): Promise<unknown> | unknown;
|
|
27
|
+
on(event: "message", handler: (channel: string, message: string) => void): unknown;
|
|
28
|
+
off?(event: "message", handler: (channel: string, message: string) => void): unknown;
|
|
29
|
+
removeListener?(event: "message", handler: (channel: string, message: string) => void): unknown;
|
|
30
|
+
ping?(): Promise<string> | string;
|
|
31
|
+
}
|
|
32
|
+
export interface RedisEventBusHealth {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
message?: string;
|
|
35
|
+
metadata: {
|
|
36
|
+
adapter: "redis-event-bus";
|
|
37
|
+
channelPrefix: string;
|
|
38
|
+
publisherResponse?: string;
|
|
39
|
+
subscriberResponse?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface RedisEventBusEscapeHatch {
|
|
43
|
+
publisher: RedisEventBusPublisherClient;
|
|
44
|
+
subscriber: RedisEventBusSubscriberClient;
|
|
45
|
+
channelPrefix: string;
|
|
46
|
+
checkHealth(): Promise<RedisEventBusHealth>;
|
|
47
|
+
}
|
|
48
|
+
export interface RedisEventBusProviderPorts {
|
|
49
|
+
eventBus: EventBusPort;
|
|
50
|
+
redisEventBus: RedisEventBusEscapeHatch;
|
|
51
|
+
}
|
|
52
|
+
export interface CreateRedisEventBusOptions {
|
|
53
|
+
publisher: RedisEventBusPublisherClient;
|
|
54
|
+
subscriber: RedisEventBusSubscriberClient;
|
|
55
|
+
channelPrefix?: string;
|
|
56
|
+
instrumentation?: ProviderInstrumentationTarget;
|
|
57
|
+
onHandlerError?: (error: unknown, eventName: string, payload: unknown) => void;
|
|
58
|
+
onSubscriberError?: (error: unknown) => void;
|
|
59
|
+
}
|
|
60
|
+
export interface CreateRedisEventBusProviderOptions {
|
|
61
|
+
channelPrefix?: string;
|
|
62
|
+
connectTimeoutMs?: number;
|
|
63
|
+
maxRetriesPerRequest?: number;
|
|
64
|
+
retryStrategy?: (times: number) => number | null;
|
|
65
|
+
db?: number;
|
|
66
|
+
}
|
|
67
|
+
export declare function createRedisEventBus(options: CreateRedisEventBusOptions): EventBusPort;
|
|
68
|
+
export declare function createRedisEventBusProvider(options?: CreateRedisEventBusProviderOptions): import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
|
|
69
|
+
URL: z.ZodString;
|
|
70
|
+
DB: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
71
|
+
CHANNEL_PREFIX: z.ZodDefault<z.ZodString>;
|
|
72
|
+
CONNECT_TIMEOUT_MS: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
73
|
+
MAX_RETRIES_PER_REQUEST: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
74
|
+
CONNECT_MAX_ATTEMPTS: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
75
|
+
}, z.core.$strip>, {
|
|
76
|
+
eventBus: EventBusPort;
|
|
77
|
+
redisEventBus: {
|
|
78
|
+
publisher: Redis;
|
|
79
|
+
subscriber: Redis;
|
|
80
|
+
channelPrefix: string;
|
|
81
|
+
checkHealth: () => Promise<RedisEventBusHealth>;
|
|
82
|
+
};
|
|
83
|
+
}, unknown, void>;
|
|
84
|
+
export declare const redisEventBusProvider: import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
|
|
85
|
+
URL: z.ZodString;
|
|
86
|
+
DB: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
87
|
+
CHANNEL_PREFIX: z.ZodDefault<z.ZodString>;
|
|
88
|
+
CONNECT_TIMEOUT_MS: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
89
|
+
MAX_RETRIES_PER_REQUEST: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
90
|
+
CONNECT_MAX_ATTEMPTS: z.ZodDefault<z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodNumber]>>;
|
|
91
|
+
}, z.core.$strip>, {
|
|
92
|
+
eventBus: EventBusPort;
|
|
93
|
+
redisEventBus: {
|
|
94
|
+
publisher: Redis;
|
|
95
|
+
subscriber: Redis;
|
|
96
|
+
channelPrefix: string;
|
|
97
|
+
checkHealth: () => Promise<RedisEventBusHealth>;
|
|
98
|
+
};
|
|
99
|
+
}, unknown, void>;
|
|
100
|
+
export {};
|
|
101
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-event-bus-redis
|
|
3
|
+
*
|
|
4
|
+
* Redis Pub/Sub event bus provider that implements EventBusPort for
|
|
5
|
+
* cross-process, best-effort domain event delivery.
|
|
6
|
+
*/
|
|
7
|
+
import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
|
|
8
|
+
import { Redis } from "ioredis";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
11
|
+
const DEFAULT_MAX_RETRIES_PER_REQUEST = 2;
|
|
12
|
+
const DEFAULT_CONNECT_MAX_ATTEMPTS = 3;
|
|
13
|
+
const DEFAULT_CHANNEL_PREFIX = "beignet:event-bus:";
|
|
14
|
+
const RECONNECT_BACKOFF_CAP_MS = 2000;
|
|
15
|
+
const NumberString = z.union([
|
|
16
|
+
z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^\d+$/, "Expected a non-negative integer string")
|
|
19
|
+
.transform((value) => Number.parseInt(value, 10)),
|
|
20
|
+
z.number().int().nonnegative(),
|
|
21
|
+
]);
|
|
22
|
+
const RedisEventBusConfigSchema = z.object({
|
|
23
|
+
URL: z.string().url(),
|
|
24
|
+
DB: NumberString.optional(),
|
|
25
|
+
CHANNEL_PREFIX: z.string().default(DEFAULT_CHANNEL_PREFIX),
|
|
26
|
+
CONNECT_TIMEOUT_MS: NumberString.default(DEFAULT_CONNECT_TIMEOUT_MS),
|
|
27
|
+
MAX_RETRIES_PER_REQUEST: NumberString.default(DEFAULT_MAX_RETRIES_PER_REQUEST),
|
|
28
|
+
CONNECT_MAX_ATTEMPTS: NumberString.default(DEFAULT_CONNECT_MAX_ATTEMPTS),
|
|
29
|
+
});
|
|
30
|
+
function errorMessage(error) {
|
|
31
|
+
return error instanceof Error ? error.message : String(error);
|
|
32
|
+
}
|
|
33
|
+
function redactConnectionUrl(url) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
if (parsed.username)
|
|
37
|
+
parsed.username = "redacted";
|
|
38
|
+
if (parsed.password)
|
|
39
|
+
parsed.password = "redacted";
|
|
40
|
+
if (parsed.search)
|
|
41
|
+
parsed.search = "?redacted";
|
|
42
|
+
parsed.hash = "";
|
|
43
|
+
return parsed.toString();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return "[invalid URL]";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function reconnectBackoff(attempt) {
|
|
50
|
+
return Math.min(2 ** attempt * 100, RECONNECT_BACKOFF_CAP_MS);
|
|
51
|
+
}
|
|
52
|
+
function createRetryStrategy(connectMaxAttempts) {
|
|
53
|
+
let connectedOnce = false;
|
|
54
|
+
return {
|
|
55
|
+
strategy(attempt) {
|
|
56
|
+
if (!connectedOnce && attempt >= connectMaxAttempts) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return reconnectBackoff(attempt);
|
|
60
|
+
},
|
|
61
|
+
markConnected() {
|
|
62
|
+
connectedOnce = true;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function channelForEvent(channelPrefix, eventName) {
|
|
67
|
+
return `${channelPrefix}${eventName}`;
|
|
68
|
+
}
|
|
69
|
+
function eventNameForChannel(channelPrefix, channel) {
|
|
70
|
+
if (channelPrefix === "")
|
|
71
|
+
return channel;
|
|
72
|
+
if (!channel.startsWith(channelPrefix))
|
|
73
|
+
return undefined;
|
|
74
|
+
return channel.slice(channelPrefix.length);
|
|
75
|
+
}
|
|
76
|
+
function safeJsonParse(input) {
|
|
77
|
+
return JSON.parse(input);
|
|
78
|
+
}
|
|
79
|
+
async function checkRedisEventBusHealth(publisher, subscriber, channelPrefix) {
|
|
80
|
+
try {
|
|
81
|
+
const [publisherResponse, subscriberResponse] = await Promise.all([
|
|
82
|
+
publisher.ping ? Promise.resolve(publisher.ping()) : Promise.resolve(""),
|
|
83
|
+
subscriber.ping
|
|
84
|
+
? Promise.resolve(subscriber.ping())
|
|
85
|
+
: Promise.resolve(""),
|
|
86
|
+
]);
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
metadata: {
|
|
90
|
+
adapter: "redis-event-bus",
|
|
91
|
+
channelPrefix,
|
|
92
|
+
...(publisherResponse ? { publisherResponse } : {}),
|
|
93
|
+
...(subscriberResponse ? { subscriberResponse } : {}),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
message: errorMessage(error),
|
|
101
|
+
metadata: {
|
|
102
|
+
adapter: "redis-event-bus",
|
|
103
|
+
channelPrefix,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function createRedisEventBus(options) {
|
|
109
|
+
const channelPrefix = options.channelPrefix ?? DEFAULT_CHANNEL_PREFIX;
|
|
110
|
+
const handlers = new Map();
|
|
111
|
+
const subscribedChannels = new Set();
|
|
112
|
+
const instrumentation = createProviderInstrumentation(options.instrumentation, {
|
|
113
|
+
providerName: "event-bus-redis",
|
|
114
|
+
watcher: "eventBus",
|
|
115
|
+
});
|
|
116
|
+
const reportSubscriberError = (error) => {
|
|
117
|
+
try {
|
|
118
|
+
options.onSubscriberError?.(error);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Error callbacks should not create unhandled rejections.
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const reportHandlerError = (error, eventName, payload) => {
|
|
125
|
+
instrumentation.custom({
|
|
126
|
+
name: "eventBus.handler.failed",
|
|
127
|
+
label: "Event handler failed",
|
|
128
|
+
summary: `Handler failed for "${eventName}"`,
|
|
129
|
+
details: {
|
|
130
|
+
eventName,
|
|
131
|
+
error: errorMessage(error),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
options.onHandlerError?.(error, eventName, payload);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Error callbacks should not create unhandled rejections.
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const messageHandler = (channel, message) => {
|
|
142
|
+
const eventName = eventNameForChannel(channelPrefix, channel);
|
|
143
|
+
if (!eventName)
|
|
144
|
+
return;
|
|
145
|
+
const eventHandlers = handlers.get(eventName);
|
|
146
|
+
if (!eventHandlers || eventHandlers.size === 0)
|
|
147
|
+
return;
|
|
148
|
+
let payload;
|
|
149
|
+
try {
|
|
150
|
+
const parsed = safeJsonParse(message);
|
|
151
|
+
if (typeof parsed !== "object" ||
|
|
152
|
+
parsed === null ||
|
|
153
|
+
!("payload" in parsed)) {
|
|
154
|
+
throw new Error("Redis event bus message is missing a payload field.");
|
|
155
|
+
}
|
|
156
|
+
payload = parsed.payload;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
reportSubscriberError(error);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
instrumentation.custom({
|
|
163
|
+
name: "eventBus.received",
|
|
164
|
+
label: "Event received",
|
|
165
|
+
summary: `Received "${eventName}"`,
|
|
166
|
+
details: { eventName },
|
|
167
|
+
});
|
|
168
|
+
for (const handler of [...eventHandlers]) {
|
|
169
|
+
try {
|
|
170
|
+
Promise.resolve(handler(payload)).catch((error) => {
|
|
171
|
+
reportHandlerError(error, eventName, payload);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
reportHandlerError(error, eventName, payload);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
options.subscriber.on("message", messageHandler);
|
|
180
|
+
const ensureSubscribed = (eventName) => {
|
|
181
|
+
const channel = channelForEvent(channelPrefix, eventName);
|
|
182
|
+
if (subscribedChannels.has(channel))
|
|
183
|
+
return;
|
|
184
|
+
subscribedChannels.add(channel);
|
|
185
|
+
Promise.resolve(options.subscriber.subscribe(channel)).catch((error) => {
|
|
186
|
+
subscribedChannels.delete(channel);
|
|
187
|
+
reportSubscriberError(error);
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
const unsubscribeChannel = (eventName) => {
|
|
191
|
+
const channel = channelForEvent(channelPrefix, eventName);
|
|
192
|
+
if (!subscribedChannels.delete(channel))
|
|
193
|
+
return;
|
|
194
|
+
Promise.resolve(options.subscriber.unsubscribe(channel)).catch(reportSubscriberError);
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
async publish(event, payload) {
|
|
198
|
+
instrumentation.record({
|
|
199
|
+
type: "eventBus",
|
|
200
|
+
eventName: event.name,
|
|
201
|
+
});
|
|
202
|
+
await options.publisher.publish(channelForEvent(channelPrefix, event.name), JSON.stringify({ eventName: event.name, payload }));
|
|
203
|
+
},
|
|
204
|
+
subscribe(event, handler) {
|
|
205
|
+
let eventHandlers = handlers.get(event.name);
|
|
206
|
+
if (!eventHandlers) {
|
|
207
|
+
eventHandlers = new Set();
|
|
208
|
+
handlers.set(event.name, eventHandlers);
|
|
209
|
+
}
|
|
210
|
+
eventHandlers.add(handler);
|
|
211
|
+
ensureSubscribed(event.name);
|
|
212
|
+
return () => {
|
|
213
|
+
const currentHandlers = handlers.get(event.name);
|
|
214
|
+
if (!currentHandlers)
|
|
215
|
+
return;
|
|
216
|
+
currentHandlers.delete(handler);
|
|
217
|
+
if (currentHandlers.size === 0) {
|
|
218
|
+
handlers.delete(event.name);
|
|
219
|
+
unsubscribeChannel(event.name);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export function createRedisEventBusProvider(options = {}) {
|
|
226
|
+
const providerMetadata = {
|
|
227
|
+
packageName: "@beignet/provider-event-bus-redis",
|
|
228
|
+
ports: ["eventBus", "redisEventBus"],
|
|
229
|
+
env: [
|
|
230
|
+
"REDIS_EVENT_BUS_URL",
|
|
231
|
+
"REDIS_EVENT_BUS_DB",
|
|
232
|
+
"REDIS_EVENT_BUS_CHANNEL_PREFIX",
|
|
233
|
+
"REDIS_EVENT_BUS_CONNECT_TIMEOUT_MS",
|
|
234
|
+
"REDIS_EVENT_BUS_MAX_RETRIES_PER_REQUEST",
|
|
235
|
+
"REDIS_EVENT_BUS_CONNECT_MAX_ATTEMPTS",
|
|
236
|
+
],
|
|
237
|
+
watchers: ["eventBus"],
|
|
238
|
+
};
|
|
239
|
+
return createProvider({
|
|
240
|
+
name: "event-bus-redis",
|
|
241
|
+
metadata: providerMetadata,
|
|
242
|
+
config: {
|
|
243
|
+
schema: RedisEventBusConfigSchema,
|
|
244
|
+
envPrefix: "REDIS_EVENT_BUS_",
|
|
245
|
+
// Defined options win over env-derived values at config load time.
|
|
246
|
+
overrides: {
|
|
247
|
+
DB: options.db,
|
|
248
|
+
CHANNEL_PREFIX: options.channelPrefix,
|
|
249
|
+
CONNECT_TIMEOUT_MS: options.connectTimeoutMs,
|
|
250
|
+
MAX_RETRIES_PER_REQUEST: options.maxRetriesPerRequest,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
async setup({ config, ports }) {
|
|
254
|
+
if (!config) {
|
|
255
|
+
throw new Error("[redisEventBusProvider] Missing Redis event bus config. " +
|
|
256
|
+
"Please set REDIS_EVENT_BUS_URL.");
|
|
257
|
+
}
|
|
258
|
+
const configuredChannelPrefix = config.CHANNEL_PREFIX ??
|
|
259
|
+
options.channelPrefix ??
|
|
260
|
+
DEFAULT_CHANNEL_PREFIX;
|
|
261
|
+
const connectMaxAttempts = config.CONNECT_MAX_ATTEMPTS ?? DEFAULT_CONNECT_MAX_ATTEMPTS;
|
|
262
|
+
const publisherRetry = createRetryStrategy(connectMaxAttempts);
|
|
263
|
+
const subscriberRetry = createRetryStrategy(connectMaxAttempts);
|
|
264
|
+
const redisOptions = {
|
|
265
|
+
db: config.DB ?? options.db ?? 0,
|
|
266
|
+
lazyConnect: true,
|
|
267
|
+
connectTimeout: config.CONNECT_TIMEOUT_MS ??
|
|
268
|
+
options.connectTimeoutMs ??
|
|
269
|
+
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
270
|
+
maxRetriesPerRequest: config.MAX_RETRIES_PER_REQUEST ??
|
|
271
|
+
options.maxRetriesPerRequest ??
|
|
272
|
+
DEFAULT_MAX_RETRIES_PER_REQUEST,
|
|
273
|
+
};
|
|
274
|
+
const publisher = new Redis(config.URL, {
|
|
275
|
+
...redisOptions,
|
|
276
|
+
retryStrategy: options.retryStrategy ?? publisherRetry.strategy,
|
|
277
|
+
});
|
|
278
|
+
const subscriber = new Redis(config.URL, {
|
|
279
|
+
...redisOptions,
|
|
280
|
+
retryStrategy: options.retryStrategy ?? subscriberRetry.strategy,
|
|
281
|
+
});
|
|
282
|
+
try {
|
|
283
|
+
await publisher.connect();
|
|
284
|
+
publisherRetry.markConnected();
|
|
285
|
+
await subscriber.connect();
|
|
286
|
+
subscriberRetry.markConnected();
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
publisher.disconnect();
|
|
290
|
+
subscriber.disconnect();
|
|
291
|
+
throw new Error(`[redisEventBusProvider] Failed to connect to Redis at ${redactConnectionUrl(config.URL)}: ${errorMessage(error)}`);
|
|
292
|
+
}
|
|
293
|
+
const eventBus = createRedisEventBus({
|
|
294
|
+
publisher,
|
|
295
|
+
subscriber,
|
|
296
|
+
channelPrefix: configuredChannelPrefix,
|
|
297
|
+
instrumentation: ports,
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
ports: {
|
|
301
|
+
eventBus,
|
|
302
|
+
redisEventBus: {
|
|
303
|
+
publisher,
|
|
304
|
+
subscriber,
|
|
305
|
+
channelPrefix: configuredChannelPrefix,
|
|
306
|
+
checkHealth: () => checkRedisEventBusHealth(publisher, subscriber, configuredChannelPrefix),
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
async stop() {
|
|
310
|
+
try {
|
|
311
|
+
await subscriber.quit();
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
subscriber.disconnect();
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
await publisher.quit();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
publisher.disconnect();
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
export const redisEventBusProvider = createRedisEventBusProvider();
|
|
328
|
+
//# 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,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beignet/provider-event-bus-redis",
|
|
3
|
+
"version": "0.0.33",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Redis Pub/Sub event bus provider for Beignet - implements EventBusPort for cross-process best-effort delivery",
|
|
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
|
+
"prepack": "bun run build",
|
|
26
|
+
"dev": "tsc --watch",
|
|
27
|
+
"clean": "rm -rf dist coverage .turbo",
|
|
28
|
+
"test": "bun test src/index.test.ts",
|
|
29
|
+
"test:live": "bun test src/redis-live.test.ts",
|
|
30
|
+
"test:coverage": "bun test --coverage src/index.test.ts",
|
|
31
|
+
"lint": "biome check ."
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"beignet",
|
|
35
|
+
"event-bus",
|
|
36
|
+
"redis",
|
|
37
|
+
"provider",
|
|
38
|
+
"pubsub",
|
|
39
|
+
"ports"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/taylorbryant/beignet.git",
|
|
45
|
+
"directory": "packages/provider-event-bus-redis"
|
|
46
|
+
},
|
|
47
|
+
"author": "Taylor Bryant",
|
|
48
|
+
"homepage": "https://github.com/taylorbryant/beignet#readme",
|
|
49
|
+
"bugs": "https://github.com/taylorbryant/beignet/issues",
|
|
50
|
+
"sideEffects": false,
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@beignet/core": ">=0.0.3 <1.0.0",
|
|
59
|
+
"ioredis": "^5.0.0"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"zod": "^4.0.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@beignet/devtools": "*",
|
|
66
|
+
"@types/bun": "^1.3.13",
|
|
67
|
+
"@types/node": "^20.10.0",
|
|
68
|
+
"ioredis": "^5.0.0",
|
|
69
|
+
"typescript": "^5.3.0"
|
|
70
|
+
},
|
|
71
|
+
"beignet": {
|
|
72
|
+
"provider": {
|
|
73
|
+
"displayName": "Redis event bus provider",
|
|
74
|
+
"ports": [
|
|
75
|
+
"eventBus",
|
|
76
|
+
"redisEventBus"
|
|
77
|
+
],
|
|
78
|
+
"appPorts": [
|
|
79
|
+
{
|
|
80
|
+
"name": "eventBus",
|
|
81
|
+
"type": "EventBusPort"
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"env": [
|
|
85
|
+
"REDIS_EVENT_BUS_URL",
|
|
86
|
+
"REDIS_EVENT_BUS_DB",
|
|
87
|
+
"REDIS_EVENT_BUS_CHANNEL_PREFIX",
|
|
88
|
+
"REDIS_EVENT_BUS_CONNECT_TIMEOUT_MS",
|
|
89
|
+
"REDIS_EVENT_BUS_MAX_RETRIES_PER_REQUEST",
|
|
90
|
+
"REDIS_EVENT_BUS_CONNECT_MAX_ATTEMPTS"
|
|
91
|
+
],
|
|
92
|
+
"requiredEnv": [
|
|
93
|
+
"REDIS_EVENT_BUS_URL"
|
|
94
|
+
],
|
|
95
|
+
"watchers": [
|
|
96
|
+
"eventBus"
|
|
97
|
+
],
|
|
98
|
+
"registration": {
|
|
99
|
+
"required": true,
|
|
100
|
+
"tokens": [
|
|
101
|
+
"redisEventBusProvider",
|
|
102
|
+
"createRedisEventBusProvider"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-event-bus-redis
|
|
3
|
+
*
|
|
4
|
+
* Redis Pub/Sub event bus provider that implements EventBusPort for
|
|
5
|
+
* cross-process, best-effort domain event delivery.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
DomainEventDef,
|
|
10
|
+
EventBusPort,
|
|
11
|
+
InferEventPayload,
|
|
12
|
+
} from "@beignet/core/ports";
|
|
13
|
+
import {
|
|
14
|
+
createProvider,
|
|
15
|
+
createProviderInstrumentation,
|
|
16
|
+
type ProviderInstrumentationTarget,
|
|
17
|
+
} from "@beignet/core/providers";
|
|
18
|
+
import { Redis } from "ioredis";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
22
|
+
const DEFAULT_MAX_RETRIES_PER_REQUEST = 2;
|
|
23
|
+
const DEFAULT_CONNECT_MAX_ATTEMPTS = 3;
|
|
24
|
+
const DEFAULT_CHANNEL_PREFIX = "beignet:event-bus:";
|
|
25
|
+
const RECONNECT_BACKOFF_CAP_MS = 2000;
|
|
26
|
+
|
|
27
|
+
const NumberString = z.union([
|
|
28
|
+
z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^\d+$/, "Expected a non-negative integer string")
|
|
31
|
+
.transform((value) => Number.parseInt(value, 10)),
|
|
32
|
+
z.number().int().nonnegative(),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const RedisEventBusConfigSchema = z.object({
|
|
36
|
+
URL: z.string().url(),
|
|
37
|
+
DB: NumberString.optional(),
|
|
38
|
+
CHANNEL_PREFIX: z.string().default(DEFAULT_CHANNEL_PREFIX),
|
|
39
|
+
CONNECT_TIMEOUT_MS: NumberString.default(DEFAULT_CONNECT_TIMEOUT_MS),
|
|
40
|
+
MAX_RETRIES_PER_REQUEST: NumberString.default(
|
|
41
|
+
DEFAULT_MAX_RETRIES_PER_REQUEST,
|
|
42
|
+
),
|
|
43
|
+
CONNECT_MAX_ATTEMPTS: NumberString.default(DEFAULT_CONNECT_MAX_ATTEMPTS),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type RedisEventBusConfig = z.infer<typeof RedisEventBusConfigSchema>;
|
|
47
|
+
|
|
48
|
+
export interface RedisEventBusPublisherClient {
|
|
49
|
+
publish(channel: string, message: string): Promise<number> | number;
|
|
50
|
+
ping?(): Promise<string> | string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RedisEventBusSubscriberClient {
|
|
54
|
+
subscribe(...channels: string[]): Promise<unknown> | unknown;
|
|
55
|
+
unsubscribe(...channels: string[]): Promise<unknown> | unknown;
|
|
56
|
+
on(
|
|
57
|
+
event: "message",
|
|
58
|
+
handler: (channel: string, message: string) => void,
|
|
59
|
+
): unknown;
|
|
60
|
+
off?(
|
|
61
|
+
event: "message",
|
|
62
|
+
handler: (channel: string, message: string) => void,
|
|
63
|
+
): unknown;
|
|
64
|
+
removeListener?(
|
|
65
|
+
event: "message",
|
|
66
|
+
handler: (channel: string, message: string) => void,
|
|
67
|
+
): unknown;
|
|
68
|
+
ping?(): Promise<string> | string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface RedisEventBusHealth {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
message?: string;
|
|
74
|
+
metadata: {
|
|
75
|
+
adapter: "redis-event-bus";
|
|
76
|
+
channelPrefix: string;
|
|
77
|
+
publisherResponse?: string;
|
|
78
|
+
subscriberResponse?: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RedisEventBusEscapeHatch {
|
|
83
|
+
publisher: RedisEventBusPublisherClient;
|
|
84
|
+
subscriber: RedisEventBusSubscriberClient;
|
|
85
|
+
channelPrefix: string;
|
|
86
|
+
checkHealth(): Promise<RedisEventBusHealth>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RedisEventBusProviderPorts {
|
|
90
|
+
eventBus: EventBusPort;
|
|
91
|
+
redisEventBus: RedisEventBusEscapeHatch;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CreateRedisEventBusOptions {
|
|
95
|
+
publisher: RedisEventBusPublisherClient;
|
|
96
|
+
subscriber: RedisEventBusSubscriberClient;
|
|
97
|
+
channelPrefix?: string;
|
|
98
|
+
instrumentation?: ProviderInstrumentationTarget;
|
|
99
|
+
onHandlerError?: (
|
|
100
|
+
error: unknown,
|
|
101
|
+
eventName: string,
|
|
102
|
+
payload: unknown,
|
|
103
|
+
) => void;
|
|
104
|
+
onSubscriberError?: (error: unknown) => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface CreateRedisEventBusProviderOptions {
|
|
108
|
+
channelPrefix?: string;
|
|
109
|
+
connectTimeoutMs?: number;
|
|
110
|
+
maxRetriesPerRequest?: number;
|
|
111
|
+
retryStrategy?: (times: number) => number | null;
|
|
112
|
+
db?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function errorMessage(error: unknown): string {
|
|
116
|
+
return error instanceof Error ? error.message : String(error);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function redactConnectionUrl(url: string): string {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = new URL(url);
|
|
122
|
+
if (parsed.username) parsed.username = "redacted";
|
|
123
|
+
if (parsed.password) parsed.password = "redacted";
|
|
124
|
+
if (parsed.search) parsed.search = "?redacted";
|
|
125
|
+
parsed.hash = "";
|
|
126
|
+
return parsed.toString();
|
|
127
|
+
} catch {
|
|
128
|
+
return "[invalid URL]";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function reconnectBackoff(attempt: number): number {
|
|
133
|
+
return Math.min(2 ** attempt * 100, RECONNECT_BACKOFF_CAP_MS);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createRetryStrategy(connectMaxAttempts: number): {
|
|
137
|
+
strategy: (attempt: number) => number | null;
|
|
138
|
+
markConnected: () => void;
|
|
139
|
+
} {
|
|
140
|
+
let connectedOnce = false;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
strategy(attempt) {
|
|
144
|
+
if (!connectedOnce && attempt >= connectMaxAttempts) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return reconnectBackoff(attempt);
|
|
148
|
+
},
|
|
149
|
+
markConnected() {
|
|
150
|
+
connectedOnce = true;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function channelForEvent(channelPrefix: string, eventName: string): string {
|
|
156
|
+
return `${channelPrefix}${eventName}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function eventNameForChannel(
|
|
160
|
+
channelPrefix: string,
|
|
161
|
+
channel: string,
|
|
162
|
+
): string | undefined {
|
|
163
|
+
if (channelPrefix === "") return channel;
|
|
164
|
+
if (!channel.startsWith(channelPrefix)) return undefined;
|
|
165
|
+
return channel.slice(channelPrefix.length);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function safeJsonParse(input: string): unknown {
|
|
169
|
+
return JSON.parse(input) as unknown;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function checkRedisEventBusHealth(
|
|
173
|
+
publisher: RedisEventBusPublisherClient,
|
|
174
|
+
subscriber: RedisEventBusSubscriberClient,
|
|
175
|
+
channelPrefix: string,
|
|
176
|
+
): Promise<RedisEventBusHealth> {
|
|
177
|
+
try {
|
|
178
|
+
const [publisherResponse, subscriberResponse] = await Promise.all([
|
|
179
|
+
publisher.ping ? Promise.resolve(publisher.ping()) : Promise.resolve(""),
|
|
180
|
+
subscriber.ping
|
|
181
|
+
? Promise.resolve(subscriber.ping())
|
|
182
|
+
: Promise.resolve(""),
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
metadata: {
|
|
188
|
+
adapter: "redis-event-bus",
|
|
189
|
+
channelPrefix,
|
|
190
|
+
...(publisherResponse ? { publisherResponse } : {}),
|
|
191
|
+
...(subscriberResponse ? { subscriberResponse } : {}),
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
message: errorMessage(error),
|
|
198
|
+
metadata: {
|
|
199
|
+
adapter: "redis-event-bus",
|
|
200
|
+
channelPrefix,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createRedisEventBus(
|
|
207
|
+
options: CreateRedisEventBusOptions,
|
|
208
|
+
): EventBusPort {
|
|
209
|
+
const channelPrefix = options.channelPrefix ?? DEFAULT_CHANNEL_PREFIX;
|
|
210
|
+
const handlers = new Map<
|
|
211
|
+
string,
|
|
212
|
+
Set<(payload: unknown) => void | Promise<void>>
|
|
213
|
+
>();
|
|
214
|
+
const subscribedChannels = new Set<string>();
|
|
215
|
+
const instrumentation = createProviderInstrumentation(
|
|
216
|
+
options.instrumentation,
|
|
217
|
+
{
|
|
218
|
+
providerName: "event-bus-redis",
|
|
219
|
+
watcher: "eventBus",
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const reportSubscriberError = (error: unknown) => {
|
|
224
|
+
try {
|
|
225
|
+
options.onSubscriberError?.(error);
|
|
226
|
+
} catch {
|
|
227
|
+
// Error callbacks should not create unhandled rejections.
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const reportHandlerError = (
|
|
232
|
+
error: unknown,
|
|
233
|
+
eventName: string,
|
|
234
|
+
payload: unknown,
|
|
235
|
+
) => {
|
|
236
|
+
instrumentation.custom({
|
|
237
|
+
name: "eventBus.handler.failed",
|
|
238
|
+
label: "Event handler failed",
|
|
239
|
+
summary: `Handler failed for "${eventName}"`,
|
|
240
|
+
details: {
|
|
241
|
+
eventName,
|
|
242
|
+
error: errorMessage(error),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
options.onHandlerError?.(error, eventName, payload);
|
|
248
|
+
} catch {
|
|
249
|
+
// Error callbacks should not create unhandled rejections.
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const messageHandler = (channel: string, message: string) => {
|
|
254
|
+
const eventName = eventNameForChannel(channelPrefix, channel);
|
|
255
|
+
if (!eventName) return;
|
|
256
|
+
|
|
257
|
+
const eventHandlers = handlers.get(eventName);
|
|
258
|
+
if (!eventHandlers || eventHandlers.size === 0) return;
|
|
259
|
+
|
|
260
|
+
let payload: unknown;
|
|
261
|
+
try {
|
|
262
|
+
const parsed = safeJsonParse(message);
|
|
263
|
+
if (
|
|
264
|
+
typeof parsed !== "object" ||
|
|
265
|
+
parsed === null ||
|
|
266
|
+
!("payload" in parsed)
|
|
267
|
+
) {
|
|
268
|
+
throw new Error("Redis event bus message is missing a payload field.");
|
|
269
|
+
}
|
|
270
|
+
payload = (parsed as { payload: unknown }).payload;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
reportSubscriberError(error);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
instrumentation.custom({
|
|
277
|
+
name: "eventBus.received",
|
|
278
|
+
label: "Event received",
|
|
279
|
+
summary: `Received "${eventName}"`,
|
|
280
|
+
details: { eventName },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
for (const handler of [...eventHandlers]) {
|
|
284
|
+
try {
|
|
285
|
+
Promise.resolve(handler(payload)).catch((error: unknown) => {
|
|
286
|
+
reportHandlerError(error, eventName, payload);
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
reportHandlerError(error, eventName, payload);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
options.subscriber.on("message", messageHandler);
|
|
295
|
+
|
|
296
|
+
const ensureSubscribed = (eventName: string) => {
|
|
297
|
+
const channel = channelForEvent(channelPrefix, eventName);
|
|
298
|
+
if (subscribedChannels.has(channel)) return;
|
|
299
|
+
subscribedChannels.add(channel);
|
|
300
|
+
Promise.resolve(options.subscriber.subscribe(channel)).catch((error) => {
|
|
301
|
+
subscribedChannels.delete(channel);
|
|
302
|
+
reportSubscriberError(error);
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const unsubscribeChannel = (eventName: string) => {
|
|
307
|
+
const channel = channelForEvent(channelPrefix, eventName);
|
|
308
|
+
if (!subscribedChannels.delete(channel)) return;
|
|
309
|
+
Promise.resolve(options.subscriber.unsubscribe(channel)).catch(
|
|
310
|
+
reportSubscriberError,
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
async publish(event, payload) {
|
|
316
|
+
instrumentation.record({
|
|
317
|
+
type: "eventBus",
|
|
318
|
+
eventName: event.name,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await options.publisher.publish(
|
|
322
|
+
channelForEvent(channelPrefix, event.name),
|
|
323
|
+
JSON.stringify({ eventName: event.name, payload }),
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
subscribe<E extends DomainEventDef>(
|
|
328
|
+
event: E,
|
|
329
|
+
handler: (payload: InferEventPayload<E>) => void | Promise<void>,
|
|
330
|
+
) {
|
|
331
|
+
let eventHandlers = handlers.get(event.name);
|
|
332
|
+
if (!eventHandlers) {
|
|
333
|
+
eventHandlers = new Set();
|
|
334
|
+
handlers.set(event.name, eventHandlers);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
eventHandlers.add(handler as (payload: unknown) => void | Promise<void>);
|
|
338
|
+
ensureSubscribed(event.name);
|
|
339
|
+
|
|
340
|
+
return () => {
|
|
341
|
+
const currentHandlers = handlers.get(event.name);
|
|
342
|
+
if (!currentHandlers) return;
|
|
343
|
+
currentHandlers.delete(
|
|
344
|
+
handler as (payload: unknown) => void | Promise<void>,
|
|
345
|
+
);
|
|
346
|
+
if (currentHandlers.size === 0) {
|
|
347
|
+
handlers.delete(event.name);
|
|
348
|
+
unsubscribeChannel(event.name);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function createRedisEventBusProvider(
|
|
356
|
+
options: CreateRedisEventBusProviderOptions = {},
|
|
357
|
+
) {
|
|
358
|
+
const providerMetadata = {
|
|
359
|
+
packageName: "@beignet/provider-event-bus-redis",
|
|
360
|
+
ports: ["eventBus", "redisEventBus"],
|
|
361
|
+
env: [
|
|
362
|
+
"REDIS_EVENT_BUS_URL",
|
|
363
|
+
"REDIS_EVENT_BUS_DB",
|
|
364
|
+
"REDIS_EVENT_BUS_CHANNEL_PREFIX",
|
|
365
|
+
"REDIS_EVENT_BUS_CONNECT_TIMEOUT_MS",
|
|
366
|
+
"REDIS_EVENT_BUS_MAX_RETRIES_PER_REQUEST",
|
|
367
|
+
"REDIS_EVENT_BUS_CONNECT_MAX_ATTEMPTS",
|
|
368
|
+
],
|
|
369
|
+
watchers: ["eventBus"],
|
|
370
|
+
} as const;
|
|
371
|
+
|
|
372
|
+
return createProvider({
|
|
373
|
+
name: "event-bus-redis",
|
|
374
|
+
metadata: providerMetadata,
|
|
375
|
+
|
|
376
|
+
config: {
|
|
377
|
+
schema: RedisEventBusConfigSchema,
|
|
378
|
+
envPrefix: "REDIS_EVENT_BUS_",
|
|
379
|
+
// Defined options win over env-derived values at config load time.
|
|
380
|
+
overrides: {
|
|
381
|
+
DB: options.db,
|
|
382
|
+
CHANNEL_PREFIX: options.channelPrefix,
|
|
383
|
+
CONNECT_TIMEOUT_MS: options.connectTimeoutMs,
|
|
384
|
+
MAX_RETRIES_PER_REQUEST: options.maxRetriesPerRequest,
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
async setup({ config, ports }) {
|
|
389
|
+
if (!config) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
"[redisEventBusProvider] Missing Redis event bus config. " +
|
|
392
|
+
"Please set REDIS_EVENT_BUS_URL.",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const configuredChannelPrefix =
|
|
397
|
+
config.CHANNEL_PREFIX ??
|
|
398
|
+
options.channelPrefix ??
|
|
399
|
+
DEFAULT_CHANNEL_PREFIX;
|
|
400
|
+
const connectMaxAttempts =
|
|
401
|
+
config.CONNECT_MAX_ATTEMPTS ?? DEFAULT_CONNECT_MAX_ATTEMPTS;
|
|
402
|
+
const publisherRetry = createRetryStrategy(connectMaxAttempts);
|
|
403
|
+
const subscriberRetry = createRetryStrategy(connectMaxAttempts);
|
|
404
|
+
const redisOptions = {
|
|
405
|
+
db: config.DB ?? options.db ?? 0,
|
|
406
|
+
lazyConnect: true,
|
|
407
|
+
connectTimeout:
|
|
408
|
+
config.CONNECT_TIMEOUT_MS ??
|
|
409
|
+
options.connectTimeoutMs ??
|
|
410
|
+
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
411
|
+
maxRetriesPerRequest:
|
|
412
|
+
config.MAX_RETRIES_PER_REQUEST ??
|
|
413
|
+
options.maxRetriesPerRequest ??
|
|
414
|
+
DEFAULT_MAX_RETRIES_PER_REQUEST,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const publisher = new Redis(config.URL, {
|
|
418
|
+
...redisOptions,
|
|
419
|
+
retryStrategy: options.retryStrategy ?? publisherRetry.strategy,
|
|
420
|
+
});
|
|
421
|
+
const subscriber = new Redis(config.URL, {
|
|
422
|
+
...redisOptions,
|
|
423
|
+
retryStrategy: options.retryStrategy ?? subscriberRetry.strategy,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
await publisher.connect();
|
|
428
|
+
publisherRetry.markConnected();
|
|
429
|
+
await subscriber.connect();
|
|
430
|
+
subscriberRetry.markConnected();
|
|
431
|
+
} catch (error) {
|
|
432
|
+
publisher.disconnect();
|
|
433
|
+
subscriber.disconnect();
|
|
434
|
+
throw new Error(
|
|
435
|
+
`[redisEventBusProvider] Failed to connect to Redis at ${redactConnectionUrl(
|
|
436
|
+
config.URL,
|
|
437
|
+
)}: ${errorMessage(error)}`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const eventBus = createRedisEventBus({
|
|
442
|
+
publisher,
|
|
443
|
+
subscriber,
|
|
444
|
+
channelPrefix: configuredChannelPrefix,
|
|
445
|
+
instrumentation: ports,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
ports: {
|
|
450
|
+
eventBus,
|
|
451
|
+
redisEventBus: {
|
|
452
|
+
publisher,
|
|
453
|
+
subscriber,
|
|
454
|
+
channelPrefix: configuredChannelPrefix,
|
|
455
|
+
checkHealth: () =>
|
|
456
|
+
checkRedisEventBusHealth(
|
|
457
|
+
publisher,
|
|
458
|
+
subscriber,
|
|
459
|
+
configuredChannelPrefix,
|
|
460
|
+
),
|
|
461
|
+
},
|
|
462
|
+
} satisfies RedisEventBusProviderPorts,
|
|
463
|
+
async stop() {
|
|
464
|
+
try {
|
|
465
|
+
await subscriber.quit();
|
|
466
|
+
} catch {
|
|
467
|
+
subscriber.disconnect();
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
await publisher.quit();
|
|
471
|
+
} catch {
|
|
472
|
+
publisher.disconnect();
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export const redisEventBusProvider = createRedisEventBusProvider();
|