@codemation/eventbus-redis 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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # `@codemation/eventbus-redis`
2
+
3
+ **Redis-backed run event bus** implementation for Codemation, bridging engine events to infrastructure using **ioredis**. It implements host/runtime expectations for publishing and subscribing to run lifecycle traffic.
4
+
5
+ ## At a glance
6
+
7
+ **End-to-end (why Redis):** run lifecycle signals must cross processes (API process vs worker, or multiple hosts). The engine publishes **`RunEvent`**; this package serializes them to Redis so every subscriber sees the same stream.
8
+
9
+ ```
10
+ Run starts
11
+ ──────────
12
+ · User clicks Run / HTTP command ─┐
13
+ · Webhook / trigger / schedule ├──► @codemation/host ──► Engine ──► RunEvent
14
+ │ (StartWorkflowRun, etc.) │ (publish)
15
+ │ │
16
+ │ ┌────────────────────────────┘
17
+ │ ▼
18
+ │ ┌──────────────────┐
19
+ │ │ eventbus-redis │ Redis PUBLISH to:
20
+ └──►│ RedisRunEventBus │ · codemation.run-events.all
21
+ │ (this package) │ · codemation.run-events.workflow.<id>
22
+ └────────┬─────────┘
23
+
24
+ any process with a subscriber │ SUBSCRIBE (same channels)
25
+
26
+ ┌──────────────────┐
27
+ │ @codemation/host │
28
+ │ WorkflowRunEvent │
29
+ │ WebsocketRelay │──► workflow WebSocket room
30
+ └────────┬─────────┘ (clients subscribed by
31
+ │ workflowId)
32
+
33
+ Browser / live UI updates
34
+ (nodeQueued, nodeStarted, …)
35
+ ```
36
+
37
+ **Compact view**
38
+
39
+ ```
40
+ Engine (main or worker) Redis pub/sub @codemation/host
41
+ ┌────────────────────┐ ┌──────────────┐ ┌────────────────────────┐
42
+ │ RunEvent publishers│────►│ RedisRunEvent│◄────────│ WorkflowRunEvent │
43
+ │ (engine + run store)│ pub │ Bus channels │ sub │ WebsocketRelay → room │
44
+ └────────────────────┘ └──────────────┘ └────────────────────────┘
45
+ ```
46
+
47
+ Workers and the HTTP server can live on different machines; all subscribe to the same Redis channels. **In-memory** `RunEventBus` only works inside one process.
48
+
49
+ ## Typical events on this bus
50
+
51
+ These mirror `@codemation/core` **`RunEvent`** kinds (serialized over Redis):
52
+
53
+ | Kind | Meaning (high level) |
54
+ | --------------- | --------------------------------------- |
55
+ | `runCreated` | A workflow run was started (or resumed) |
56
+ | `runSaved` | Persisted run state was written |
57
+ | `nodeQueued` | A node is scheduled / entered the queue |
58
+ | `nodeStarted` | Execution of a node began |
59
+ | `nodeCompleted` | A node finished successfully |
60
+ | `nodeFailed` | A node failed with an error snapshot |
61
+
62
+ Use **in-memory** bus for single-process dev; switch to **Redis** when multiple processes or machines must observe the same run lifecycle.
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ pnpm add @codemation/eventbus-redis@^0.0.0
68
+ # or
69
+ npm install @codemation/eventbus-redis@^0.0.0
70
+ ```
71
+
72
+ Requires a reachable Redis deployment compatible with your host configuration.
73
+
74
+ ## When to use
75
+
76
+ Wire this package when you run Codemation in multi-process or scaled setups and want run events (progress, completion, errors) to flow through Redis instead of an in-memory bus.
77
+
78
+ ## Usage
79
+
80
+ ```ts
81
+ import { RedisRunEventBus } from "@codemation/eventbus-redis";
82
+ ```
83
+
84
+ Register the implementation through your host DI/bootstrap where `EventBus`-style services are bound (exact wiring depends on your `CodemationApplication` configuration).
@@ -0,0 +1,25 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
+
24
+ //#endregion
25
+ export { };
@@ -0,0 +1,308 @@
1
+ import IORedis from "ioredis";
2
+
3
+ //#region ../core/src/contracts/runTypes.d.ts
4
+ interface RunExecutionOptions {
5
+ /** Run-intent override: force the inline scheduler and bypass node-level offload decisions. */
6
+ localOnly?: boolean;
7
+ /** Marks runs started from webhook handling so orchestration can apply webhook-specific continuation rules. */
8
+ webhook?: boolean;
9
+ mode?: "manual" | "debug";
10
+ sourceWorkflowId?: WorkflowId;
11
+ sourceRunId?: RunId;
12
+ derivedFromRunId?: RunId;
13
+ isMutable?: boolean;
14
+ /** Set by the engine for this run: 0 = root, 1 = first child subworkflow, … */
15
+ subworkflowDepth?: number;
16
+ /** Effective cap after engine policy merge (successful node completions per run). */
17
+ maxNodeActivations?: number;
18
+ /** Effective cap after engine policy merge (subworkflow nesting). */
19
+ maxSubworkflowDepth?: number;
20
+ }
21
+ /** Engine-owned counters persisted with the run (worker-safe). */
22
+ interface EngineRunCounters {
23
+ completedNodeActivations: number;
24
+ }
25
+ type RunStopCondition = Readonly<{
26
+ kind: "workflowCompleted";
27
+ }> | Readonly<{
28
+ kind: "nodeCompleted";
29
+ nodeId: NodeId;
30
+ }>;
31
+ interface PersistedRunControlState {
32
+ stopCondition?: RunStopCondition;
33
+ }
34
+ interface PersistedWorkflowSnapshotNode {
35
+ id: NodeId;
36
+ kind: NodeKind;
37
+ name?: string;
38
+ nodeTokenId: PersistedTokenId;
39
+ configTokenId: PersistedTokenId;
40
+ tokenName?: string;
41
+ configTokenName?: string;
42
+ config: unknown;
43
+ }
44
+ interface PersistedWorkflowSnapshot {
45
+ id: WorkflowId;
46
+ name: string;
47
+ nodes: ReadonlyArray<PersistedWorkflowSnapshotNode>;
48
+ edges: ReadonlyArray<Edge>;
49
+ /** When the snapshot was built from a live workflow definition that configured a workflow error handler. */
50
+ workflowErrorHandlerConfigured?: boolean;
51
+ /** Connection metadata for child nodes not in the execution graph (e.g. AI agent attachments). */
52
+ connections?: ReadonlyArray<WorkflowNodeConnection>;
53
+ }
54
+ type PinnedNodeOutputsByPort = Readonly<Record<OutputPortKey, Items>>;
55
+ interface PersistedMutableNodeState {
56
+ pinnedOutputsByPort?: PinnedNodeOutputsByPort;
57
+ lastDebugInput?: Items;
58
+ }
59
+ interface PersistedMutableRunState {
60
+ nodesById: Readonly<Record<NodeId, PersistedMutableNodeState>>;
61
+ }
62
+ type NodeInputsByPort = Readonly<Record<InputPortKey, Items>>;
63
+ interface RunQueueEntry {
64
+ nodeId: NodeId;
65
+ input: Items;
66
+ toInput?: InputPortKey;
67
+ batchId?: string;
68
+ from?: Readonly<{
69
+ nodeId: NodeId;
70
+ output: OutputPortKey;
71
+ }>;
72
+ collect?: Readonly<{
73
+ expectedInputs: ReadonlyArray<InputPortKey>;
74
+ received: Readonly<Record<InputPortKey, Items>>;
75
+ }>;
76
+ }
77
+ type NodeExecutionStatus = "pending" | "queued" | "running" | "completed" | "failed" | "skipped";
78
+ interface NodeExecutionError {
79
+ message: string;
80
+ name?: string;
81
+ stack?: string;
82
+ }
83
+ interface NodeExecutionSnapshot {
84
+ runId: RunId;
85
+ workflowId: WorkflowId;
86
+ nodeId: NodeId;
87
+ activationId?: NodeActivationId;
88
+ parent?: ParentExecutionRef;
89
+ status: NodeExecutionStatus;
90
+ usedPinnedOutput?: boolean;
91
+ queuedAt?: string;
92
+ startedAt?: string;
93
+ finishedAt?: string;
94
+ updatedAt: string;
95
+ inputsByPort?: NodeInputsByPort;
96
+ outputs?: NodeOutputs;
97
+ error?: NodeExecutionError;
98
+ }
99
+ /** Stable id for a single connection invocation row in {@link ConnectionInvocationRecord}. */
100
+ type ConnectionInvocationId = string;
101
+ /**
102
+ * One logical LLM or tool call under an owning workflow node (e.g. AI agent).
103
+ * The owning node defines what {@link managedInput} and {@link managedOutput} contain.
104
+ */
105
+ interface ConnectionInvocationRecord {
106
+ readonly invocationId: ConnectionInvocationId;
107
+ readonly runId: RunId;
108
+ readonly workflowId: WorkflowId;
109
+ readonly connectionNodeId: NodeId;
110
+ readonly parentAgentNodeId: NodeId;
111
+ readonly parentAgentActivationId: NodeActivationId;
112
+ readonly status: NodeExecutionStatus;
113
+ readonly managedInput?: JsonValue;
114
+ readonly managedOutput?: JsonValue;
115
+ readonly error?: NodeExecutionError;
116
+ readonly queuedAt?: string;
117
+ readonly startedAt?: string;
118
+ readonly finishedAt?: string;
119
+ readonly updatedAt: string;
120
+ }
121
+ type RunStatus = "running" | "pending" | "completed" | "failed";
122
+ interface PendingNodeExecution {
123
+ runId: RunId;
124
+ activationId: NodeActivationId;
125
+ workflowId: WorkflowId;
126
+ nodeId: NodeId;
127
+ itemsIn: number;
128
+ inputsByPort: NodeInputsByPort;
129
+ receiptId: string;
130
+ queue?: string;
131
+ batchId?: string;
132
+ enqueuedAt: string;
133
+ }
134
+ interface PersistedRunState {
135
+ runId: RunId;
136
+ workflowId: WorkflowId;
137
+ startedAt: string;
138
+ parent?: ParentExecutionRef;
139
+ executionOptions?: RunExecutionOptions;
140
+ control?: PersistedRunControlState;
141
+ workflowSnapshot?: PersistedWorkflowSnapshot;
142
+ mutableState?: PersistedMutableRunState;
143
+ /** Frozen at createRun from workflow + runtime defaults for prune/storage decisions. */
144
+ policySnapshot?: PersistedRunPolicySnapshot;
145
+ /** Successful node completions so far (for activation budget). */
146
+ engineCounters?: EngineRunCounters;
147
+ status: RunStatus;
148
+ pending?: PendingNodeExecution;
149
+ queue: RunQueueEntry[];
150
+ outputsByNode: Record<NodeId, NodeOutputs>;
151
+ nodeSnapshotsByNodeId: Record<NodeId, NodeExecutionSnapshot>;
152
+ /** Append-only history of connection invocations (LLM/tool) nested under owning nodes. */
153
+ connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>;
154
+ }
155
+ //#endregion
156
+ //#region ../core/src/contracts/workflowTypes.d.ts
157
+ type WorkflowId = string;
158
+ type NodeId = string;
159
+ type OutputPortKey = string;
160
+ type InputPortKey = string;
161
+ type PersistedTokenId = string;
162
+ type NodeKind = "trigger" | "node";
163
+ type JsonPrimitive = string | number | boolean | null;
164
+ interface JsonObject {
165
+ readonly [key: string]: JsonValue;
166
+ }
167
+ type JsonValue = JsonPrimitive | JsonObject | JsonArray;
168
+ type JsonArray = ReadonlyArray<JsonValue>;
169
+ interface Edge {
170
+ from: {
171
+ nodeId: NodeId;
172
+ output: OutputPortKey;
173
+ };
174
+ to: {
175
+ nodeId: NodeId;
176
+ input: InputPortKey;
177
+ };
178
+ }
179
+ type NodeConnectionName = string;
180
+ /**
181
+ * Named connection from an executable parent node to child nodes that exist in {@link WorkflowDefinition.nodes}
182
+ * but are not traversed by the main execution graph.
183
+ */
184
+ interface WorkflowNodeConnection {
185
+ readonly parentNodeId: NodeId;
186
+ readonly connectionName: NodeConnectionName;
187
+ readonly childNodeIds: ReadonlyArray<NodeId>;
188
+ }
189
+ type PairedItemRef = Readonly<{
190
+ nodeId: NodeId;
191
+ output: OutputPortKey;
192
+ itemIndex: number;
193
+ }>;
194
+ type BinaryPreviewKind = "image" | "audio" | "video" | "download";
195
+ type BinaryAttachment = Readonly<{
196
+ id: string;
197
+ storageKey: string;
198
+ mimeType: string;
199
+ size: number;
200
+ storageDriver: string;
201
+ previewKind: BinaryPreviewKind;
202
+ createdAt: string;
203
+ runId: RunId;
204
+ workflowId: WorkflowId;
205
+ nodeId: NodeId;
206
+ activationId: NodeActivationId;
207
+ filename?: string;
208
+ sha256?: string;
209
+ }>;
210
+ type ItemBinary = Readonly<Record<string, BinaryAttachment>>;
211
+ type Item<TJson = unknown> = Readonly<{
212
+ json: TJson;
213
+ binary?: ItemBinary;
214
+ meta?: Readonly<Record<string, unknown>>;
215
+ paired?: ReadonlyArray<PairedItemRef>;
216
+ }>;
217
+ type Items<TJson = unknown> = ReadonlyArray<Item<TJson>>;
218
+ type NodeOutputs = Partial<Record<OutputPortKey, Items>>;
219
+ type RunId = string;
220
+ type NodeActivationId = string;
221
+ interface ParentExecutionRef {
222
+ runId: RunId;
223
+ workflowId: WorkflowId;
224
+ nodeId: NodeId;
225
+ /** Subworkflow depth of the **spawning** run (0 = root). Passed when starting a child run. */
226
+ subworkflowDepth?: number;
227
+ /** Effective max node activations from the parent run (propagated to child policy merge). */
228
+ engineMaxNodeActivations?: number;
229
+ /** Effective max subworkflow depth from the parent run (propagated to child policy merge). */
230
+ engineMaxSubworkflowDepth?: number;
231
+ }
232
+ /** Whether to persist run execution data after the workflow finishes. */
233
+ type WorkflowStoragePolicyMode = "ALL" | "SUCCESS" | "ERROR" | "NEVER";
234
+ interface PersistedRunPolicySnapshot {
235
+ readonly retentionSeconds?: number;
236
+ readonly binaryRetentionSeconds?: number;
237
+ readonly storagePolicy: WorkflowStoragePolicyMode;
238
+ }
239
+ //#endregion
240
+ //#region ../core/src/events/runEvents.d.ts
241
+ type RunEvent = Readonly<{
242
+ kind: "runCreated";
243
+ runId: RunId;
244
+ workflowId: WorkflowId;
245
+ parent?: ParentExecutionRef;
246
+ at: string;
247
+ }> | Readonly<{
248
+ kind: "runSaved";
249
+ runId: RunId;
250
+ workflowId: WorkflowId;
251
+ parent?: ParentExecutionRef;
252
+ at: string;
253
+ state: PersistedRunState;
254
+ }> | Readonly<{
255
+ kind: "nodeQueued";
256
+ runId: RunId;
257
+ workflowId: WorkflowId;
258
+ parent?: ParentExecutionRef;
259
+ at: string;
260
+ snapshot: NodeExecutionSnapshot;
261
+ }> | Readonly<{
262
+ kind: "nodeStarted";
263
+ runId: RunId;
264
+ workflowId: WorkflowId;
265
+ parent?: ParentExecutionRef;
266
+ at: string;
267
+ snapshot: NodeExecutionSnapshot;
268
+ }> | Readonly<{
269
+ kind: "nodeCompleted";
270
+ runId: RunId;
271
+ workflowId: WorkflowId;
272
+ parent?: ParentExecutionRef;
273
+ at: string;
274
+ snapshot: NodeExecutionSnapshot;
275
+ }> | Readonly<{
276
+ kind: "nodeFailed";
277
+ runId: RunId;
278
+ workflowId: WorkflowId;
279
+ parent?: ParentExecutionRef;
280
+ at: string;
281
+ snapshot: NodeExecutionSnapshot;
282
+ }>;
283
+ interface RunEventSubscription {
284
+ close(): Promise<void>;
285
+ }
286
+ interface RunEventBus {
287
+ publish(event: RunEvent): Promise<void>;
288
+ subscribe(onEvent: (event: RunEvent) => void): Promise<RunEventSubscription>;
289
+ subscribeToWorkflow(workflowId: WorkflowId, onEvent: (event: RunEvent) => void): Promise<RunEventSubscription>;
290
+ }
291
+ //#endregion
292
+ //#region src/RedisRunEventBusRegistry.d.ts
293
+ declare class RedisRunEventBus implements RunEventBus {
294
+ private readonly redisUrl;
295
+ private readonly globalChannel;
296
+ private publisher;
297
+ constructor(redisUrl: string, channelPrefix?: string);
298
+ private readonly channelPrefix;
299
+ publish(event: RunEvent): Promise<void>;
300
+ subscribe(onEvent: (event: RunEvent) => void): Promise<RunEventSubscription>;
301
+ subscribeToWorkflow(workflowId: WorkflowId, onEvent: (event: RunEvent) => void): Promise<RunEventSubscription>;
302
+ private ensurePublisher;
303
+ private createSubscription;
304
+ private getWorkflowChannel;
305
+ private parseEvent;
306
+ }
307
+ //#endregion
308
+ export { RedisRunEventBus };
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ import IORedis from "ioredis";
2
+
3
+ //#region src/RedisRunEventSubscription.ts
4
+ var RedisRunEventSubscription = class {
5
+ constructor(subscriber, channel, handler) {
6
+ this.subscriber = subscriber;
7
+ this.channel = channel;
8
+ this.handler = handler;
9
+ }
10
+ async close() {
11
+ this.subscriber.off("message", this.handler);
12
+ await this.subscriber.unsubscribe(this.channel);
13
+ this.subscriber.disconnect();
14
+ }
15
+ };
16
+
17
+ //#endregion
18
+ //#region src/RedisRunEventBusRegistry.ts
19
+ var RedisRunEventBus = class {
20
+ globalChannel;
21
+ publisher;
22
+ constructor(redisUrl, channelPrefix = "codemation") {
23
+ this.redisUrl = redisUrl;
24
+ this.globalChannel = `${channelPrefix}.run-events.all`;
25
+ this.channelPrefix = `${channelPrefix}.run-events.workflow`;
26
+ }
27
+ channelPrefix;
28
+ async publish(event) {
29
+ const pub = this.ensurePublisher();
30
+ const serialized = JSON.stringify(event);
31
+ await pub.publish(this.globalChannel, serialized);
32
+ await pub.publish(this.getWorkflowChannel(event.workflowId), serialized);
33
+ }
34
+ async subscribe(onEvent) {
35
+ return await this.createSubscription(this.globalChannel, onEvent);
36
+ }
37
+ async subscribeToWorkflow(workflowId, onEvent) {
38
+ return await this.createSubscription(this.getWorkflowChannel(workflowId), onEvent);
39
+ }
40
+ ensurePublisher() {
41
+ if (this.publisher) return this.publisher;
42
+ this.publisher = new IORedis(this.redisUrl);
43
+ return this.publisher;
44
+ }
45
+ async createSubscription(channel, onEvent) {
46
+ const sub = new IORedis(this.redisUrl);
47
+ const onMessage = (receivedChannel, message) => {
48
+ if (receivedChannel !== channel) return;
49
+ onEvent(this.parseEvent(message));
50
+ };
51
+ sub.on("message", onMessage);
52
+ await sub.subscribe(channel);
53
+ return new RedisRunEventSubscription(sub, channel, onMessage);
54
+ }
55
+ getWorkflowChannel(workflowId) {
56
+ return `${this.channelPrefix}.${workflowId}`;
57
+ }
58
+ parseEvent(raw) {
59
+ return JSON.parse(raw);
60
+ }
61
+ };
62
+
63
+ //#endregion
64
+ export { RedisRunEventBus };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@codemation/eventbus-redis",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "author": "Made Relevant B.V.",
8
+ "homepage": "https://www.maderelevant.com",
9
+ "type": "module",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "development": {
17
+ "import": "./src/index.ts",
18
+ "require": "./dist/index.cjs"
19
+ },
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "ioredis": "^5.7.0",
26
+ "@codemation/core": "0.0.1"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.3.5",
30
+ "eslint": "^10.0.3",
31
+ "tsdown": "^0.15.5",
32
+ "tsx": "^4.21.0",
33
+ "typescript": "^5.9.3"
34
+ },
35
+ "scripts": {
36
+ "dev": "tsdown --watch",
37
+ "build": "tsdown",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "lint": "eslint .",
40
+ "test": "echo \"(test placeholder)\""
41
+ }
42
+ }
@@ -0,0 +1,63 @@
1
+ import type { RunEvent, RunEventBus, RunEventSubscription, WorkflowId } from "@codemation/core";
2
+
3
+ import IORedis from "ioredis";
4
+
5
+ import { RedisRunEventSubscription } from "./RedisRunEventSubscription";
6
+
7
+ export class RedisRunEventBus implements RunEventBus {
8
+ private readonly globalChannel: string;
9
+ private publisher: IORedis | undefined;
10
+
11
+ constructor(
12
+ private readonly redisUrl: string,
13
+ channelPrefix = "codemation",
14
+ ) {
15
+ this.globalChannel = `${channelPrefix}.run-events.all`;
16
+ this.channelPrefix = `${channelPrefix}.run-events.workflow`;
17
+ }
18
+
19
+ private readonly channelPrefix: string;
20
+
21
+ async publish(event: RunEvent): Promise<void> {
22
+ const pub = this.ensurePublisher();
23
+ const serialized = JSON.stringify(event);
24
+ await pub.publish(this.globalChannel, serialized);
25
+ await pub.publish(this.getWorkflowChannel(event.workflowId), serialized);
26
+ }
27
+
28
+ async subscribe(onEvent: (event: RunEvent) => void): Promise<RunEventSubscription> {
29
+ return await this.createSubscription(this.globalChannel, onEvent);
30
+ }
31
+
32
+ async subscribeToWorkflow(workflowId: WorkflowId, onEvent: (event: RunEvent) => void): Promise<RunEventSubscription> {
33
+ return await this.createSubscription(this.getWorkflowChannel(workflowId), onEvent);
34
+ }
35
+
36
+ private ensurePublisher(): IORedis {
37
+ if (this.publisher) return this.publisher;
38
+ this.publisher = new IORedis(this.redisUrl);
39
+ return this.publisher;
40
+ }
41
+
42
+ private async createSubscription(channel: string, onEvent: (event: RunEvent) => void): Promise<RunEventSubscription> {
43
+ const sub = new IORedis(this.redisUrl);
44
+ const onMessage = (receivedChannel: string, message: string) => {
45
+ if (receivedChannel !== channel) return;
46
+ onEvent(this.parseEvent(message));
47
+ };
48
+
49
+ sub.on("message", onMessage);
50
+ await sub.subscribe(channel);
51
+ return new RedisRunEventSubscription(sub, channel, onMessage);
52
+ }
53
+
54
+ private getWorkflowChannel(workflowId: WorkflowId): string {
55
+ return `${this.channelPrefix}.${workflowId}`;
56
+ }
57
+
58
+ private parseEvent(raw: string): RunEvent {
59
+ return JSON.parse(raw) as RunEvent;
60
+ }
61
+ }
62
+
63
+ export { RedisRunEventSubscription } from "./RedisRunEventSubscription";
@@ -0,0 +1,17 @@
1
+ import type { RunEventSubscription } from "@codemation/core";
2
+
3
+ import IORedis from "ioredis";
4
+
5
+ export class RedisRunEventSubscription implements RunEventSubscription {
6
+ constructor(
7
+ private readonly subscriber: IORedis,
8
+ private readonly channel: string,
9
+ private readonly handler: (channel: string, message: string) => void,
10
+ ) {}
11
+
12
+ async close(): Promise<void> {
13
+ this.subscriber.off("message", this.handler);
14
+ await this.subscriber.unsubscribe(this.channel);
15
+ this.subscriber.disconnect();
16
+ }
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { RedisRunEventBus } from "./RedisRunEventBusRegistry";
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "lib": ["ES2022"],
5
+ "types": ["node"],
6
+ "noEmit": true
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }