@nymbalhq/event-bus-redis-streams 0.0.0-stage → 1.2.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.
- package/dist/index.d.ts +46 -0
- package/dist/index.js +697 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -3
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { EventBusAdapter, Logger, HealthReport, NymbalEvent, EventHandler, SubscriptionConfig, Subscription, AdapterContext } from '@nymbalhq/types';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
|
|
4
|
+
interface RedisStreamsEventBusOptions {
|
|
5
|
+
/** `redis://[user:password@]host:port[/db]`; the db index in the path is honoured. */
|
|
6
|
+
url: string;
|
|
7
|
+
/** Namespaces the stream (`<prefix>:events`) and so every consumer group on it. */
|
|
8
|
+
prefix: string;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
/** How long `publish` waits for the groups it delivers to before it rejects (default 15 000). */
|
|
11
|
+
ackTimeoutMs?: number;
|
|
12
|
+
/** How long an entry may stay pending on a consumer before a live consumer of its group takes it over (5 000). */
|
|
13
|
+
claimIdleMs?: number;
|
|
14
|
+
/** The approximate length the stream is trimmed to (`XADD MAXLEN ~`, default 10 000). */
|
|
15
|
+
streamMaxLen?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* An event bus on Redis Streams that keeps the in-process delivery contract. Every event is one entry on
|
|
19
|
+
* `<prefix>:events`. A named subscription is a durable consumer group, shared by every instance, so each event is
|
|
20
|
+
* handled once per name; an unnamed subscription reads the stream from where it subscribed, so every subscriber on
|
|
21
|
+
* every instance receives every event. `publish` resolves once every matching group this instance reads, and every
|
|
22
|
+
* matching unnamed subscription it holds, has handled the event.
|
|
23
|
+
*/
|
|
24
|
+
declare class RedisStreamsEventBus implements EventBusAdapter {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(cmd: Redis, reader: Redis, options: RedisStreamsEventBusOptions);
|
|
27
|
+
capabilities(): readonly string[];
|
|
28
|
+
healthCheck(): Promise<HealthReport>;
|
|
29
|
+
publish<T>(event: NymbalEvent<T>): Promise<void>;
|
|
30
|
+
publishBatch(events: NymbalEvent<unknown>[]): Promise<void>;
|
|
31
|
+
replay(events: NymbalEvent<unknown>[]): Promise<void>;
|
|
32
|
+
subscribe(eventPattern: string | string[], handler: EventHandler, config?: SubscriptionConfig): Promise<Subscription>;
|
|
33
|
+
unsubscribe(subscription: Subscription): Promise<void>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Connects the bus's two clients: one for commands, one for the blocking read of the stream. */
|
|
37
|
+
declare function createRedisStreamsEventBus(options: RedisStreamsEventBusOptions): Promise<RedisStreamsEventBus>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The kernel's entry: `infrastructure.eventBus: 'redis-streams'`, with `{ url, prefix, ackTimeoutMs, claimIdleMs,
|
|
41
|
+
* streamMaxLen }` from the `redis` block.
|
|
42
|
+
*/
|
|
43
|
+
declare function createAdapter(ctx: AdapterContext): Promise<EventBusAdapter>;
|
|
44
|
+
|
|
45
|
+
export { RedisStreamsEventBus, createAdapter, createRedisStreamsEventBus };
|
|
46
|
+
export type { RedisStreamsEventBusOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { ConfigError } from "@nymbalhq/types";
|
|
3
|
+
|
|
4
|
+
// src/redis-streams-event-bus.ts
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { Redis } from "ioredis";
|
|
7
|
+
import {
|
|
8
|
+
AdapterError,
|
|
9
|
+
boundedHealthCheck,
|
|
10
|
+
connectionSecrets,
|
|
11
|
+
HEALTH_CHECK_TIMEOUT_MS,
|
|
12
|
+
ValidationError
|
|
13
|
+
} from "@nymbalhq/types";
|
|
14
|
+
var READ_COUNT = 10;
|
|
15
|
+
var TAIL_BLOCK_MS = 5e3;
|
|
16
|
+
var ACK_POLL_MS = 25;
|
|
17
|
+
function compilePattern(pattern) {
|
|
18
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\*]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
19
|
+
return new RegExp(`^${escaped}$`);
|
|
20
|
+
}
|
|
21
|
+
function compareIds(a, b) {
|
|
22
|
+
const [am = 0, as = 0] = a.split("-").map(Number);
|
|
23
|
+
const [bm = 0, bs = 0] = b.split("-").map(Number);
|
|
24
|
+
return am !== bm ? am - bm : as - bs;
|
|
25
|
+
}
|
|
26
|
+
function fieldsOf(raw) {
|
|
27
|
+
const out = {};
|
|
28
|
+
if (!Array.isArray(raw)) return out;
|
|
29
|
+
for (let i = 0; i + 1 < raw.length; i += 2) out[String(raw[i])] = String(raw[i + 1]);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function entriesOf(raw) {
|
|
33
|
+
if (!Array.isArray(raw)) return [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const item of raw) {
|
|
36
|
+
if (!Array.isArray(item) || typeof item[0] !== "string") continue;
|
|
37
|
+
const json = fieldsOf(item[1])["event"];
|
|
38
|
+
let event = null;
|
|
39
|
+
if (json !== void 0) {
|
|
40
|
+
try {
|
|
41
|
+
event = JSON.parse(json);
|
|
42
|
+
} catch {
|
|
43
|
+
event = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
out.push({ id: item[0], event });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function streamEntries(reply) {
|
|
51
|
+
if (!Array.isArray(reply)) return [];
|
|
52
|
+
return reply.flatMap((stream) => Array.isArray(stream) ? entriesOf(stream[1]) : []);
|
|
53
|
+
}
|
|
54
|
+
var RedisStreamsEventBus = class {
|
|
55
|
+
#cmd;
|
|
56
|
+
#reader;
|
|
57
|
+
#url;
|
|
58
|
+
#stream;
|
|
59
|
+
#consumer = randomUUID();
|
|
60
|
+
#logger;
|
|
61
|
+
#ackTimeoutMs;
|
|
62
|
+
#claimIdleMs;
|
|
63
|
+
#streamMaxLen;
|
|
64
|
+
#subs = /* @__PURE__ */ new Map();
|
|
65
|
+
#groups = /* @__PURE__ */ new Map();
|
|
66
|
+
#waits = /* @__PURE__ */ new Map();
|
|
67
|
+
#tail = null;
|
|
68
|
+
#tailStarted = null;
|
|
69
|
+
#tailFrom = "0-0";
|
|
70
|
+
#tick = null;
|
|
71
|
+
#poll = null;
|
|
72
|
+
#closed = false;
|
|
73
|
+
constructor(cmd, reader, options) {
|
|
74
|
+
this.#cmd = cmd;
|
|
75
|
+
this.#reader = reader;
|
|
76
|
+
this.#url = options.url;
|
|
77
|
+
this.#stream = `${options.prefix}:events`;
|
|
78
|
+
this.#logger = options.logger;
|
|
79
|
+
this.#ackTimeoutMs = options.ackTimeoutMs ?? 15e3;
|
|
80
|
+
this.#claimIdleMs = options.claimIdleMs ?? 5e3;
|
|
81
|
+
this.#streamMaxLen = options.streamMaxLen ?? 1e4;
|
|
82
|
+
}
|
|
83
|
+
capabilities() {
|
|
84
|
+
return ["publish", "subscribe", "wildcard", "replay", "consumer-groups", "fan-out"];
|
|
85
|
+
}
|
|
86
|
+
healthCheck() {
|
|
87
|
+
return pingRedis(this.#url);
|
|
88
|
+
}
|
|
89
|
+
async publish(event) {
|
|
90
|
+
await this.publishBatch([event]);
|
|
91
|
+
}
|
|
92
|
+
async publishBatch(events) {
|
|
93
|
+
for (const event of events) validate(event);
|
|
94
|
+
this.#assertOpen();
|
|
95
|
+
if (events.length === 0) return;
|
|
96
|
+
const waits = events.map((event) => this.#wait(event));
|
|
97
|
+
const pipeline = this.#cmd.pipeline();
|
|
98
|
+
for (const event of events) {
|
|
99
|
+
pipeline.xadd(
|
|
100
|
+
this.#stream,
|
|
101
|
+
"MAXLEN",
|
|
102
|
+
"~",
|
|
103
|
+
this.#streamMaxLen,
|
|
104
|
+
"*",
|
|
105
|
+
"type",
|
|
106
|
+
event.type,
|
|
107
|
+
"event",
|
|
108
|
+
JSON.stringify(event)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
let replies;
|
|
112
|
+
try {
|
|
113
|
+
replies = await pipeline.exec();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
for (const wait of waits) this.#drop(wait);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
const failed = replies === null ? new AdapterError("publish_failed", "the stream write was aborted") : replies.find(([error]) => error !== null)?.[0];
|
|
119
|
+
if (failed !== void 0 && failed !== null) {
|
|
120
|
+
for (const wait of waits) this.#drop(wait);
|
|
121
|
+
throw failed;
|
|
122
|
+
}
|
|
123
|
+
replies?.forEach(([, id], i) => {
|
|
124
|
+
waits[i].entryId = String(id);
|
|
125
|
+
});
|
|
126
|
+
this.#signalGroups();
|
|
127
|
+
this.#schedulePoll();
|
|
128
|
+
await Promise.all(waits.map((w) => w.promise));
|
|
129
|
+
}
|
|
130
|
+
async replay(events) {
|
|
131
|
+
for (const event of events) {
|
|
132
|
+
validate(event);
|
|
133
|
+
const derived = [...this.#subs.values()].filter((s) => s.derived && matches(s, event.type));
|
|
134
|
+
await Promise.all(derived.map((s) => this.#run(s, event)));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async subscribe(eventPattern, handler, config) {
|
|
138
|
+
this.#assertOpen();
|
|
139
|
+
await this.#startTail();
|
|
140
|
+
const patterns = Array.isArray(eventPattern) ? eventPattern : [eventPattern];
|
|
141
|
+
const sub = {
|
|
142
|
+
id: randomUUID(),
|
|
143
|
+
patterns,
|
|
144
|
+
regexes: patterns.map(compilePattern),
|
|
145
|
+
handler,
|
|
146
|
+
derived: config?.derived === true,
|
|
147
|
+
live: true,
|
|
148
|
+
starting: false,
|
|
149
|
+
queue: [],
|
|
150
|
+
running: null
|
|
151
|
+
};
|
|
152
|
+
const name = config?.name;
|
|
153
|
+
if (name !== void 0) {
|
|
154
|
+
let group = this.#groups.get(name);
|
|
155
|
+
if (group === void 0) {
|
|
156
|
+
group = {
|
|
157
|
+
name,
|
|
158
|
+
subs: /* @__PURE__ */ new Set(),
|
|
159
|
+
ready: this.#createGroup(name),
|
|
160
|
+
draining: null,
|
|
161
|
+
again: false,
|
|
162
|
+
claimDue: false
|
|
163
|
+
};
|
|
164
|
+
this.#groups.set(name, group);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await group.ready;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (group.subs.size === 0) this.#groups.delete(name);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
group.subs.add(sub);
|
|
173
|
+
this.#subs.set(sub.id, sub);
|
|
174
|
+
group.claimDue = true;
|
|
175
|
+
void this.#drainGroup(group);
|
|
176
|
+
} else {
|
|
177
|
+
sub.since = this.#tailFrom;
|
|
178
|
+
sub.starting = true;
|
|
179
|
+
this.#subs.set(sub.id, sub);
|
|
180
|
+
let last;
|
|
181
|
+
try {
|
|
182
|
+
last = await this.#lastId();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
sub.live = false;
|
|
185
|
+
this.#subs.delete(sub.id);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
sub.since = last;
|
|
189
|
+
sub.starting = false;
|
|
190
|
+
const later = sub.queue.filter((q) => compareIds(q.entryId, last) > 0);
|
|
191
|
+
sub.queue.splice(0, sub.queue.length, ...later);
|
|
192
|
+
if (sub.queue.length > 0 && sub.running === null) sub.running = this.#runQueue(sub);
|
|
193
|
+
}
|
|
194
|
+
this.#startTick();
|
|
195
|
+
const self = this;
|
|
196
|
+
return {
|
|
197
|
+
id: sub.id,
|
|
198
|
+
patterns,
|
|
199
|
+
async unsubscribe() {
|
|
200
|
+
await self.#unsubscribe(sub.id);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async unsubscribe(subscription) {
|
|
205
|
+
await this.#unsubscribe(subscription.id);
|
|
206
|
+
}
|
|
207
|
+
async close() {
|
|
208
|
+
if (this.#closed) return;
|
|
209
|
+
this.#closed = true;
|
|
210
|
+
if (this.#tick !== null) clearInterval(this.#tick);
|
|
211
|
+
if (this.#poll !== null) clearTimeout(this.#poll);
|
|
212
|
+
this.#tick = null;
|
|
213
|
+
this.#poll = null;
|
|
214
|
+
for (const waits of [...this.#waits.values()]) {
|
|
215
|
+
for (const wait of [...waits]) {
|
|
216
|
+
this.#drop(wait);
|
|
217
|
+
wait.resolve();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
this.#reader.disconnect();
|
|
221
|
+
await this.#tail?.catch(() => void 0);
|
|
222
|
+
const groups = [...this.#groups.values()];
|
|
223
|
+
await Promise.all([
|
|
224
|
+
...groups.map((g) => g.draining?.catch(() => void 0)),
|
|
225
|
+
...[...this.#subs.values()].map((s) => s.running?.catch(() => void 0))
|
|
226
|
+
]);
|
|
227
|
+
for (const sub of this.#subs.values()) sub.live = false;
|
|
228
|
+
this.#subs.clear();
|
|
229
|
+
await Promise.all(groups.map((g) => this.#leaveGroup(g.name)));
|
|
230
|
+
this.#groups.clear();
|
|
231
|
+
await this.#cmd.quit().catch(() => this.#cmd.disconnect());
|
|
232
|
+
}
|
|
233
|
+
// ── Waiting ────────────────────────────────────────────────────────────────
|
|
234
|
+
#wait(event) {
|
|
235
|
+
const targets = /* @__PURE__ */ new Set();
|
|
236
|
+
for (const group of this.#groups.values()) {
|
|
237
|
+
if ([...group.subs].some((s) => s.live && matches(s, event.type)))
|
|
238
|
+
targets.add(`g:${group.name}`);
|
|
239
|
+
}
|
|
240
|
+
for (const sub of this.#subs.values()) {
|
|
241
|
+
if (sub.since !== void 0 && sub.live && !sub.starting && matches(sub, event.type))
|
|
242
|
+
targets.add(`s:${sub.id}`);
|
|
243
|
+
}
|
|
244
|
+
let resolve;
|
|
245
|
+
let reject;
|
|
246
|
+
const promise = new Promise((res, rej) => {
|
|
247
|
+
resolve = res;
|
|
248
|
+
reject = rej;
|
|
249
|
+
});
|
|
250
|
+
const wait = {
|
|
251
|
+
event,
|
|
252
|
+
entryId: null,
|
|
253
|
+
targets,
|
|
254
|
+
startedAt: Date.now(),
|
|
255
|
+
resolve,
|
|
256
|
+
reject,
|
|
257
|
+
promise,
|
|
258
|
+
timer: void 0
|
|
259
|
+
};
|
|
260
|
+
if (targets.size === 0) {
|
|
261
|
+
resolve();
|
|
262
|
+
return wait;
|
|
263
|
+
}
|
|
264
|
+
wait.timer = setTimeout(() => this.#timeout(wait), this.#ackTimeoutMs);
|
|
265
|
+
const forEvent = this.#waits.get(event.id) ?? /* @__PURE__ */ new Set();
|
|
266
|
+
forEvent.add(wait);
|
|
267
|
+
this.#waits.set(event.id, forEvent);
|
|
268
|
+
return wait;
|
|
269
|
+
}
|
|
270
|
+
#timeout(wait) {
|
|
271
|
+
if (!this.#drop(wait)) return;
|
|
272
|
+
const pending = [...wait.targets];
|
|
273
|
+
this.#logger?.error(
|
|
274
|
+
{
|
|
275
|
+
eventId: wait.event.id,
|
|
276
|
+
eventType: wait.event.type,
|
|
277
|
+
entryId: wait.entryId,
|
|
278
|
+
unacknowledged: pending
|
|
279
|
+
},
|
|
280
|
+
"redis streams publish timed out waiting for its handlers"
|
|
281
|
+
);
|
|
282
|
+
wait.reject(
|
|
283
|
+
new AdapterError(
|
|
284
|
+
"publish_timeout",
|
|
285
|
+
`publish of ${wait.event.type} (${wait.event.id}) was not handled within ${this.#ackTimeoutMs} ms by ${pending.join(", ")}`,
|
|
286
|
+
{ context: { eventId: wait.event.id, unacknowledged: pending } }
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
/** Forgets `wait`; false when it was already settled. */
|
|
291
|
+
#drop(wait) {
|
|
292
|
+
const forEvent = this.#waits.get(wait.event.id);
|
|
293
|
+
if (forEvent === void 0 || !forEvent.delete(wait)) return false;
|
|
294
|
+
if (forEvent.size === 0) this.#waits.delete(wait.event.id);
|
|
295
|
+
clearTimeout(wait.timer);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
/** `target` has finished with `eventId`: a wait left with nothing to wait on resolves. */
|
|
299
|
+
#settle(eventId, target) {
|
|
300
|
+
for (const wait of [...this.#waits.get(eventId) ?? []]) this.#remove(wait, target);
|
|
301
|
+
}
|
|
302
|
+
#settleEverywhere(target) {
|
|
303
|
+
for (const waits of [...this.#waits.values()]) {
|
|
304
|
+
for (const wait of [...waits]) this.#remove(wait, target);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
#remove(wait, target) {
|
|
308
|
+
if (!wait.targets.delete(target) || wait.targets.size > 0) return;
|
|
309
|
+
if (this.#drop(wait)) wait.resolve();
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* A group's entry may be handled by another instance's consumer: asks Redis, for each wait still on a group, whether
|
|
313
|
+
* the group has delivered the entry and holds it pending no more.
|
|
314
|
+
*/
|
|
315
|
+
#schedulePoll() {
|
|
316
|
+
if (this.#poll !== null || this.#closed || this.#waits.size === 0) return;
|
|
317
|
+
this.#poll = setTimeout(() => {
|
|
318
|
+
this.#poll = null;
|
|
319
|
+
void this.#pollAcks().finally(() => this.#schedulePoll());
|
|
320
|
+
}, ACK_POLL_MS);
|
|
321
|
+
}
|
|
322
|
+
async #pollAcks() {
|
|
323
|
+
const due = [];
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
for (const waits of this.#waits.values()) {
|
|
326
|
+
for (const wait of waits) {
|
|
327
|
+
if (wait.entryId === null || now - wait.startedAt < ACK_POLL_MS) continue;
|
|
328
|
+
for (const target of wait.targets) {
|
|
329
|
+
if (target.startsWith("g:")) due.push({ wait, group: target.slice(2) });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (due.length === 0) return;
|
|
334
|
+
try {
|
|
335
|
+
const pipeline = this.#cmd.pipeline().xinfo("GROUPS", this.#stream);
|
|
336
|
+
for (const { wait, group } of due) {
|
|
337
|
+
pipeline.xpending(this.#stream, group, wait.entryId, wait.entryId, 1);
|
|
338
|
+
}
|
|
339
|
+
const replies = await pipeline.exec() ?? [];
|
|
340
|
+
const delivered = /* @__PURE__ */ new Map();
|
|
341
|
+
const [infoError, info] = replies[0] ?? [null, []];
|
|
342
|
+
if (infoError !== null) return;
|
|
343
|
+
for (const raw of info ?? []) {
|
|
344
|
+
const fields = fieldsOf(raw);
|
|
345
|
+
if (fields["name"] !== void 0) {
|
|
346
|
+
delivered.set(fields["name"], fields["last-delivered-id"] ?? "0-0");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
due.forEach(({ wait, group }, i) => {
|
|
350
|
+
const [error, pending] = replies[i + 1] ?? [null, null];
|
|
351
|
+
const last = delivered.get(group);
|
|
352
|
+
if (error !== null || last === void 0 || !Array.isArray(pending)) return;
|
|
353
|
+
if (pending.length === 0 && compareIds(last, wait.entryId) >= 0) {
|
|
354
|
+
this.#remove(wait, `g:${group}`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (!this.#closed) this.#logger?.warn({ err }, "redis streams: acknowledgement poll failed");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// ── Reading ────────────────────────────────────────────────────────────────
|
|
362
|
+
async #createGroup(name) {
|
|
363
|
+
try {
|
|
364
|
+
await this.#cmd.xgroup("CREATE", this.#stream, name, "$", "MKSTREAM");
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (!String(error.message).includes("BUSYGROUP")) throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async #lastId() {
|
|
370
|
+
const last = await this.#cmd.xrevrange(this.#stream, "+", "-", "COUNT", 1);
|
|
371
|
+
return last[0]?.[0] ?? "0-0";
|
|
372
|
+
}
|
|
373
|
+
/** One reader per instance tails the stream: it feeds the unnamed subscriptions and wakes the groups. */
|
|
374
|
+
#startTail() {
|
|
375
|
+
this.#tailStarted ??= this.#lastId().then((last) => {
|
|
376
|
+
this.#tailFrom = last;
|
|
377
|
+
this.#tail = this.#runTail();
|
|
378
|
+
});
|
|
379
|
+
return this.#tailStarted;
|
|
380
|
+
}
|
|
381
|
+
async #runTail() {
|
|
382
|
+
while (!this.#closed) {
|
|
383
|
+
let reply;
|
|
384
|
+
try {
|
|
385
|
+
reply = await this.#reader.xread(
|
|
386
|
+
"COUNT",
|
|
387
|
+
100,
|
|
388
|
+
"BLOCK",
|
|
389
|
+
TAIL_BLOCK_MS,
|
|
390
|
+
"STREAMS",
|
|
391
|
+
this.#stream,
|
|
392
|
+
this.#tailFrom
|
|
393
|
+
);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
if (this.#closed) return;
|
|
396
|
+
this.#logger?.warn({ err }, "redis streams: reading the stream failed; retrying");
|
|
397
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const entries = streamEntries(reply);
|
|
401
|
+
for (const entry of entries) {
|
|
402
|
+
this.#tailFrom = entry.id;
|
|
403
|
+
if (entry.event !== null) this.#feedUnnamed(entry.id, entry.event);
|
|
404
|
+
}
|
|
405
|
+
if (entries.length > 0) this.#signalGroups();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
#feedUnnamed(entryId, event) {
|
|
409
|
+
for (const sub of this.#subs.values()) {
|
|
410
|
+
if (sub.since === void 0 || !sub.live || compareIds(entryId, sub.since) <= 0) continue;
|
|
411
|
+
if (!matches(sub, event.type)) continue;
|
|
412
|
+
sub.queue.push({ entryId, event });
|
|
413
|
+
if (sub.running === null && !sub.starting) sub.running = this.#runQueue(sub);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** An unnamed subscription takes its events one at a time, in stream order. */
|
|
417
|
+
async #runQueue(sub) {
|
|
418
|
+
try {
|
|
419
|
+
for (let next = sub.queue.shift(); next !== void 0; next = sub.queue.shift()) {
|
|
420
|
+
if (!sub.live || this.#closed) break;
|
|
421
|
+
await this.#run(sub, next.event);
|
|
422
|
+
this.#settle(next.event.id, `s:${sub.id}`);
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
425
|
+
sub.running = null;
|
|
426
|
+
sub.queue.length = 0;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
#startTick() {
|
|
430
|
+
if (this.#tick !== null || this.#closed) return;
|
|
431
|
+
const every = Math.min(1e3, Math.max(50, Math.floor(this.#claimIdleMs / 4)));
|
|
432
|
+
this.#tick = setInterval(() => {
|
|
433
|
+
for (const group of this.#groups.values()) {
|
|
434
|
+
group.claimDue = true;
|
|
435
|
+
void this.#drainGroup(group);
|
|
436
|
+
}
|
|
437
|
+
}, every);
|
|
438
|
+
this.#tick.unref();
|
|
439
|
+
}
|
|
440
|
+
#signalGroups() {
|
|
441
|
+
for (const group of this.#groups.values()) void this.#drainGroup(group);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* A group's consumer on this instance, one pass at a time: entries a dead consumer left pending past `claimIdleMs`
|
|
445
|
+
* first (when due), then new ones, each handled and acknowledged in stream order.
|
|
446
|
+
*/
|
|
447
|
+
#drainGroup(group) {
|
|
448
|
+
if (group.draining !== null) {
|
|
449
|
+
group.again = true;
|
|
450
|
+
return group.draining;
|
|
451
|
+
}
|
|
452
|
+
group.draining = (async () => {
|
|
453
|
+
do {
|
|
454
|
+
group.again = false;
|
|
455
|
+
if (group.claimDue) {
|
|
456
|
+
group.claimDue = false;
|
|
457
|
+
await this.#claim(group);
|
|
458
|
+
}
|
|
459
|
+
await this.#readNew(group);
|
|
460
|
+
} while (group.again && !this.#closed && group.subs.size > 0);
|
|
461
|
+
})().catch((err) => {
|
|
462
|
+
if (!this.#closed)
|
|
463
|
+
this.#logger?.warn({ err, group: group.name }, "redis streams: group read failed");
|
|
464
|
+
}).finally(() => {
|
|
465
|
+
group.draining = null;
|
|
466
|
+
if (group.again && !this.#closed && group.subs.size > 0) void this.#drainGroup(group);
|
|
467
|
+
});
|
|
468
|
+
return group.draining;
|
|
469
|
+
}
|
|
470
|
+
async #readNew(group) {
|
|
471
|
+
for (; ; ) {
|
|
472
|
+
if (this.#closed || group.subs.size === 0) return;
|
|
473
|
+
let reply;
|
|
474
|
+
try {
|
|
475
|
+
reply = await this.#cmd.xreadgroup(
|
|
476
|
+
"GROUP",
|
|
477
|
+
group.name,
|
|
478
|
+
this.#consumer,
|
|
479
|
+
"COUNT",
|
|
480
|
+
READ_COUNT,
|
|
481
|
+
"STREAMS",
|
|
482
|
+
this.#stream,
|
|
483
|
+
">"
|
|
484
|
+
);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (String(error.message).includes("NOGROUP")) {
|
|
487
|
+
await this.#createGroup(group.name);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
const entries = streamEntries(reply);
|
|
493
|
+
await this.#handle(group, entries);
|
|
494
|
+
if (entries.length < READ_COUNT) return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async #claim(group) {
|
|
498
|
+
let start = "0-0";
|
|
499
|
+
do {
|
|
500
|
+
if (this.#closed || group.subs.size === 0) return;
|
|
501
|
+
const reply = await this.#cmd.xautoclaim(
|
|
502
|
+
this.#stream,
|
|
503
|
+
group.name,
|
|
504
|
+
this.#consumer,
|
|
505
|
+
this.#claimIdleMs,
|
|
506
|
+
start,
|
|
507
|
+
"COUNT",
|
|
508
|
+
READ_COUNT
|
|
509
|
+
);
|
|
510
|
+
start = reply[0];
|
|
511
|
+
const entries = entriesOf(reply[1]);
|
|
512
|
+
if (entries.length > 0) {
|
|
513
|
+
this.#logger?.info(
|
|
514
|
+
{ group: group.name, entries: entries.map((e) => e.id) },
|
|
515
|
+
"redis streams: took over entries a consumer left pending"
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
await this.#handle(group, entries);
|
|
519
|
+
} while (start !== "0-0");
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Handles each entry for the group's matching subscriptions, then acknowledges it; non-matching ones in bulk. Once
|
|
523
|
+
* the group has no live subscription here, the rest stay pending, for a live consumer of the group to take over.
|
|
524
|
+
*/
|
|
525
|
+
async #handle(group, entries) {
|
|
526
|
+
const skipped = [];
|
|
527
|
+
for (const entry of entries) {
|
|
528
|
+
if (![...group.subs].some((s) => s.live)) break;
|
|
529
|
+
const event = entry.event;
|
|
530
|
+
const subs = event === null ? [] : [...group.subs].filter((s) => s.live && matches(s, event.type));
|
|
531
|
+
if (event === null || subs.length === 0) {
|
|
532
|
+
if (event === null)
|
|
533
|
+
this.#logger?.error({ entryId: entry.id }, "redis streams: unreadable entry");
|
|
534
|
+
skipped.push(entry.id);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
await Promise.all(subs.map((s) => this.#run(s, event)));
|
|
538
|
+
await this.#cmd.xack(this.#stream, group.name, entry.id);
|
|
539
|
+
this.#settle(event.id, `g:${group.name}`);
|
|
540
|
+
}
|
|
541
|
+
if (skipped.length > 0) await this.#cmd.xack(this.#stream, group.name, ...skipped);
|
|
542
|
+
}
|
|
543
|
+
/** Runs one handler; an error is logged and acknowledged like a success, as the in-process bus does. */
|
|
544
|
+
async #run(sub, event) {
|
|
545
|
+
try {
|
|
546
|
+
await sub.handler(event);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
this.#logger?.error(
|
|
549
|
+
{ err, subscriptionId: sub.id, eventType: event.type, eventId: event.id },
|
|
550
|
+
"Event handler failed"
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// ── Leaving ────────────────────────────────────────────────────────────────
|
|
555
|
+
async #unsubscribe(id) {
|
|
556
|
+
const sub = this.#subs.get(id);
|
|
557
|
+
if (sub === void 0) return;
|
|
558
|
+
sub.live = false;
|
|
559
|
+
this.#subs.delete(id);
|
|
560
|
+
if (sub.since !== void 0) {
|
|
561
|
+
sub.queue.length = 0;
|
|
562
|
+
this.#settleEverywhere(`s:${id}`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
for (const group of this.#groups.values()) {
|
|
566
|
+
if (!group.subs.delete(sub) || group.subs.size > 0) continue;
|
|
567
|
+
this.#groups.delete(group.name);
|
|
568
|
+
this.#settleEverywhere(`g:${group.name}`);
|
|
569
|
+
await group.draining?.catch(() => void 0);
|
|
570
|
+
if (!this.#closed) await this.#leaveGroup(group.name);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/** Removes this instance's consumer from the group when it holds nothing pending, which would be lost with it. */
|
|
574
|
+
async #leaveGroup(name) {
|
|
575
|
+
if (this.#groups.has(name) && !this.#closed) return;
|
|
576
|
+
try {
|
|
577
|
+
const summary = await this.#cmd.xpending(
|
|
578
|
+
this.#stream,
|
|
579
|
+
name,
|
|
580
|
+
"-",
|
|
581
|
+
"+",
|
|
582
|
+
1,
|
|
583
|
+
this.#consumer
|
|
584
|
+
);
|
|
585
|
+
if (summary.length === 0) {
|
|
586
|
+
await this.#cmd.xgroup("DELCONSUMER", this.#stream, name, this.#consumer);
|
|
587
|
+
}
|
|
588
|
+
} catch (err) {
|
|
589
|
+
this.#logger?.debug({ err, group: name }, "redis streams: consumer left in place");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
#assertOpen() {
|
|
593
|
+
if (this.#closed) {
|
|
594
|
+
throw new AdapterError("closed", "the redis streams event bus is closed");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function matches(sub, type) {
|
|
599
|
+
return sub.regexes.some((re) => re.test(type));
|
|
600
|
+
}
|
|
601
|
+
function validate(event) {
|
|
602
|
+
if (!event.id || !event.type || !event.timestamp || !event.source) {
|
|
603
|
+
throw new ValidationError("Event is missing required envelope fields", { context: { event } });
|
|
604
|
+
}
|
|
605
|
+
if (!event.metadata || !event.metadata.storeId || !event.metadata.environment) {
|
|
606
|
+
throw new ValidationError("Event metadata is missing required fields", { context: { event } });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function pingRedis(url, timeoutMs = HEALTH_CHECK_TIMEOUT_MS) {
|
|
610
|
+
const client = new Redis(url, {
|
|
611
|
+
lazyConnect: true,
|
|
612
|
+
retryStrategy: () => null,
|
|
613
|
+
maxRetriesPerRequest: 0,
|
|
614
|
+
enableOfflineQueue: false,
|
|
615
|
+
connectTimeout: timeoutMs
|
|
616
|
+
});
|
|
617
|
+
client.on("error", () => void 0);
|
|
618
|
+
return boundedHealthCheck(
|
|
619
|
+
async () => {
|
|
620
|
+
await client.connect();
|
|
621
|
+
await client.ping();
|
|
622
|
+
return { status: "healthy" };
|
|
623
|
+
},
|
|
624
|
+
{ timeoutMs, redact: connectionSecrets(url) }
|
|
625
|
+
).finally(() => client.disconnect());
|
|
626
|
+
}
|
|
627
|
+
async function connect(url, logger, role) {
|
|
628
|
+
const client = new Redis(
|
|
629
|
+
url,
|
|
630
|
+
role === "reader" ? { lazyConnect: true, maxRetriesPerRequest: null } : {
|
|
631
|
+
lazyConnect: true,
|
|
632
|
+
maxRetriesPerRequest: 1,
|
|
633
|
+
retryStrategy: (times) => Math.min(times * 200, 1e3),
|
|
634
|
+
connectTimeout: 2e3
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
client.on("error", (err) => logger?.warn({ err, role }, "redis connection error"));
|
|
638
|
+
try {
|
|
639
|
+
await client.connect();
|
|
640
|
+
} catch (error) {
|
|
641
|
+
client.disconnect();
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
return client;
|
|
645
|
+
}
|
|
646
|
+
async function createRedisStreamsEventBus(options) {
|
|
647
|
+
const cmd = await connect(options.url, options.logger, "commands");
|
|
648
|
+
try {
|
|
649
|
+
const reader = await connect(options.url, options.logger, "reader");
|
|
650
|
+
return new RedisStreamsEventBus(cmd, reader, options);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
cmd.disconnect();
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/index.ts
|
|
658
|
+
function positive(value, name) {
|
|
659
|
+
if (value === void 0) return void 0;
|
|
660
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
661
|
+
throw new ConfigError(`@nymbalhq/event-bus-redis-streams: ${name} must be a positive integer`, {
|
|
662
|
+
code: "config.invalid",
|
|
663
|
+
context: { slot: "eventBus", [name]: value }
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
async function createAdapter(ctx) {
|
|
667
|
+
if (ctx.slot !== "eventBus") {
|
|
668
|
+
throw new ConfigError(`@nymbalhq/event-bus-redis-streams serves eventBus, not ${ctx.slot}`, {
|
|
669
|
+
code: "adapter.wrong_slot",
|
|
670
|
+
context: { slot: ctx.slot }
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const { url, prefix } = ctx.config;
|
|
674
|
+
if (typeof url !== "string" || url === "" || typeof prefix !== "string" || prefix === "") {
|
|
675
|
+
throw new ConfigError("@nymbalhq/event-bus-redis-streams needs a Redis url and a key prefix", {
|
|
676
|
+
code: "config.invalid",
|
|
677
|
+
context: { slot: ctx.slot }
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const ackTimeoutMs = positive(ctx.config["ackTimeoutMs"], "ackTimeoutMs");
|
|
681
|
+
const claimIdleMs = positive(ctx.config["claimIdleMs"], "claimIdleMs");
|
|
682
|
+
const streamMaxLen = positive(ctx.config["streamMaxLen"], "streamMaxLen");
|
|
683
|
+
return createRedisStreamsEventBus({
|
|
684
|
+
url,
|
|
685
|
+
prefix,
|
|
686
|
+
logger: ctx.logger,
|
|
687
|
+
...ackTimeoutMs !== void 0 && { ackTimeoutMs },
|
|
688
|
+
...claimIdleMs !== void 0 && { claimIdleMs },
|
|
689
|
+
...streamMaxLen !== void 0 && { streamMaxLen }
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
export {
|
|
693
|
+
RedisStreamsEventBus,
|
|
694
|
+
createAdapter,
|
|
695
|
+
createRedisStreamsEventBus
|
|
696
|
+
};
|
|
697
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/redis-streams-event-bus.ts"],"sourcesContent":["import { ConfigError, type AdapterContext, type EventBusAdapter } from '@nymbalhq/types'\nimport {\n createRedisStreamsEventBus,\n RedisStreamsEventBus,\n type RedisStreamsEventBusOptions,\n} from './redis-streams-event-bus.js'\n\nexport { createRedisStreamsEventBus, RedisStreamsEventBus, type RedisStreamsEventBusOptions }\n\nfunction positive(value: unknown, name: string): number | undefined {\n if (value === undefined) return undefined\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value\n throw new ConfigError(`@nymbalhq/event-bus-redis-streams: ${name} must be a positive integer`, {\n code: 'config.invalid',\n context: { slot: 'eventBus', [name]: value },\n })\n}\n\n/**\n * The kernel's entry: `infrastructure.eventBus: 'redis-streams'`, with `{ url, prefix, ackTimeoutMs, claimIdleMs,\n * streamMaxLen }` from the `redis` block.\n */\nexport async function createAdapter(ctx: AdapterContext): Promise<EventBusAdapter> {\n if (ctx.slot !== 'eventBus') {\n throw new ConfigError(`@nymbalhq/event-bus-redis-streams serves eventBus, not ${ctx.slot}`, {\n code: 'adapter.wrong_slot',\n context: { slot: ctx.slot },\n })\n }\n const { url, prefix } = ctx.config\n if (typeof url !== 'string' || url === '' || typeof prefix !== 'string' || prefix === '') {\n throw new ConfigError('@nymbalhq/event-bus-redis-streams needs a Redis url and a key prefix', {\n code: 'config.invalid',\n context: { slot: ctx.slot },\n })\n }\n const ackTimeoutMs = positive(ctx.config['ackTimeoutMs'], 'ackTimeoutMs')\n const claimIdleMs = positive(ctx.config['claimIdleMs'], 'claimIdleMs')\n const streamMaxLen = positive(ctx.config['streamMaxLen'], 'streamMaxLen')\n return createRedisStreamsEventBus({\n url,\n prefix,\n logger: ctx.logger,\n ...(ackTimeoutMs !== undefined && { ackTimeoutMs }),\n ...(claimIdleMs !== undefined && { claimIdleMs }),\n ...(streamMaxLen !== undefined && { streamMaxLen }),\n })\n}\n","import { randomUUID } from 'node:crypto'\nimport { Redis } from 'ioredis'\nimport {\n AdapterError,\n boundedHealthCheck,\n connectionSecrets,\n HEALTH_CHECK_TIMEOUT_MS,\n ValidationError,\n type EventBusAdapter,\n type EventHandler,\n type HealthReport,\n type Logger,\n type NymbalEvent,\n type Subscription,\n type SubscriptionConfig,\n} from '@nymbalhq/types'\n\nexport interface RedisStreamsEventBusOptions {\n /** `redis://[user:password@]host:port[/db]`; the db index in the path is honoured. */\n url: string\n /** Namespaces the stream (`<prefix>:events`) and so every consumer group on it. */\n prefix: string\n logger?: Logger\n /** How long `publish` waits for the groups it delivers to before it rejects (default 15 000). */\n ackTimeoutMs?: number\n /** How long an entry may stay pending on a consumer before a live consumer of its group takes it over (5 000). */\n claimIdleMs?: number\n /** The approximate length the stream is trimmed to (`XADD MAXLEN ~`, default 10 000). */\n streamMaxLen?: number\n}\n\ninterface Sub {\n id: string\n patterns: string[]\n regexes: RegExp[]\n handler: EventHandler\n derived: boolean\n live: boolean\n /** Unnamed only: the stream's last entry when it subscribed; it receives what comes after. */\n since?: string\n /** Unnamed only: its starting point is still being read; what the tail feeds it meanwhile waits in the queue. */\n starting: boolean\n queue: Array<{ entryId: string; event: NymbalEvent }>\n running: Promise<void> | null\n}\n\ninterface Group {\n name: string\n subs: Set<Sub>\n ready: Promise<void>\n draining: Promise<void> | null\n again: boolean\n claimDue: boolean\n}\n\ninterface Wait {\n event: NymbalEvent<unknown>\n entryId: string | null\n /** `g:<group>` for a named group, `s:<subscription>` for an unnamed subscription of this instance. */\n targets: Set<string>\n startedAt: number\n resolve(): void\n reject(error: unknown): void\n timer: NodeJS.Timeout\n}\n\ntype Entry = { id: string; event: NymbalEvent | null }\n\nconst READ_COUNT = 10\nconst TAIL_BLOCK_MS = 5_000\n/** How often a wait on another instance's consumer asks Redis whether its group acknowledged the entry. */\nconst ACK_POLL_MS = 25\n\nfunction compilePattern(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\*]/g, '\\\\$&').replace(/\\\\\\*/g, '.*')\n return new RegExp(`^${escaped}$`)\n}\n\n/** Stream ids are `<ms>-<seq>`; ordered by both parts. */\nfunction compareIds(a: string, b: string): number {\n const [am = 0, as = 0] = a.split('-').map(Number)\n const [bm = 0, bs = 0] = b.split('-').map(Number)\n return am !== bm ? am - bm : as - bs\n}\n\nfunction fieldsOf(raw: unknown): Record<string, string> {\n const out: Record<string, string> = {}\n if (!Array.isArray(raw)) return out\n for (let i = 0; i + 1 < raw.length; i += 2) out[String(raw[i])] = String(raw[i + 1])\n return out\n}\n\n/** `[[id, fields], …]` from XREAD, XREADGROUP or XAUTOCLAIM; an entry trimmed away has no fields. */\nfunction entriesOf(raw: unknown): Entry[] {\n if (!Array.isArray(raw)) return []\n const out: Entry[] = []\n for (const item of raw) {\n if (!Array.isArray(item) || typeof item[0] !== 'string') continue\n const json = fieldsOf(item[1])['event']\n let event: NymbalEvent | null = null\n if (json !== undefined) {\n try {\n event = JSON.parse(json) as NymbalEvent\n } catch {\n event = null\n }\n }\n out.push({ id: item[0], event })\n }\n return out\n}\n\nfunction streamEntries(reply: unknown): Entry[] {\n if (!Array.isArray(reply)) return []\n return reply.flatMap((stream: unknown) => (Array.isArray(stream) ? entriesOf(stream[1]) : []))\n}\n\n/**\n * An event bus on Redis Streams that keeps the in-process delivery contract. Every event is one entry on\n * `<prefix>:events`. A named subscription is a durable consumer group, shared by every instance, so each event is\n * handled once per name; an unnamed subscription reads the stream from where it subscribed, so every subscriber on\n * every instance receives every event. `publish` resolves once every matching group this instance reads, and every\n * matching unnamed subscription it holds, has handled the event.\n */\nexport class RedisStreamsEventBus implements EventBusAdapter {\n readonly #cmd: Redis\n readonly #reader: Redis\n readonly #url: string\n readonly #stream: string\n readonly #consumer = randomUUID()\n readonly #logger: Logger | undefined\n readonly #ackTimeoutMs: number\n readonly #claimIdleMs: number\n readonly #streamMaxLen: number\n readonly #subs = new Map<string, Sub>()\n readonly #groups = new Map<string, Group>()\n readonly #waits = new Map<string, Set<Wait>>()\n #tail: Promise<void> | null = null\n #tailStarted: Promise<void> | null = null\n #tailFrom = '0-0'\n #tick: NodeJS.Timeout | null = null\n #poll: NodeJS.Timeout | null = null\n #closed = false\n\n constructor(cmd: Redis, reader: Redis, options: RedisStreamsEventBusOptions) {\n this.#cmd = cmd\n this.#reader = reader\n this.#url = options.url\n this.#stream = `${options.prefix}:events`\n this.#logger = options.logger\n this.#ackTimeoutMs = options.ackTimeoutMs ?? 15_000\n this.#claimIdleMs = options.claimIdleMs ?? 5_000\n this.#streamMaxLen = options.streamMaxLen ?? 10_000\n }\n\n capabilities(): readonly string[] {\n return ['publish', 'subscribe', 'wildcard', 'replay', 'consumer-groups', 'fan-out']\n }\n\n healthCheck(): Promise<HealthReport> {\n return pingRedis(this.#url)\n }\n\n async publish<T>(event: NymbalEvent<T>): Promise<void> {\n await this.publishBatch([event as NymbalEvent<unknown>])\n }\n\n async publishBatch(events: NymbalEvent<unknown>[]): Promise<void> {\n for (const event of events) validate(event)\n this.#assertOpen()\n if (events.length === 0) return\n // Registered before the entry exists: a reader may finish with it before XADD's reply is read.\n const waits = events.map((event) => this.#wait(event))\n const pipeline = this.#cmd.pipeline()\n for (const event of events) {\n pipeline.xadd(\n this.#stream,\n 'MAXLEN',\n '~',\n this.#streamMaxLen,\n '*',\n 'type',\n event.type,\n 'event',\n JSON.stringify(event),\n )\n }\n let replies: Array<[Error | null, unknown]> | null\n try {\n replies = await pipeline.exec()\n } catch (error) {\n for (const wait of waits) this.#drop(wait)\n throw error\n }\n const failed =\n replies === null\n ? new AdapterError('publish_failed', 'the stream write was aborted')\n : replies.find(([error]) => error !== null)?.[0]\n if (failed !== undefined && failed !== null) {\n // The relay records the failure and publishes the whole set again later.\n for (const wait of waits) this.#drop(wait)\n throw failed\n }\n replies?.forEach(([, id], i) => {\n waits[i]!.entryId = String(id)\n })\n this.#signalGroups()\n this.#schedulePoll()\n await Promise.all(waits.map((w) => w.promise))\n }\n\n async replay(events: NymbalEvent<unknown>[]): Promise<void> {\n for (const event of events) {\n validate(event)\n const derived = [...this.#subs.values()].filter((s) => s.derived && matches(s, event.type))\n await Promise.all(derived.map((s) => this.#run(s, event)))\n }\n }\n\n async subscribe(\n eventPattern: string | string[],\n handler: EventHandler,\n config?: SubscriptionConfig,\n ): Promise<Subscription> {\n this.#assertOpen()\n // The tail starts before an unnamed subscription takes its starting point, so it reads everything after it.\n await this.#startTail()\n const patterns = Array.isArray(eventPattern) ? eventPattern : [eventPattern]\n const sub: Sub = {\n id: randomUUID(),\n patterns,\n regexes: patterns.map(compilePattern),\n handler,\n derived: config?.derived === true,\n live: true,\n starting: false,\n queue: [],\n running: null,\n }\n const name = config?.name\n if (name !== undefined) {\n let group = this.#groups.get(name)\n if (group === undefined) {\n group = {\n name,\n subs: new Set(),\n ready: this.#createGroup(name),\n draining: null,\n again: false,\n claimDue: false,\n }\n this.#groups.set(name, group)\n }\n try {\n await group.ready\n } catch (error) {\n if (group.subs.size === 0) this.#groups.delete(name)\n throw error\n }\n group.subs.add(sub)\n this.#subs.set(sub.id, sub)\n // A durable group may hold entries from before this subscription, or ones a dead consumer left pending.\n group.claimDue = true\n void this.#drainGroup(group)\n } else {\n // Joins before its starting point is read: an entry the tail reads meanwhile is queued, then kept if it is later.\n sub.since = this.#tailFrom\n sub.starting = true\n this.#subs.set(sub.id, sub)\n let last: string\n try {\n last = await this.#lastId()\n } catch (error) {\n sub.live = false\n this.#subs.delete(sub.id)\n throw error\n }\n sub.since = last\n sub.starting = false\n const later = sub.queue.filter((q) => compareIds(q.entryId, last) > 0)\n sub.queue.splice(0, sub.queue.length, ...later)\n if (sub.queue.length > 0 && sub.running === null) sub.running = this.#runQueue(sub)\n }\n this.#startTick()\n const self = this\n return {\n id: sub.id,\n patterns,\n async unsubscribe() {\n await self.#unsubscribe(sub.id)\n },\n }\n }\n\n async unsubscribe(subscription: Subscription): Promise<void> {\n await this.#unsubscribe(subscription.id)\n }\n\n async close(): Promise<void> {\n if (this.#closed) return\n this.#closed = true\n if (this.#tick !== null) clearInterval(this.#tick)\n if (this.#poll !== null) clearTimeout(this.#poll)\n this.#tick = null\n this.#poll = null\n // A wait on this instance's readers ends with them.\n for (const waits of [...this.#waits.values()]) {\n for (const wait of [...waits]) {\n this.#drop(wait)\n wait.resolve()\n }\n }\n this.#reader.disconnect()\n await this.#tail?.catch(() => undefined)\n const groups = [...this.#groups.values()]\n await Promise.all([\n ...groups.map((g) => g.draining?.catch(() => undefined)),\n ...[...this.#subs.values()].map((s) => s.running?.catch(() => undefined)),\n ])\n for (const sub of this.#subs.values()) sub.live = false\n this.#subs.clear()\n await Promise.all(groups.map((g) => this.#leaveGroup(g.name)))\n this.#groups.clear()\n await this.#cmd.quit().catch(() => this.#cmd.disconnect())\n }\n\n // ── Waiting ────────────────────────────────────────────────────────────────\n\n #wait(event: NymbalEvent<unknown>): Wait & { promise: Promise<void> } {\n const targets = new Set<string>()\n for (const group of this.#groups.values()) {\n if ([...group.subs].some((s) => s.live && matches(s, event.type)))\n targets.add(`g:${group.name}`)\n }\n for (const sub of this.#subs.values()) {\n if (sub.since !== undefined && sub.live && !sub.starting && matches(sub, event.type))\n targets.add(`s:${sub.id}`)\n }\n let resolve!: () => void\n let reject!: (error: unknown) => void\n const promise = new Promise<void>((res, rej) => {\n resolve = res\n reject = rej\n })\n const wait = {\n event,\n entryId: null,\n targets,\n startedAt: Date.now(),\n resolve,\n reject,\n promise,\n timer: undefined as unknown as NodeJS.Timeout,\n }\n if (targets.size === 0) {\n resolve()\n return wait\n }\n wait.timer = setTimeout(() => this.#timeout(wait), this.#ackTimeoutMs)\n const forEvent = this.#waits.get(event.id) ?? new Set<Wait>()\n forEvent.add(wait)\n this.#waits.set(event.id, forEvent)\n return wait\n }\n\n #timeout(wait: Wait): void {\n if (!this.#drop(wait)) return\n const pending = [...wait.targets]\n this.#logger?.error(\n {\n eventId: wait.event.id,\n eventType: wait.event.type,\n entryId: wait.entryId,\n unacknowledged: pending,\n },\n 'redis streams publish timed out waiting for its handlers',\n )\n wait.reject(\n new AdapterError(\n 'publish_timeout',\n `publish of ${wait.event.type} (${wait.event.id}) was not handled within ${this.#ackTimeoutMs} ms by ${pending.join(', ')}`,\n { context: { eventId: wait.event.id, unacknowledged: pending } },\n ),\n )\n }\n\n /** Forgets `wait`; false when it was already settled. */\n #drop(wait: Wait): boolean {\n const forEvent = this.#waits.get(wait.event.id)\n if (forEvent === undefined || !forEvent.delete(wait)) return false\n if (forEvent.size === 0) this.#waits.delete(wait.event.id)\n clearTimeout(wait.timer)\n return true\n }\n\n /** `target` has finished with `eventId`: a wait left with nothing to wait on resolves. */\n #settle(eventId: string, target: string): void {\n for (const wait of [...(this.#waits.get(eventId) ?? [])]) this.#remove(wait, target)\n }\n\n #settleEverywhere(target: string): void {\n for (const waits of [...this.#waits.values()]) {\n for (const wait of [...waits]) this.#remove(wait, target)\n }\n }\n\n #remove(wait: Wait, target: string): void {\n if (!wait.targets.delete(target) || wait.targets.size > 0) return\n if (this.#drop(wait)) wait.resolve()\n }\n\n /**\n * A group's entry may be handled by another instance's consumer: asks Redis, for each wait still on a group, whether\n * the group has delivered the entry and holds it pending no more.\n */\n #schedulePoll(): void {\n if (this.#poll !== null || this.#closed || this.#waits.size === 0) return\n this.#poll = setTimeout(() => {\n this.#poll = null\n void this.#pollAcks().finally(() => this.#schedulePoll())\n }, ACK_POLL_MS)\n }\n\n async #pollAcks(): Promise<void> {\n const due: Array<{ wait: Wait; group: string }> = []\n const now = Date.now()\n for (const waits of this.#waits.values()) {\n for (const wait of waits) {\n if (wait.entryId === null || now - wait.startedAt < ACK_POLL_MS) continue\n for (const target of wait.targets) {\n if (target.startsWith('g:')) due.push({ wait, group: target.slice(2) })\n }\n }\n }\n if (due.length === 0) return\n try {\n const pipeline = this.#cmd.pipeline().xinfo('GROUPS', this.#stream)\n for (const { wait, group } of due) {\n pipeline.xpending(this.#stream, group, wait.entryId!, wait.entryId!, 1)\n }\n const replies = (await pipeline.exec()) ?? []\n const delivered = new Map<string, string>()\n const [infoError, info] = replies[0] ?? [null, []]\n if (infoError !== null) return\n for (const raw of (info as unknown[]) ?? []) {\n const fields = fieldsOf(raw)\n if (fields['name'] !== undefined) {\n delivered.set(fields['name'], fields['last-delivered-id'] ?? '0-0')\n }\n }\n due.forEach(({ wait, group }, i) => {\n const [error, pending] = replies[i + 1] ?? [null, null]\n const last = delivered.get(group)\n if (error !== null || last === undefined || !Array.isArray(pending)) return\n if (pending.length === 0 && compareIds(last, wait.entryId!) >= 0) {\n this.#remove(wait, `g:${group}`)\n }\n })\n } catch (err) {\n if (!this.#closed) this.#logger?.warn({ err }, 'redis streams: acknowledgement poll failed')\n }\n }\n\n // ── Reading ────────────────────────────────────────────────────────────────\n\n async #createGroup(name: string): Promise<void> {\n try {\n await this.#cmd.xgroup('CREATE', this.#stream, name, '$', 'MKSTREAM')\n } catch (error) {\n if (!String((error as Error).message).includes('BUSYGROUP')) throw error\n }\n }\n\n async #lastId(): Promise<string> {\n const last = (await this.#cmd.xrevrange(this.#stream, '+', '-', 'COUNT', 1)) as Array<\n [string, string[]]\n >\n return last[0]?.[0] ?? '0-0'\n }\n\n /** One reader per instance tails the stream: it feeds the unnamed subscriptions and wakes the groups. */\n #startTail(): Promise<void> {\n this.#tailStarted ??= this.#lastId().then((last) => {\n this.#tailFrom = last\n this.#tail = this.#runTail()\n })\n return this.#tailStarted\n }\n\n async #runTail(): Promise<void> {\n while (!this.#closed) {\n let reply: unknown\n try {\n reply = await this.#reader.xread(\n 'COUNT',\n 100,\n 'BLOCK',\n TAIL_BLOCK_MS,\n 'STREAMS',\n this.#stream,\n this.#tailFrom,\n )\n } catch (err) {\n if (this.#closed) return\n this.#logger?.warn({ err }, 'redis streams: reading the stream failed; retrying')\n await new Promise((r) => setTimeout(r, 200))\n continue\n }\n const entries = streamEntries(reply)\n for (const entry of entries) {\n this.#tailFrom = entry.id\n if (entry.event !== null) this.#feedUnnamed(entry.id, entry.event)\n }\n if (entries.length > 0) this.#signalGroups()\n }\n }\n\n #feedUnnamed(entryId: string, event: NymbalEvent): void {\n for (const sub of this.#subs.values()) {\n if (sub.since === undefined || !sub.live || compareIds(entryId, sub.since) <= 0) continue\n if (!matches(sub, event.type)) continue\n sub.queue.push({ entryId, event })\n if (sub.running === null && !sub.starting) sub.running = this.#runQueue(sub)\n }\n }\n\n /** An unnamed subscription takes its events one at a time, in stream order. */\n async #runQueue(sub: Sub): Promise<void> {\n try {\n for (let next = sub.queue.shift(); next !== undefined; next = sub.queue.shift()) {\n if (!sub.live || this.#closed) break\n await this.#run(sub, next.event)\n this.#settle(next.event.id, `s:${sub.id}`)\n }\n } finally {\n sub.running = null\n sub.queue.length = 0\n }\n }\n\n #startTick(): void {\n if (this.#tick !== null || this.#closed) return\n const every = Math.min(1_000, Math.max(50, Math.floor(this.#claimIdleMs / 4)))\n this.#tick = setInterval(() => {\n for (const group of this.#groups.values()) {\n group.claimDue = true\n void this.#drainGroup(group)\n }\n }, every)\n this.#tick.unref()\n }\n\n #signalGroups(): void {\n for (const group of this.#groups.values()) void this.#drainGroup(group)\n }\n\n /**\n * A group's consumer on this instance, one pass at a time: entries a dead consumer left pending past `claimIdleMs`\n * first (when due), then new ones, each handled and acknowledged in stream order.\n */\n #drainGroup(group: Group): Promise<void> {\n if (group.draining !== null) {\n group.again = true\n return group.draining\n }\n group.draining = (async () => {\n do {\n group.again = false\n if (group.claimDue) {\n group.claimDue = false\n await this.#claim(group)\n }\n await this.#readNew(group)\n } while (group.again && !this.#closed && group.subs.size > 0)\n })()\n .catch((err: unknown) => {\n if (!this.#closed)\n this.#logger?.warn({ err, group: group.name }, 'redis streams: group read failed')\n })\n .finally(() => {\n group.draining = null\n // Asked for again after the last pass looked: one more.\n if (group.again && !this.#closed && group.subs.size > 0) void this.#drainGroup(group)\n })\n return group.draining\n }\n\n async #readNew(group: Group): Promise<void> {\n for (;;) {\n if (this.#closed || group.subs.size === 0) return\n let reply: unknown\n try {\n reply = await this.#cmd.xreadgroup(\n 'GROUP',\n group.name,\n this.#consumer,\n 'COUNT',\n READ_COUNT,\n 'STREAMS',\n this.#stream,\n '>',\n )\n } catch (error) {\n // The stream or the group was deleted under us: create it again and read on the next pass.\n if (String((error as Error).message).includes('NOGROUP')) {\n await this.#createGroup(group.name)\n return\n }\n throw error\n }\n const entries = streamEntries(reply)\n await this.#handle(group, entries)\n if (entries.length < READ_COUNT) return\n }\n }\n\n async #claim(group: Group): Promise<void> {\n let start = '0-0'\n do {\n if (this.#closed || group.subs.size === 0) return\n const reply = (await this.#cmd.xautoclaim(\n this.#stream,\n group.name,\n this.#consumer,\n this.#claimIdleMs,\n start,\n 'COUNT',\n READ_COUNT,\n )) as [string, unknown, unknown?]\n start = reply[0]\n const entries = entriesOf(reply[1])\n if (entries.length > 0) {\n this.#logger?.info(\n { group: group.name, entries: entries.map((e) => e.id) },\n 'redis streams: took over entries a consumer left pending',\n )\n }\n await this.#handle(group, entries)\n } while (start !== '0-0')\n }\n\n /**\n * Handles each entry for the group's matching subscriptions, then acknowledges it; non-matching ones in bulk. Once\n * the group has no live subscription here, the rest stay pending, for a live consumer of the group to take over.\n */\n async #handle(group: Group, entries: Entry[]): Promise<void> {\n const skipped: string[] = []\n for (const entry of entries) {\n if (![...group.subs].some((s) => s.live)) break\n const event = entry.event\n const subs =\n event === null ? [] : [...group.subs].filter((s) => s.live && matches(s, event.type))\n if (event === null || subs.length === 0) {\n if (event === null)\n this.#logger?.error({ entryId: entry.id }, 'redis streams: unreadable entry')\n skipped.push(entry.id)\n continue\n }\n await Promise.all(subs.map((s) => this.#run(s, event)))\n await this.#cmd.xack(this.#stream, group.name, entry.id)\n this.#settle(event.id, `g:${group.name}`)\n }\n if (skipped.length > 0) await this.#cmd.xack(this.#stream, group.name, ...skipped)\n }\n\n /** Runs one handler; an error is logged and acknowledged like a success, as the in-process bus does. */\n async #run(sub: Sub, event: NymbalEvent): Promise<void> {\n try {\n await sub.handler(event)\n } catch (err) {\n this.#logger?.error(\n { err, subscriptionId: sub.id, eventType: event.type, eventId: event.id },\n 'Event handler failed',\n )\n }\n }\n\n // ── Leaving ────────────────────────────────────────────────────────────────\n\n async #unsubscribe(id: string): Promise<void> {\n const sub = this.#subs.get(id)\n if (sub === undefined) return\n sub.live = false\n this.#subs.delete(id)\n if (sub.since !== undefined) {\n sub.queue.length = 0\n this.#settleEverywhere(`s:${id}`)\n return\n }\n for (const group of this.#groups.values()) {\n if (!group.subs.delete(sub) || group.subs.size > 0) continue\n // No live reader of the group on this instance: no publish here waits on it any more.\n this.#groups.delete(group.name)\n this.#settleEverywhere(`g:${group.name}`)\n await group.draining?.catch(() => undefined)\n if (!this.#closed) await this.#leaveGroup(group.name)\n }\n }\n\n /** Removes this instance's consumer from the group when it holds nothing pending, which would be lost with it. */\n async #leaveGroup(name: string): Promise<void> {\n if (this.#groups.has(name) && !this.#closed) return\n try {\n const summary = (await this.#cmd.xpending(\n this.#stream,\n name,\n '-',\n '+',\n 1,\n this.#consumer,\n )) as unknown[]\n if (summary.length === 0) {\n await this.#cmd.xgroup('DELCONSUMER', this.#stream, name, this.#consumer)\n }\n } catch (err) {\n this.#logger?.debug({ err, group: name }, 'redis streams: consumer left in place')\n }\n }\n\n #assertOpen(): void {\n if (this.#closed) {\n throw new AdapterError('closed', 'the redis streams event bus is closed')\n }\n }\n}\n\nfunction matches(sub: Sub, type: string): boolean {\n return sub.regexes.some((re) => re.test(type))\n}\n\nfunction validate(event: NymbalEvent<unknown>): void {\n if (!event.id || !event.type || !event.timestamp || !event.source) {\n throw new ValidationError('Event is missing required envelope fields', { context: { event } })\n }\n if (!event.metadata || !event.metadata.storeId || !event.metadata.environment) {\n throw new ValidationError('Event metadata is missing required fields', { context: { event } })\n }\n}\n\n/**\n * PING on a client of its own that never retries and queues nothing, disconnected afterwards: the store's or bus's own\n * clients retry a closed port for as long as the process lives, so they cannot answer within the bound.\n */\nfunction pingRedis(url: string, timeoutMs = HEALTH_CHECK_TIMEOUT_MS): Promise<HealthReport> {\n const client = new Redis(url, {\n lazyConnect: true,\n retryStrategy: () => null,\n maxRetriesPerRequest: 0,\n enableOfflineQueue: false,\n connectTimeout: timeoutMs,\n })\n // connect() rejects with the same error, and the report carries it; without a listener ioredis would throw it.\n client.on('error', () => undefined)\n return boundedHealthCheck(\n async () => {\n await client.connect()\n await client.ping()\n return { status: 'healthy' }\n },\n { timeoutMs, redact: connectionSecrets(url) },\n ).finally(() => client.disconnect())\n}\n\nasync function connect(\n url: string,\n logger: Logger | undefined,\n role: 'commands' | 'reader',\n): Promise<Redis> {\n // The reader blocks on the stream and retries for as long as it takes. A command waits out at most two reconnect\n // attempts (each a connect of at most 2 s, a second apart) and then rejects, so a publish on a dead server fails\n // within the bound; the client keeps reconnecting, so publishes succeed again once the server is back.\n const client = new Redis(\n url,\n role === 'reader'\n ? { lazyConnect: true, maxRetriesPerRequest: null }\n : {\n lazyConnect: true,\n maxRetriesPerRequest: 1,\n retryStrategy: (times: number) => Math.min(times * 200, 1000),\n connectTimeout: 2000,\n },\n )\n client.on('error', (err: Error) => logger?.warn({ err, role }, 'redis connection error'))\n try {\n await client.connect()\n } catch (error) {\n client.disconnect()\n throw error\n }\n return client\n}\n\n/** Connects the bus's two clients: one for commands, one for the blocking read of the stream. */\nexport async function createRedisStreamsEventBus(\n options: RedisStreamsEventBusOptions,\n): Promise<RedisStreamsEventBus> {\n const cmd = await connect(options.url, options.logger, 'commands')\n try {\n const reader = await connect(options.url, options.logger, 'reader')\n return new RedisStreamsEventBus(cmd, reader, options)\n } catch (error) {\n cmd.disconnect()\n throw error\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nymbalhq/event-bus-redis-streams",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "The Redis Streams event bus for Nymbal: `infrastructure.eventBus: 'redis-streams'`",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/nymbalhq/nymbal.git",
|
|
9
|
+
"directory": "packages/event-bus-redis-streams"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@nymbalhq/types": "1.2.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"ioredis": "^5.4.0 || ^6.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@nymbalhq/contract-tests": "1.2.0",
|
|
37
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
38
|
+
"fast-check": "^4.10.2",
|
|
39
|
+
"ioredis": "^6.0.0",
|
|
40
|
+
"tsup": "^9.0.0",
|
|
41
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
42
|
+
"vitest": "^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"dev": "tsup --watch",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit --rootDir ../.."
|
|
48
|
+
}
|
|
6
49
|
}
|
package/README.md
DELETED