@ouija-dev/bus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ import { type ConnectionOptions } from 'bullmq';
2
+ import type { OuijaEventMap, OuijaTopic } from '@ouija-dev/types';
3
+ import type { EventBus, EventHandler, PatternEventHandler, PublishOptions, Unsubscribe } from './event-bus.js';
4
+ /**
5
+ * BullMQ-backed EventBus implementation.
6
+ *
7
+ * Fan-out strategy (v1 — simple and correct):
8
+ * Each registered subscriber gets its own BullMQ queue named
9
+ * `ouija.event-bus.<subscriberKey>`. When an event is published, one job is
10
+ * enqueued per subscriber whose topic/pattern matches. The subscriber's Worker
11
+ * processes that queue and calls the handler.
12
+ *
13
+ * This is O(N subscribers) on publish, which is acceptable for v1 where
14
+ * subscriber counts are single digits. If N grows, swap to Redis Streams
15
+ * consumer groups without changing the EventBus interface.
16
+ *
17
+ * Assumptions:
18
+ * - Redis is configured with `maxmemory-policy noeviction`. Jobs must never
19
+ * be silently dropped due to memory pressure.
20
+ * - Connection is managed externally and passed in via `connection` option.
21
+ */
22
+ export declare class BullMQEventBus implements EventBus {
23
+ private readonly connection;
24
+ private readonly subscribers;
25
+ private readonly deliveryQueues;
26
+ private readonly dispatchQueue;
27
+ private readonly dispatchWorker;
28
+ private closed;
29
+ constructor(connection: ConnectionOptions);
30
+ publish<TTopic extends OuijaTopic>(topic: TTopic, payload: OuijaEventMap[TTopic], options?: PublishOptions): Promise<string>;
31
+ subscribe<TTopic extends OuijaTopic>(topic: TTopic, handler: EventHandler<TTopic>): Promise<Unsubscribe>;
32
+ subscribePattern(pattern: string, handler: PatternEventHandler): Promise<Unsubscribe>;
33
+ /**
34
+ * Replay is not natively supported by BullMQ (jobs are ephemeral).
35
+ * A production implementation would query a persistent event store
36
+ * (Postgres `pipeline_events` table) and re-deliver events.
37
+ *
38
+ * This stub iterates completed jobs in the dispatch queue as a best-effort
39
+ * fallback. Callers should not rely on this for full catch-up — use the
40
+ * Postgres event log (Task 5) once available.
41
+ */
42
+ replay(topic: OuijaTopic, from: string, to: string, handler: PatternEventHandler): Promise<void>;
43
+ close(): Promise<void>;
44
+ private addSubscriber;
45
+ /**
46
+ * Fan out a published event to all matching subscribers.
47
+ * Called by the dispatch worker — runs inside BullMQ.
48
+ */
49
+ private fanOut;
50
+ }
51
+ //# sourceMappingURL=bullmq-event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-event-bus.d.ts","sourceRoot":"","sources":["../src/bullmq-event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAE/D,OAAO,KAAK,EAAc,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,WAAW,EACZ,MAAM,gBAAgB,CAAC;AA0DxB;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,cAAe,YAAW,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAElE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4B;IAE3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,MAAM,CAAS;gBAEX,UAAU,EAAE,iBAAiB;IAoCnC,OAAO,CAAC,MAAM,SAAS,UAAU,EACrC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC;IAuBZ,SAAS,CAAC,MAAM,SAAS,UAAU,EACvC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,GAC5B,OAAO,CAAC,WAAW,CAAC;IAajB,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,WAAW,CAAC;IAQvB;;;;;;;;OAQG;IACG,MAAM,CACV,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC;IAgCV,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAwBd,aAAa;IA0D3B;;;OAGG;YACW,MAAM;CAwBrB"}
@@ -0,0 +1,246 @@
1
+ import { Queue, Worker } from 'bullmq';
2
+ import { randomUUID } from 'node:crypto';
3
+ // ---------------------------------------------------------------------------
4
+ // Glob pattern matching
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Convert a glob-style pattern into a RegExp.
8
+ *
9
+ * Rules:
10
+ * - `*` matches any sequence of characters that does not contain `.`
11
+ * - `**` matches any sequence of characters including `.`
12
+ * - `.` is treated as a literal dot
13
+ *
14
+ * Examples:
15
+ * - `kanban.card.*` matches `kanban.card.moved`, `kanban.card.created`
16
+ * - `agent.**` matches `agent.work.progress`, `agent.work.pr_ready`
17
+ * - `git.*` matches `git.pr` but NOT `git.pr.opened`
18
+ */
19
+ function globToRegex(pattern) {
20
+ // Escape all regex metacharacters except * which we handle specially
21
+ const escaped = pattern
22
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars (. included)
23
+ .replace(/\\\.\\\./g, '\\.') // shouldn't occur but safety net
24
+ // Replace ** first (two stars), then single *
25
+ .replace(/\*\*/g, '\x00GLOBSTAR\x00')
26
+ .replace(/\*/g, '[^.]+')
27
+ .replace(/\x00GLOBSTAR\x00/g, '.+');
28
+ return new RegExp(`^${escaped}$`);
29
+ }
30
+ function topicMatchesPattern(topic, pattern) {
31
+ // Exact match short-circuit
32
+ if (topic === pattern)
33
+ return true;
34
+ try {
35
+ return globToRegex(pattern).test(topic);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // BullMQEventBus
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * BullMQ-backed EventBus implementation.
46
+ *
47
+ * Fan-out strategy (v1 — simple and correct):
48
+ * Each registered subscriber gets its own BullMQ queue named
49
+ * `ouija.event-bus.<subscriberKey>`. When an event is published, one job is
50
+ * enqueued per subscriber whose topic/pattern matches. The subscriber's Worker
51
+ * processes that queue and calls the handler.
52
+ *
53
+ * This is O(N subscribers) on publish, which is acceptable for v1 where
54
+ * subscriber counts are single digits. If N grows, swap to Redis Streams
55
+ * consumer groups without changing the EventBus interface.
56
+ *
57
+ * Assumptions:
58
+ * - Redis is configured with `maxmemory-policy noeviction`. Jobs must never
59
+ * be silently dropped due to memory pressure.
60
+ * - Connection is managed externally and passed in via `connection` option.
61
+ */
62
+ export class BullMQEventBus {
63
+ connection;
64
+ subscribers = new Map();
65
+ // One Queue per subscriber key for fan-out delivery
66
+ deliveryQueues = new Map();
67
+ // Single publish queue used as the fan-out coordinator
68
+ dispatchQueue;
69
+ dispatchWorker;
70
+ closed = false;
71
+ constructor(connection) {
72
+ this.connection = connection;
73
+ // The dispatch queue receives one job per publish() call.
74
+ // Its worker fans out to per-subscriber delivery queues.
75
+ this.dispatchQueue = new Queue('ouija.event-bus.dispatch', {
76
+ connection,
77
+ defaultJobOptions: {
78
+ removeOnComplete: { count: 1000 },
79
+ removeOnFail: { count: 500 },
80
+ attempts: 3,
81
+ backoff: { type: 'exponential', delay: 1000 },
82
+ },
83
+ });
84
+ this.dispatchWorker = new Worker('ouija.event-bus.dispatch', async (job) => {
85
+ const event = job.data;
86
+ await this.fanOut(event);
87
+ }, { connection, concurrency: 10 });
88
+ this.dispatchWorker.on('failed', (job, err) => {
89
+ console.error(`[BullMQEventBus] dispatch job ${job?.id ?? 'unknown'} failed:`, err);
90
+ });
91
+ }
92
+ // -------------------------------------------------------------------------
93
+ // publish
94
+ // -------------------------------------------------------------------------
95
+ async publish(topic, payload, options = {}) {
96
+ if (this.closed)
97
+ throw new Error('BullMQEventBus is closed');
98
+ const event = {
99
+ id: randomUUID(),
100
+ topic,
101
+ payload,
102
+ timestamp: new Date().toISOString(),
103
+ sourcePlugin: options.sourcePlugin ?? 'unknown',
104
+ correlationId: options.correlationId ?? randomUUID(),
105
+ };
106
+ await this.dispatchQueue.add(`event:${topic}`, event, {
107
+ jobId: `event:${event.id}`,
108
+ });
109
+ return event.id;
110
+ }
111
+ // -------------------------------------------------------------------------
112
+ // subscribe (exact topic, fully typed)
113
+ // -------------------------------------------------------------------------
114
+ async subscribe(topic, handler) {
115
+ // Wrap typed handler as PatternEventHandler so we can store it uniformly
116
+ const patternHandler = async (event) => {
117
+ await handler(event);
118
+ };
119
+ return this.addSubscriber(topic, false, patternHandler);
120
+ }
121
+ // -------------------------------------------------------------------------
122
+ // subscribePattern (glob, intentionally type-unsafe)
123
+ // -------------------------------------------------------------------------
124
+ async subscribePattern(pattern, handler) {
125
+ return this.addSubscriber(pattern, true, handler);
126
+ }
127
+ // -------------------------------------------------------------------------
128
+ // replay
129
+ // -------------------------------------------------------------------------
130
+ /**
131
+ * Replay is not natively supported by BullMQ (jobs are ephemeral).
132
+ * A production implementation would query a persistent event store
133
+ * (Postgres `pipeline_events` table) and re-deliver events.
134
+ *
135
+ * This stub iterates completed jobs in the dispatch queue as a best-effort
136
+ * fallback. Callers should not rely on this for full catch-up — use the
137
+ * Postgres event log (Task 5) once available.
138
+ */
139
+ async replay(topic, from, to, handler) {
140
+ if (this.closed)
141
+ throw new Error('BullMQEventBus is closed');
142
+ const fromMs = new Date(from).getTime();
143
+ const toMs = new Date(to).getTime();
144
+ // BullMQ stores completed jobs temporarily. Fetch and filter.
145
+ const completed = await this.dispatchQueue.getCompleted(0, 1000);
146
+ const inRange = completed.filter((job) => {
147
+ const event = job.data;
148
+ if (event.topic !== topic)
149
+ return false;
150
+ const ts = new Date(event.timestamp).getTime();
151
+ return ts >= fromMs && ts <= toMs;
152
+ });
153
+ // Sort ascending by timestamp
154
+ inRange.sort((a, b) => {
155
+ const aTs = new Date(a.data.timestamp).getTime();
156
+ const bTs = new Date(b.data.timestamp).getTime();
157
+ return aTs - bTs;
158
+ });
159
+ for (const job of inRange) {
160
+ await handler(job.data);
161
+ }
162
+ }
163
+ // -------------------------------------------------------------------------
164
+ // close
165
+ // -------------------------------------------------------------------------
166
+ async close() {
167
+ if (this.closed)
168
+ return;
169
+ this.closed = true;
170
+ await this.dispatchWorker.close();
171
+ await this.dispatchQueue.close();
172
+ const closePromises = [];
173
+ for (const entry of this.subscribers.values()) {
174
+ closePromises.push(entry.worker.close());
175
+ }
176
+ for (const queue of this.deliveryQueues.values()) {
177
+ closePromises.push(queue.close());
178
+ }
179
+ await Promise.all(closePromises);
180
+ this.subscribers.clear();
181
+ this.deliveryQueues.clear();
182
+ }
183
+ // -------------------------------------------------------------------------
184
+ // Private helpers
185
+ // -------------------------------------------------------------------------
186
+ async addSubscriber(topicOrPattern, isPattern, handler) {
187
+ if (this.closed)
188
+ throw new Error('BullMQEventBus is closed');
189
+ const key = `sub:${randomUUID()}`;
190
+ const deliveryQueueName = `ouija.event-bus.delivery.${key}`;
191
+ const deliveryQueue = new Queue(deliveryQueueName, {
192
+ connection: this.connection,
193
+ defaultJobOptions: {
194
+ removeOnComplete: { count: 500 },
195
+ removeOnFail: { count: 100 },
196
+ attempts: 3,
197
+ backoff: { type: 'exponential', delay: 500 },
198
+ },
199
+ });
200
+ const worker = new Worker(deliveryQueueName, async (job) => {
201
+ const event = job.data;
202
+ await handler(event);
203
+ }, { connection: this.connection, concurrency: 1 });
204
+ worker.on('failed', (job, err) => {
205
+ console.error(`[BullMQEventBus] delivery job ${job?.id ?? 'unknown'} for subscriber ${key} failed:`, err);
206
+ });
207
+ const entry = {
208
+ key,
209
+ topic: topicOrPattern,
210
+ isPattern,
211
+ handler,
212
+ worker,
213
+ };
214
+ this.subscribers.set(key, entry);
215
+ this.deliveryQueues.set(key, deliveryQueue);
216
+ const unsubscribe = async () => {
217
+ this.subscribers.delete(key);
218
+ const q = this.deliveryQueues.get(key);
219
+ this.deliveryQueues.delete(key);
220
+ await worker.close();
221
+ if (q)
222
+ await q.close();
223
+ };
224
+ return unsubscribe;
225
+ }
226
+ /**
227
+ * Fan out a published event to all matching subscribers.
228
+ * Called by the dispatch worker — runs inside BullMQ.
229
+ */
230
+ async fanOut(event) {
231
+ const enqueuePromises = [];
232
+ for (const [key, entry] of this.subscribers) {
233
+ const matches = entry.isPattern
234
+ ? topicMatchesPattern(event.topic, entry.topic)
235
+ : event.topic === entry.topic;
236
+ if (!matches)
237
+ continue;
238
+ const deliveryQueue = this.deliveryQueues.get(key);
239
+ if (!deliveryQueue)
240
+ continue;
241
+ enqueuePromises.push(deliveryQueue.add(`deliver:${event.id}:${key}`, event, { jobId: `deliver:${event.id}:${key}` }));
242
+ }
243
+ await Promise.all(enqueuePromises);
244
+ }
245
+ }
246
+ //# sourceMappingURL=bullmq-event-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-event-bus.js","sourceRoot":"","sources":["../src/bullmq-event-bus.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type ConnectionOptions } from 'bullmq';
2
+ import type { EnqueueOptions, JobHandler, JobQueue, QueueDataMap, QueueName } from './job-queue.js';
3
+ /**
4
+ * BullMQ-backed JobQueue implementation.
5
+ *
6
+ * One BullMQ Queue per queue name. Workers are created lazily on the first
7
+ * `process()` call for a given queue name.
8
+ *
9
+ * Assumptions:
10
+ * - Redis is configured with `maxmemory-policy noeviction`.
11
+ * - All queue names are treated as static. Dynamic queue creation at runtime
12
+ * is not supported in v1.
13
+ */
14
+ export declare class BullMQJobQueue implements JobQueue {
15
+ private readonly connection;
16
+ private readonly queues;
17
+ private readonly workers;
18
+ private closed;
19
+ constructor(connection: ConnectionOptions);
20
+ enqueue<TQueue extends QueueName>(queue: TQueue, data: QueueDataMap[TQueue], options?: EnqueueOptions): Promise<string>;
21
+ process<TQueue extends QueueName>(queue: TQueue, handler: JobHandler<QueueDataMap[TQueue]>, concurrency?: number): Promise<void>;
22
+ cancelJob(queue: QueueName, jobId: string): Promise<void>;
23
+ close(): Promise<void>;
24
+ private getOrCreateQueue;
25
+ }
26
+ //# sourceMappingURL=bullmq-job-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-job-queue.d.ts","sourceRoot":"","sources":["../src/bullmq-job-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC/D,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,SAAS,EACV,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,YAAW,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,MAAM,CAAS;gBAEX,UAAU,EAAE,iBAAiB;IAQnC,OAAO,CAAC,MAAM,SAAS,SAAS,EACpC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAC1B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC;IAgCZ,OAAO,CAAC,MAAM,SAAS,SAAS,EACpC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,EACzC,WAAW,SAAI,GACd,OAAO,CAAC,IAAI,CAAC;IAuCV,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BzD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB5B,OAAO,CAAC,gBAAgB;CAezB"}
@@ -0,0 +1,134 @@
1
+ import { Queue, Worker } from 'bullmq';
2
+ /**
3
+ * BullMQ-backed JobQueue implementation.
4
+ *
5
+ * One BullMQ Queue per queue name. Workers are created lazily on the first
6
+ * `process()` call for a given queue name.
7
+ *
8
+ * Assumptions:
9
+ * - Redis is configured with `maxmemory-policy noeviction`.
10
+ * - All queue names are treated as static. Dynamic queue creation at runtime
11
+ * is not supported in v1.
12
+ */
13
+ export class BullMQJobQueue {
14
+ connection;
15
+ queues = new Map();
16
+ workers = new Map();
17
+ closed = false;
18
+ constructor(connection) {
19
+ this.connection = connection;
20
+ }
21
+ // -------------------------------------------------------------------------
22
+ // enqueue
23
+ // -------------------------------------------------------------------------
24
+ async enqueue(queue, data, options = {}) {
25
+ if (this.closed)
26
+ throw new Error('BullMQJobQueue is closed');
27
+ const q = this.getOrCreateQueue(queue);
28
+ // Build options conditionally to satisfy exactOptionalPropertyTypes:
29
+ // never assign `undefined` to an optional property.
30
+ const jobOptions = {
31
+ attempts: options.attempts ?? 3,
32
+ backoff: options.backoff
33
+ ? { type: options.backoff.type, delay: options.backoff.delay }
34
+ : { type: 'exponential', delay: 1000 },
35
+ priority: options.priority ?? 0,
36
+ removeOnComplete: { count: 1000 },
37
+ removeOnFail: { count: 500 },
38
+ };
39
+ if (options.jobId !== undefined)
40
+ jobOptions['jobId'] = options.jobId;
41
+ if (options.delayMs !== undefined)
42
+ jobOptions['delay'] = options.delayMs;
43
+ const job = await q.add(queue, data, jobOptions);
44
+ if (!job.id) {
45
+ throw new Error(`BullMQ did not assign an ID to job on queue "${queue}"`);
46
+ }
47
+ return job.id;
48
+ }
49
+ // -------------------------------------------------------------------------
50
+ // process
51
+ // -------------------------------------------------------------------------
52
+ async process(queue, handler, concurrency = 1) {
53
+ if (this.closed)
54
+ throw new Error('BullMQJobQueue is closed');
55
+ // Close any existing worker for this queue before replacing the handler
56
+ const existing = this.workers.get(queue);
57
+ if (existing) {
58
+ await existing.close();
59
+ this.workers.delete(queue);
60
+ }
61
+ // Ensure the queue exists
62
+ this.getOrCreateQueue(queue);
63
+ const worker = new Worker(queue, async (job) => {
64
+ const jobId = job.id ?? 'unknown';
65
+ await handler(job.data, jobId);
66
+ }, {
67
+ connection: this.connection,
68
+ concurrency,
69
+ });
70
+ worker.on('failed', (job, err) => {
71
+ console.error(`[BullMQJobQueue] job ${job?.id ?? 'unknown'} on queue "${queue}" failed:`, err);
72
+ });
73
+ this.workers.set(queue, worker);
74
+ }
75
+ // -------------------------------------------------------------------------
76
+ // cancelJob
77
+ // -------------------------------------------------------------------------
78
+ async cancelJob(queue, jobId) {
79
+ if (this.closed)
80
+ return;
81
+ const q = this.queues.get(queue);
82
+ if (!q)
83
+ return;
84
+ const job = await q.getJob(jobId);
85
+ if (!job)
86
+ return;
87
+ // Remove the job regardless of its state (waiting, delayed, etc.)
88
+ // For active jobs, this will fail silently — a running job cannot be
89
+ // force-removed; the worker must complete or fail it naturally.
90
+ try {
91
+ await job.remove();
92
+ }
93
+ catch {
94
+ // Job may already be processing — log and move on
95
+ console.warn(`[BullMQJobQueue] could not cancel job "${jobId}" on queue "${queue}" — it may be active`);
96
+ }
97
+ }
98
+ // -------------------------------------------------------------------------
99
+ // close
100
+ // -------------------------------------------------------------------------
101
+ async close() {
102
+ if (this.closed)
103
+ return;
104
+ this.closed = true;
105
+ const closePromises = [];
106
+ for (const worker of this.workers.values()) {
107
+ closePromises.push(worker.close());
108
+ }
109
+ for (const queue of this.queues.values()) {
110
+ closePromises.push(queue.close());
111
+ }
112
+ await Promise.all(closePromises);
113
+ this.workers.clear();
114
+ this.queues.clear();
115
+ }
116
+ // -------------------------------------------------------------------------
117
+ // Private helpers
118
+ // -------------------------------------------------------------------------
119
+ getOrCreateQueue(name) {
120
+ const existing = this.queues.get(name);
121
+ if (existing)
122
+ return existing;
123
+ const queue = new Queue(name, {
124
+ connection: this.connection,
125
+ defaultJobOptions: {
126
+ removeOnComplete: { count: 1000 },
127
+ removeOnFail: { count: 500 },
128
+ },
129
+ });
130
+ this.queues.set(name, queue);
131
+ return queue;
132
+ }
133
+ }
134
+ //# sourceMappingURL=bullmq-job-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-job-queue.js","sourceRoot":"","sources":["../src/bullmq-job-queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import type { OuijaEvent, OuijaEventMap, OuijaTopic } from '@ouija-dev/types';
2
+ /**
3
+ * Handler for a specific typed topic. Receives the full OuijaEvent envelope.
4
+ */
5
+ export type EventHandler<TTopic extends OuijaTopic> = (event: OuijaEvent<TTopic>) => Promise<void>;
6
+ /**
7
+ * Handler for pattern subscriptions. Type-unsafe by design — wildcards cannot
8
+ * be statically constrained to a single payload shape.
9
+ */
10
+ export type PatternEventHandler = (event: OuijaEvent) => Promise<void>;
11
+ /**
12
+ * Unsubscribe function returned by subscribe / subscribePattern.
13
+ * Calling it deregisters the handler and cleans up any BullMQ workers.
14
+ */
15
+ export type Unsubscribe = () => Promise<void>;
16
+ /**
17
+ * EventBus — pub/sub interface for Ouija domain events.
18
+ *
19
+ * Design rules (Decision 2 in spec §2.4):
20
+ * - Separate from JobQueue at the interface level even though both use BullMQ.
21
+ * - `subscribe` is typed: the handler receives the exact payload for TTopic.
22
+ * - `subscribePattern` is intentionally type-unsafe: wildcards span multiple
23
+ * topics and therefore multiple payload shapes. Callers must narrow.
24
+ * - `replay` allows catch-up processing from persistent event storage.
25
+ */
26
+ export interface EventBus {
27
+ /**
28
+ * Publish an event to all subscribers of `topic`.
29
+ * The bus generates the event envelope (id, timestamp, correlationId).
30
+ * Returns the generated event id.
31
+ */
32
+ publish<TTopic extends OuijaTopic>(topic: TTopic, payload: OuijaEventMap[TTopic], options?: PublishOptions): Promise<string>;
33
+ /**
34
+ * Subscribe to an exact topic. Returns an unsubscribe function.
35
+ * The handler is called once per published event.
36
+ */
37
+ subscribe<TTopic extends OuijaTopic>(topic: TTopic, handler: EventHandler<TTopic>): Promise<Unsubscribe>;
38
+ /**
39
+ * Subscribe to events matching a glob-style pattern, e.g. `kanban.card.*`
40
+ * or `agent.**`. Intentionally type-unsafe — callers must narrow the payload
41
+ * themselves. Returns an unsubscribe function.
42
+ */
43
+ subscribePattern(pattern: string, handler: PatternEventHandler): Promise<Unsubscribe>;
44
+ /**
45
+ * Replay stored events for a topic between `from` and `to` (ISO strings).
46
+ * Events are delivered to `handler` in chronological order.
47
+ * Used for catch-up processing and debugging.
48
+ */
49
+ replay(topic: OuijaTopic, from: string, to: string, handler: PatternEventHandler): Promise<void>;
50
+ /**
51
+ * Gracefully shut down the bus, draining in-flight handlers.
52
+ */
53
+ close(): Promise<void>;
54
+ }
55
+ export interface PublishOptions {
56
+ /** Explicit correlationId to propagate across services. Defaults to a new UUID. */
57
+ correlationId?: string;
58
+ /** Source plugin identifier. Defaults to 'unknown'. */
59
+ sourcePlugin?: string;
60
+ }
61
+ //# sourceMappingURL=event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../src/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9E;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,MAAM,SAAS,UAAU,IAAI,CACpD,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,KACtB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;OAIG;IACH,OAAO,CAAC,MAAM,SAAS,UAAU,EAC/B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,MAAM,SAAS,UAAU,EACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,GAC5B,OAAO,CAAC,WAAW,CAAC,CAAC;IAExB;;;;OAIG;IACH,gBAAgB,CACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,WAAW,CAAC,CAAC;IAExB;;;;OAIG;IACH,MAAM,CACJ,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=event-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../src/event-bus.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ export type { EventBus, EventHandler, PatternEventHandler, PublishOptions, Unsubscribe, } from './event-bus.js';
2
+ export type { JobQueue, JobHandler, QueueName, QueueDataMap, EnqueueOptions, AgentDispatchJobData, StallCheckJobData, EventBusJobData, } from './job-queue.js';
3
+ export { QUEUE_NAMES } from './job-queue.js';
4
+ export { BullMQEventBus } from './bullmq-event-bus.js';
5
+ export { BullMQJobQueue } from './bullmq-job-queue.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,WAAW,GACZ,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,QAAQ,EACR,UAAU,EACV,SAAS,EACT,YAAY,EACZ,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { QUEUE_NAMES } from './job-queue.js';
2
+ // BullMQ implementations
3
+ export { BullMQEventBus } from './bullmq-event-bus.js';
4
+ export { BullMQJobQueue } from './bullmq-job-queue.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,yBAAyB;AACzB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,84 @@
1
+ import type { OuijaEvent } from '@ouija-dev/types';
2
+ export declare const QUEUE_NAMES: {
3
+ readonly agentDispatch: "ouija.agent-dispatch";
4
+ readonly stallCheck: "ouija.stall-check";
5
+ readonly eventBus: "ouija.event-bus";
6
+ };
7
+ export type QueueName = (typeof QUEUE_NAMES)[keyof typeof QUEUE_NAMES];
8
+ export interface AgentDispatchJobData {
9
+ instanceId: string;
10
+ dispatchId: string;
11
+ agentId: string;
12
+ cardId: string;
13
+ projectId: string;
14
+ workOrderDescription: string;
15
+ /** ISO timestamp of when this dispatch was requested */
16
+ dispatchedAt: string;
17
+ }
18
+ export interface StallCheckJobData {
19
+ instanceId: string;
20
+ dispatchId: string;
21
+ /** ISO timestamp the stall check should fire at (BullMQ delay handles this) */
22
+ expectedBy: string;
23
+ }
24
+ /**
25
+ * Internal job data used by BullMQEventBus to fan-out events to subscribers.
26
+ * The subscriberKey uniquely identifies the registered handler.
27
+ */
28
+ export interface EventBusJobData {
29
+ event: OuijaEvent;
30
+ subscriberKey: string;
31
+ }
32
+ export interface QueueDataMap {
33
+ [QUEUE_NAMES.agentDispatch]: AgentDispatchJobData;
34
+ [QUEUE_NAMES.stallCheck]: StallCheckJobData;
35
+ [QUEUE_NAMES.eventBus]: EventBusJobData;
36
+ }
37
+ export interface EnqueueOptions {
38
+ /** BullMQ job ID. If provided, duplicate jobs with same ID are deduplicated. */
39
+ jobId?: string;
40
+ /** Delay in milliseconds before the job becomes active. */
41
+ delayMs?: number;
42
+ /** Number of retry attempts on failure. Defaults to 3. */
43
+ attempts?: number;
44
+ /** Backoff strategy for retries. */
45
+ backoff?: {
46
+ type: 'exponential' | 'fixed';
47
+ delay: number;
48
+ };
49
+ /** Priority (lower number = higher priority). Defaults to 0. */
50
+ priority?: number;
51
+ }
52
+ export type JobHandler<TData> = (data: TData, jobId: string) => Promise<void>;
53
+ /**
54
+ * JobQueue — durable task dispatch interface.
55
+ *
56
+ * Design rules (Decision 2 in spec §2.4):
57
+ * - Separate from EventBus at the interface level.
58
+ * - Typed via QueueDataMap: `enqueue<TQueue>` ensures data matches queue.
59
+ * - One queue per concern (agentDispatch, stallCheck, eventBus).
60
+ * - BullMQ implementation uses noeviction Redis — jobs must never be evicted.
61
+ */
62
+ export interface JobQueue {
63
+ /**
64
+ * Enqueue a job on the specified queue.
65
+ * Returns the BullMQ job ID.
66
+ */
67
+ enqueue<TQueue extends QueueName>(queue: TQueue, data: QueueDataMap[TQueue], options?: EnqueueOptions): Promise<string>;
68
+ /**
69
+ * Register a processor for the specified queue.
70
+ * Workers are created lazily on the first `process()` call per queue.
71
+ * Calling process() twice on the same queue replaces the handler.
72
+ */
73
+ process<TQueue extends QueueName>(queue: TQueue, handler: JobHandler<QueueDataMap[TQueue]>, concurrency?: number): Promise<void>;
74
+ /**
75
+ * Remove a job by ID from the specified queue.
76
+ * No-ops if the job does not exist or is already completed.
77
+ */
78
+ cancelJob(queue: QueueName, jobId: string): Promise<void>;
79
+ /**
80
+ * Gracefully shut down all workers, waiting for in-flight jobs to complete.
81
+ */
82
+ close(): Promise<void>;
83
+ }
84
+ //# sourceMappingURL=job-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-queue.d.ts","sourceRoot":"","sources":["../src/job-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AAM/D,eAAO,MAAM,WAAW;;;;CAId,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAMvE,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAMD,MAAM,WAAW,YAAY;IAC3B,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,oBAAoB,CAAC;IAClD,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC5C,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;CACzC;AAMD,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,CAAC,EAAE;QACR,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;QAC9B,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD,MAAM,MAAM,UAAU,CAAC,KAAK,IAAI,CAC9B,IAAI,EAAE,KAAK,EACX,KAAK,EAAE,MAAM,KACV,OAAO,CAAC,IAAI,CAAC,CAAC;AAMnB;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB;;;OAGG;IACH,OAAO,CAAC,MAAM,SAAS,SAAS,EAC9B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAC1B,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;;OAIG;IACH,OAAO,CAAC,MAAM,SAAS,SAAS,EAC9B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,EACzC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1D;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
@@ -0,0 +1,9 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Queue names — single source of truth
3
+ // ---------------------------------------------------------------------------
4
+ export const QUEUE_NAMES = {
5
+ agentDispatch: 'ouija.agent-dispatch',
6
+ stallCheck: 'ouija.stall-check',
7
+ eventBus: 'ouija.event-bus',
8
+ };
9
+ //# sourceMappingURL=job-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-queue.js","sourceRoot":"","sources":["../src/job-queue.ts"],"names":[],"mappings":"AAEA,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,aAAa,EAAE,sBAAsB;IACrC,UAAU,EAAE,mBAAmB;IAC/B,QAAQ,EAAE,iBAAiB;CACnB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@ouija-dev/bus",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist/", "README.md"],
8
+ "publishConfig": { "access": "public" },
9
+ "repository": { "type": "git", "url": "https://github.com/muhammadkh4n/ouija.git" },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run"
20
+ },
21
+ "dependencies": {
22
+ "@ouija-dev/types": "*",
23
+ "bullmq": "^5.0.0",
24
+ "ioredis": "^5.3.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "typescript": "^5.5.0",
29
+ "vitest": "^3.0.0"
30
+ }
31
+ }