@mastra/valkey-streams 0.5.1-alpha.1 → 0.5.2-alpha.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/client.d.ts +18 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +191 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -34
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/client.d.ts
CHANGED
|
@@ -53,17 +53,25 @@ export declare class ValkeyStreamsClient {
|
|
|
53
53
|
COUNT?: number;
|
|
54
54
|
BLOCK?: number;
|
|
55
55
|
}): Promise<StreamReply[] | null>;
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* XPENDING <key> <group> [IDLE ms] <start> + <count>. `afterId` makes the
|
|
58
|
+
* range exclusive so callers can page past entries they've already seen.
|
|
59
|
+
*/
|
|
60
|
+
xPendingRange(key: string, group: string, count: number, options?: {
|
|
61
|
+
IDLE?: number;
|
|
62
|
+
afterId?: string;
|
|
58
63
|
}): Promise<{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
id: string;
|
|
65
|
+
consumer: string;
|
|
66
|
+
millisecondsSinceLastDelivery: number;
|
|
67
|
+
deliveriesCounter: number;
|
|
68
|
+
}[]>;
|
|
69
|
+
xClaim(key: string, group: string, consumer: string, minIdle: number, ids: string[]): Promise<{
|
|
70
|
+
id: string;
|
|
71
|
+
message: {
|
|
72
|
+
[k: string]: string;
|
|
73
|
+
};
|
|
74
|
+
}[]>;
|
|
67
75
|
xAck(key: string, group: string, id: string): Promise<number>;
|
|
68
76
|
del(key: string): Promise<number>;
|
|
69
77
|
set(key: string, value: string, options?: {
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -94,18 +94,32 @@ var ValkeyStreamsClient = class {
|
|
|
94
94
|
}))
|
|
95
95
|
})) ?? null;
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
/**
|
|
98
|
+
* XPENDING <key> <group> [IDLE ms] <start> + <count>. `afterId` makes the
|
|
99
|
+
* range exclusive so callers can page past entries they've already seen.
|
|
100
|
+
*/
|
|
101
|
+
async xPendingRange(key, group, count, options = {}) {
|
|
102
|
+
return (await (await this.getClient()).xpendingWithOptions(key, group, {
|
|
103
|
+
start: options.afterId ? {
|
|
104
|
+
value: options.afterId,
|
|
105
|
+
isInclusive: false
|
|
106
|
+
} : _valkey_valkey_glide.InfBoundary.NegativeInfinity,
|
|
107
|
+
end: _valkey_valkey_glide.InfBoundary.PositiveInfinity,
|
|
108
|
+
count,
|
|
109
|
+
minIdleTime: options.IDLE
|
|
110
|
+
})).map(([id, consumer, millisecondsSinceLastDelivery, deliveriesCounter]) => ({
|
|
111
|
+
id: text(id),
|
|
112
|
+
consumer: text(consumer),
|
|
113
|
+
millisecondsSinceLastDelivery,
|
|
114
|
+
deliveriesCounter
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
async xClaim(key, group, consumer, minIdle, ids) {
|
|
118
|
+
const result = await (await this.getClient()).xclaim(key, group, consumer, minIdle, ids, { decoder: _valkey_valkey_glide.Decoder.String });
|
|
119
|
+
return Object.entries(result ?? {}).map(([id, fields]) => ({
|
|
120
|
+
id,
|
|
121
|
+
message: Object.fromEntries(fields.map(([field, value]) => [text(field), text(value)]))
|
|
122
|
+
}));
|
|
109
123
|
}
|
|
110
124
|
async xAck(key, group, id) {
|
|
111
125
|
return (await this.getClient()).xack(key, group, [id]);
|
|
@@ -160,6 +174,32 @@ var ValkeyStreamsClient = class {
|
|
|
160
174
|
const createClient = (options) => new ValkeyStreamsClient(options);
|
|
161
175
|
//#endregion
|
|
162
176
|
//#region src/index.ts
|
|
177
|
+
/** Page size for the reclaim loop's XPENDING scan and the max entries claimed per tick. */
|
|
178
|
+
const RECLAIM_PAGE_SIZE = 100;
|
|
179
|
+
/**
|
|
180
|
+
* Atomically nack a pending entry only if it is still owned by the given
|
|
181
|
+
* consumer. Returns 1 if this consumer owned it and it was settled, 0 if the
|
|
182
|
+
* entry was not pending for this consumer (acked, or claimed by a sibling).
|
|
183
|
+
*
|
|
184
|
+
* KEYS[1] stream key
|
|
185
|
+
* ARGV[1] group, ARGV[2] consumer, ARGV[3] stream entry id,
|
|
186
|
+
* ARGV[4] republish payload ('' = drop without republish),
|
|
187
|
+
* ARGV[5] stream idle TTL in ms (0 = none)
|
|
188
|
+
*/
|
|
189
|
+
const NACK_IF_OWNED_SCRIPT = `
|
|
190
|
+
local pending = redis.call("XPENDING", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)
|
|
191
|
+
if #pending == 0 or pending[1][2] ~= ARGV[2] then
|
|
192
|
+
return 0
|
|
193
|
+
end
|
|
194
|
+
if ARGV[4] ~= "" then
|
|
195
|
+
redis.call("XADD", KEYS[1], "*", "event", ARGV[4])
|
|
196
|
+
if tonumber(ARGV[5]) > 0 then
|
|
197
|
+
redis.call("PEXPIRE", KEYS[1], ARGV[5])
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
redis.call("XACK", KEYS[1], ARGV[1], ARGV[3])
|
|
201
|
+
return 1
|
|
202
|
+
`;
|
|
163
203
|
/**
|
|
164
204
|
* Flatten an error into searchable text. node-redis MULTI failures throw a
|
|
165
205
|
* `MultiErrorReply` whose own message is just "N commands failed…" — the real
|
|
@@ -190,6 +230,7 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
190
230
|
#reclaimIntervalMs;
|
|
191
231
|
#reclaimIdleMs;
|
|
192
232
|
#maxDeliveryAttempts;
|
|
233
|
+
#inFlightTimeoutMs;
|
|
193
234
|
#logger;
|
|
194
235
|
#subscriptions = /* @__PURE__ */ new Map();
|
|
195
236
|
#cbIds = /* @__PURE__ */ new WeakMap();
|
|
@@ -220,6 +261,9 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
220
261
|
this.#maxDeliveryAttempts = Infinity;
|
|
221
262
|
} else if (cap < 0 || Number.isNaN(cap)) throw new Error(`valkey-streams: maxDeliveryAttempts must be >= 1 or Infinity, got ${cap}`);
|
|
222
263
|
else this.#maxDeliveryAttempts = cap;
|
|
264
|
+
const inFlightTimeout = options.inFlightTimeoutMs ?? 0;
|
|
265
|
+
if (!Number.isFinite(inFlightTimeout) || inFlightTimeout < 0) throw new Error(`valkey-streams: inFlightTimeoutMs must be a non-negative number (milliseconds), got ${inFlightTimeout}`);
|
|
266
|
+
this.#inFlightTimeoutMs = inFlightTimeout;
|
|
223
267
|
}
|
|
224
268
|
/**
|
|
225
269
|
* Attach the `'error'` listener node-redis requires on every client. Without
|
|
@@ -275,6 +319,15 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
275
319
|
this.#deliverLocal(topic, localEvent);
|
|
276
320
|
return;
|
|
277
321
|
}
|
|
322
|
+
const promise = this.#publishRemote(topic, event);
|
|
323
|
+
this.#pendingPublishes.add(promise);
|
|
324
|
+
try {
|
|
325
|
+
await promise;
|
|
326
|
+
} finally {
|
|
327
|
+
this.#pendingPublishes.delete(promise);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async #publishRemote(topic, event) {
|
|
278
331
|
await this.#ensureWriterConnected();
|
|
279
332
|
const id = (0, crypto.randomUUID)();
|
|
280
333
|
const createdAt = /* @__PURE__ */ new Date();
|
|
@@ -291,18 +344,12 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
291
344
|
threshold: this.#maxStreamLength
|
|
292
345
|
};
|
|
293
346
|
const streamKey = this.#streamKey(topic);
|
|
294
|
-
|
|
347
|
+
await (this.#streamIdleTtlMs > 0 ? this.#writeClient.multi().xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions).pExpire(streamKey, this.#streamIdleTtlMs).exec().then((replies) => {
|
|
295
348
|
if (Number(replies[1]) !== 1) this.#logger?.warn?.("valkey-streams: PEXPIRE inside publish MULTI did not apply", {
|
|
296
349
|
streamKey,
|
|
297
350
|
reply: String(replies[1])
|
|
298
351
|
});
|
|
299
|
-
}) : this.#writeClient.xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions);
|
|
300
|
-
this.#pendingPublishes.add(promise);
|
|
301
|
-
try {
|
|
302
|
-
await promise;
|
|
303
|
-
} finally {
|
|
304
|
-
this.#pendingPublishes.delete(promise);
|
|
305
|
-
}
|
|
352
|
+
}) : this.#writeClient.xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions));
|
|
306
353
|
}
|
|
307
354
|
async subscribe(topic, cb, options) {
|
|
308
355
|
if (this.#closed) throw new Error("ValkeyStreamsPubSub: cannot subscribe on closed client");
|
|
@@ -345,41 +392,91 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
345
392
|
readClient,
|
|
346
393
|
stopped: false,
|
|
347
394
|
loop: void 0,
|
|
348
|
-
reclaimTimer: void 0
|
|
395
|
+
reclaimTimer: void 0,
|
|
396
|
+
inFlight: /* @__PURE__ */ new Map()
|
|
349
397
|
};
|
|
350
398
|
this.#subscriptions.set(key, sub);
|
|
351
399
|
sub.loop = this.#runReadLoop(sub);
|
|
352
400
|
this.#startReclaimLoop(sub);
|
|
353
401
|
}
|
|
354
402
|
/**
|
|
355
|
-
* Periodically
|
|
356
|
-
* messages a crashed/stuck consumer read but never acked get redelivered
|
|
357
|
-
* a live sibling.
|
|
358
|
-
*
|
|
403
|
+
* Periodically reclaim idle pending entries in this subscription's group so
|
|
404
|
+
* that messages a crashed/stuck consumer read but never acked get redelivered
|
|
405
|
+
* to a live sibling. Fan-out groups are private to one consumer, so there is
|
|
406
|
+
* no sibling to claim from and the XPENDING/XCLAIM scan is skipped for them;
|
|
407
|
+
* the loop still runs so `inFlightTimeoutMs` can recover a hung handler,
|
|
408
|
+
* which is the only recovery path a single-consumer group has.
|
|
359
409
|
*/
|
|
360
410
|
#startReclaimLoop(sub) {
|
|
361
411
|
if (this.#reclaimIntervalMs <= 0) return;
|
|
362
|
-
if (!sub.isGrouped) return;
|
|
412
|
+
if (!sub.isGrouped && this.#inFlightTimeoutMs <= 0) return;
|
|
363
413
|
const tick = async () => {
|
|
364
414
|
if (sub.stopped || this.#closed) return;
|
|
365
415
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
416
|
+
await this.#expireInFlight(sub);
|
|
417
|
+
if (!sub.isGrouped) {
|
|
418
|
+
sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const ids = [];
|
|
422
|
+
let afterId;
|
|
423
|
+
for (;;) {
|
|
424
|
+
const page = await this.#writeClient.xPendingRange(sub.streamKey, sub.group, RECLAIM_PAGE_SIZE, {
|
|
425
|
+
IDLE: this.#reclaimIdleMs,
|
|
426
|
+
afterId
|
|
427
|
+
});
|
|
428
|
+
for (const p of page) {
|
|
429
|
+
if (sub.inFlight.has(p.id)) continue;
|
|
430
|
+
ids.push(p.id);
|
|
431
|
+
if (ids.length >= RECLAIM_PAGE_SIZE) break;
|
|
432
|
+
}
|
|
433
|
+
if (page.length < RECLAIM_PAGE_SIZE || ids.length >= RECLAIM_PAGE_SIZE) break;
|
|
434
|
+
afterId = page[page.length - 1].id;
|
|
435
|
+
}
|
|
436
|
+
if (ids.length === 0 || sub.stopped || this.#closed) {
|
|
368
437
|
if (sub.stopped || this.#closed) return;
|
|
369
|
-
|
|
370
|
-
|
|
438
|
+
sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);
|
|
439
|
+
return;
|
|
371
440
|
}
|
|
441
|
+
const claimed = await this.#writeClient.xClaim(sub.streamKey, sub.group, sub.consumer, this.#reclaimIdleMs, ids);
|
|
442
|
+
for (const entry of claimed) await this.#deliverMessage(sub, entry.id, entry.message);
|
|
372
443
|
} catch (err) {
|
|
373
|
-
this.#logger?.debug?.("valkey-streams:
|
|
444
|
+
this.#logger?.debug?.("valkey-streams: reclaim failed", {
|
|
374
445
|
topic: sub.topic,
|
|
375
446
|
group: sub.group,
|
|
376
447
|
err: err instanceof Error ? err.message : err
|
|
377
448
|
});
|
|
378
449
|
}
|
|
379
450
|
if (sub.stopped || this.#closed) return;
|
|
380
|
-
sub.reclaimTimer = setTimeout(
|
|
451
|
+
sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);
|
|
452
|
+
};
|
|
453
|
+
const runTick = () => {
|
|
454
|
+
sub.reclaimLoop = tick().finally(() => {
|
|
455
|
+
sub.reclaimLoop = void 0;
|
|
456
|
+
});
|
|
381
457
|
};
|
|
382
|
-
sub.reclaimTimer = setTimeout(
|
|
458
|
+
sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Give up on local in-flight entries older than `inFlightTimeoutMs` by
|
|
462
|
+
* nacking them on the handler's behalf. This is the only way a
|
|
463
|
+
* single-consumer group recovers a hung handler: the reclaim loop never
|
|
464
|
+
* claims its own in-flight entries (see the tick above), so without a
|
|
465
|
+
* timeout the entry stays pending until the process restarts.
|
|
466
|
+
*/
|
|
467
|
+
async #expireInFlight(sub) {
|
|
468
|
+
if (this.#inFlightTimeoutMs <= 0) return;
|
|
469
|
+
const cutoff = Date.now() - this.#inFlightTimeoutMs;
|
|
470
|
+
for (const [streamId, entry] of sub.inFlight) {
|
|
471
|
+
if (entry.since > cutoff) continue;
|
|
472
|
+
this.#logger?.warn?.("valkey-streams: handler exceeded inFlightTimeoutMs; nacking on its behalf", {
|
|
473
|
+
topic: sub.topic,
|
|
474
|
+
group: sub.group,
|
|
475
|
+
streamId,
|
|
476
|
+
inFlightMs: Date.now() - entry.since
|
|
477
|
+
});
|
|
478
|
+
await entry.expire();
|
|
479
|
+
}
|
|
383
480
|
}
|
|
384
481
|
async unsubscribe(topic, cb) {
|
|
385
482
|
const localBucket = this.#localCallbacks.get(topic);
|
|
@@ -412,6 +509,7 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
412
509
|
err: err instanceof Error ? err.message : err
|
|
413
510
|
});
|
|
414
511
|
}
|
|
512
|
+
await sub.reclaimLoop;
|
|
415
513
|
if (!sub.isGrouped) try {
|
|
416
514
|
await this.#writeClient.xGroupDestroy(sub.streamKey, sub.group);
|
|
417
515
|
} catch (err) {
|
|
@@ -579,6 +677,7 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
579
677
|
const subs = [...this.#subscriptions.values()];
|
|
580
678
|
await Promise.all(subs.map((sub) => this.unsubscribe(sub.topic, sub.cb)));
|
|
581
679
|
this.#localCallbacks.clear();
|
|
680
|
+
await this.flush();
|
|
582
681
|
if (this.#writeClient.isOpen) try {
|
|
583
682
|
await this.#writeClient.quit();
|
|
584
683
|
} catch (err) {
|
|
@@ -663,6 +762,8 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
663
762
|
topic: sub.topic,
|
|
664
763
|
err: err instanceof Error ? err.message : err
|
|
665
764
|
});
|
|
765
|
+
} finally {
|
|
766
|
+
sub.inFlight.delete(streamId);
|
|
666
767
|
}
|
|
667
768
|
};
|
|
668
769
|
const nack = async () => {
|
|
@@ -684,6 +785,8 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
684
785
|
topic: sub.topic,
|
|
685
786
|
err: err instanceof Error ? err.message : err
|
|
686
787
|
});
|
|
788
|
+
} finally {
|
|
789
|
+
sub.inFlight.delete(streamId);
|
|
687
790
|
}
|
|
688
791
|
return;
|
|
689
792
|
}
|
|
@@ -701,6 +804,7 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
701
804
|
err: err instanceof Error ? err.message : err
|
|
702
805
|
});
|
|
703
806
|
settled = false;
|
|
807
|
+
sub.inFlight.delete(streamId);
|
|
704
808
|
return;
|
|
705
809
|
}
|
|
706
810
|
try {
|
|
@@ -710,8 +814,62 @@ var ValkeyStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
|
710
814
|
topic: sub.topic,
|
|
711
815
|
err: err instanceof Error ? err.message : err
|
|
712
816
|
});
|
|
817
|
+
} finally {
|
|
818
|
+
sub.inFlight.delete(streamId);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
const expire = async () => {
|
|
822
|
+
if (settled) return;
|
|
823
|
+
settled = true;
|
|
824
|
+
const attempt = event.deliveryAttempt ?? 1;
|
|
825
|
+
const drop = attempt >= this.#maxDeliveryAttempts;
|
|
826
|
+
const payload = drop ? "" : JSON.stringify({
|
|
827
|
+
...event,
|
|
828
|
+
deliveryAttempt: attempt + 1
|
|
829
|
+
});
|
|
830
|
+
let owned;
|
|
831
|
+
try {
|
|
832
|
+
owned = await this.#writeClient.eval(NACK_IF_OWNED_SCRIPT, {
|
|
833
|
+
keys: [sub.streamKey],
|
|
834
|
+
arguments: [
|
|
835
|
+
sub.group,
|
|
836
|
+
sub.consumer,
|
|
837
|
+
streamId,
|
|
838
|
+
payload,
|
|
839
|
+
String(this.#streamIdleTtlMs)
|
|
840
|
+
]
|
|
841
|
+
});
|
|
842
|
+
} catch (err) {
|
|
843
|
+
this.#logger?.warn?.("valkey-streams: timeout nack failed; leaving original pending for reclaim", {
|
|
844
|
+
topic: sub.topic,
|
|
845
|
+
eventId: event.id,
|
|
846
|
+
err: err instanceof Error ? err.message : err
|
|
847
|
+
});
|
|
848
|
+
settled = false;
|
|
849
|
+
sub.inFlight.delete(streamId);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
sub.inFlight.delete(streamId);
|
|
853
|
+
if (owned !== 1) {
|
|
854
|
+
this.#logger?.warn?.("valkey-streams: handler exceeded inFlightTimeoutMs but entry is no longer owned; disowning", {
|
|
855
|
+
topic: sub.topic,
|
|
856
|
+
group: sub.group,
|
|
857
|
+
streamId
|
|
858
|
+
});
|
|
859
|
+
return;
|
|
713
860
|
}
|
|
861
|
+
if (drop) this.#logger?.warn?.("valkey-streams: dropping event after max delivery attempts", {
|
|
862
|
+
topic: sub.topic,
|
|
863
|
+
eventType: event.type,
|
|
864
|
+
eventId: event.id,
|
|
865
|
+
attempt,
|
|
866
|
+
max: this.#maxDeliveryAttempts
|
|
867
|
+
});
|
|
714
868
|
};
|
|
869
|
+
sub.inFlight.set(streamId, {
|
|
870
|
+
since: Date.now(),
|
|
871
|
+
expire
|
|
872
|
+
});
|
|
715
873
|
try {
|
|
716
874
|
const result = sub.cb(event, ack, nack);
|
|
717
875
|
if (result && typeof result.catch === "function") result.catch(async () => {
|