@burdenoff/be-sdk 2026.513.4 → 2026.513.6
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/EventQueueFactory-TWDDFMQY.js +8 -0
- package/dist/{EventQueueFactory-FNE7H66N.js.map → EventQueueFactory-TWDDFMQY.js.map} +1 -1
- package/dist/{chunk-WPGSKLAG.js → chunk-3JBKPVWY.js} +24 -5
- package/dist/chunk-3JBKPVWY.js.map +1 -0
- package/dist/{chunk-KSO6QQVD.js → chunk-EMSON3UD.js} +952 -10
- package/dist/chunk-EMSON3UD.js.map +1 -0
- package/dist/{chunk-LRL6GKXI.js → chunk-L72RC2EI.js} +3 -3
- package/dist/{chunk-LRL6GKXI.js.map → chunk-L72RC2EI.js.map} +1 -1
- package/dist/{chunk-NUZZRJNN.js → chunk-MI72VKNQ.js} +171 -5
- package/dist/chunk-MI72VKNQ.js.map +1 -0
- package/dist/{chunk-SXIL2AJF.js → chunk-OMKFTTAN.js} +5 -5
- package/dist/{chunk-SXIL2AJF.js.map → chunk-OMKFTTAN.js.map} +1 -1
- package/dist/{chunk-S52PJKSW.js → chunk-Q6HJARHV.js} +3 -3
- package/dist/{chunk-S52PJKSW.js.map → chunk-Q6HJARHV.js.map} +1 -1
- package/dist/{chunk-COSUYUCB.js → chunk-QKJR4L7B.js} +149 -3
- package/dist/chunk-QKJR4L7B.js.map +1 -0
- package/dist/{chunk-VOFKF27Q.js → chunk-UVYDP6P6.js} +3 -2
- package/dist/chunk-UVYDP6P6.js.map +1 -0
- package/dist/{chunk-DR5AQJT6.js → chunk-Y4TPKRZB.js} +3 -3
- package/dist/{chunk-DR5AQJT6.js.map → chunk-Y4TPKRZB.js.map} +1 -1
- package/dist/config-QVYSAUMT.js +4 -0
- package/dist/{config-7U7J3QXQ.js.map → config-QVYSAUMT.js.map} +1 -1
- package/dist/esm-M2M7HGV4.js +1356 -0
- package/dist/esm-M2M7HGV4.js.map +1 -0
- package/dist/event-queue/index.d.ts +1 -1
- package/dist/event-queue/index.js +5 -5
- package/dist/graph/index.js +6 -6
- package/dist/graphql/index.d.ts +1 -1
- package/dist/graphql/index.js +2 -2
- package/dist/{index-HkD7NBED.d.ts → index-CDFQRL-h.d.ts} +24 -2
- package/dist/{index-BF2uh_h9.d.ts → index-eUvoVdr7.d.ts} +158 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +12 -12
- package/dist/init/index.js +4 -4
- package/dist/prisma/index.js +5 -5
- package/dist/server/index.js +20 -0
- package/dist/server/index.js.map +1 -1
- package/dist/telemetry/index.js.map +1 -1
- package/package.json +14 -3
- package/dist/EventQueueFactory-FNE7H66N.js +0 -8
- package/dist/chunk-COSUYUCB.js.map +0 -1
- package/dist/chunk-KSO6QQVD.js.map +0 -1
- package/dist/chunk-NUZZRJNN.js.map +0 -1
- package/dist/chunk-VOFKF27Q.js.map +0 -1
- package/dist/chunk-WPGSKLAG.js.map +0 -1
- package/dist/config-7U7J3QXQ.js +0 -4
- package/dist/esm-K6H7EIWK.js +0 -1450
- package/dist/esm-K6H7EIWK.js.map +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { QueueType } from './chunk-
|
|
2
|
-
import { NatsConnectionManager } from './chunk-
|
|
1
|
+
import { QueueType } from './chunk-UVYDP6P6.js';
|
|
2
|
+
import { NatsConnectionManager, RedisStreamsConnection } from './chunk-QKJR4L7B.js';
|
|
3
3
|
import { DEFAULT_LAYER_RESOURCES } from './chunk-NGMEI42S.js';
|
|
4
4
|
import { logger } from './chunk-CBZNP6RF.js';
|
|
5
5
|
import { __name } from './chunk-XE4OXN2W.js';
|
|
6
6
|
import { RetentionPolicy, StorageType, AckPolicy, DeliverPolicy, headers } from 'nats';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { hostname } from 'os';
|
|
7
9
|
|
|
8
10
|
// src/event-queue/implementations/nats/trace-context.ts
|
|
9
11
|
var otelApiPromise = null;
|
|
@@ -13,7 +15,7 @@ async function loadOtelApi() {
|
|
|
13
15
|
}
|
|
14
16
|
otelApiPromise = (async () => {
|
|
15
17
|
try {
|
|
16
|
-
const mod = await import('./esm-
|
|
18
|
+
const mod = await import('./esm-M2M7HGV4.js');
|
|
17
19
|
return mod;
|
|
18
20
|
} catch {
|
|
19
21
|
logger.debug("[trace-context] @opentelemetry/api not available; NATS traces disabled");
|
|
@@ -921,7 +923,891 @@ var NatsEventPublisher = class {
|
|
|
921
923
|
}
|
|
922
924
|
};
|
|
923
925
|
|
|
926
|
+
// src/event-queue/implementations/redis-streams/keyspace.ts
|
|
927
|
+
var DEFAULT_PREFIX = "boff:events";
|
|
928
|
+
var REGISTRY_PREFIX = "boff:registry:subjects";
|
|
929
|
+
var DELAYQ_PREFIX = "boff:delayq";
|
|
930
|
+
var DELAYQ_LOCK_PREFIX = "boff:lock:delayq";
|
|
931
|
+
var DLQ_PREFIX = "boff:dlq";
|
|
932
|
+
var DEDUP_PREFIX = "boff:dedup";
|
|
933
|
+
var REPLY_PREFIX = "boff:replies";
|
|
934
|
+
function subjectToKey(subject) {
|
|
935
|
+
return subject.replace(/\./g, ":");
|
|
936
|
+
}
|
|
937
|
+
__name(subjectToKey, "subjectToKey");
|
|
938
|
+
function streamKey(subject, opts = {}) {
|
|
939
|
+
const prefix = opts.prefix ?? DEFAULT_PREFIX;
|
|
940
|
+
return `${prefix}:${subjectToKey(subject)}`;
|
|
941
|
+
}
|
|
942
|
+
__name(streamKey, "streamKey");
|
|
943
|
+
function registryKey(boundary) {
|
|
944
|
+
return `${REGISTRY_PREFIX}:${boundary}`;
|
|
945
|
+
}
|
|
946
|
+
__name(registryKey, "registryKey");
|
|
947
|
+
function boundaryOf(subject) {
|
|
948
|
+
const first = subject.split(".")[0];
|
|
949
|
+
return first ?? subject;
|
|
950
|
+
}
|
|
951
|
+
__name(boundaryOf, "boundaryOf");
|
|
952
|
+
function delayQueueKey(stream) {
|
|
953
|
+
return `${DELAYQ_PREFIX}:${stream}`;
|
|
954
|
+
}
|
|
955
|
+
__name(delayQueueKey, "delayQueueKey");
|
|
956
|
+
function delayLockKey(stream) {
|
|
957
|
+
return `${DELAYQ_LOCK_PREFIX}:${stream}`;
|
|
958
|
+
}
|
|
959
|
+
__name(delayLockKey, "delayLockKey");
|
|
960
|
+
function dlqKey(stream) {
|
|
961
|
+
return `${DLQ_PREFIX}:${stream}`;
|
|
962
|
+
}
|
|
963
|
+
__name(dlqKey, "dlqKey");
|
|
964
|
+
function dedupKey(stream, msgId) {
|
|
965
|
+
return `${DEDUP_PREFIX}:${stream}:${msgId}`;
|
|
966
|
+
}
|
|
967
|
+
__name(dedupKey, "dedupKey");
|
|
968
|
+
function replyKey(correlationId) {
|
|
969
|
+
return `${REPLY_PREFIX}:${correlationId}`;
|
|
970
|
+
}
|
|
971
|
+
__name(replyKey, "replyKey");
|
|
972
|
+
function matchSubject(pattern, subject) {
|
|
973
|
+
if (pattern === subject) return true;
|
|
974
|
+
const pParts = pattern.split(".");
|
|
975
|
+
const sParts = subject.split(".");
|
|
976
|
+
const last = pParts[pParts.length - 1];
|
|
977
|
+
if (last === ">") {
|
|
978
|
+
if (sParts.length < pParts.length) return false;
|
|
979
|
+
for (let i = 0; i < pParts.length - 1; i++) {
|
|
980
|
+
if (pParts[i] !== sParts[i]) return false;
|
|
981
|
+
}
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
if (pParts.length !== sParts.length) return false;
|
|
985
|
+
for (let i = 0; i < pParts.length; i++) {
|
|
986
|
+
if (pParts[i] !== "*" && pParts[i] !== sParts[i]) return false;
|
|
987
|
+
}
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
__name(matchSubject, "matchSubject");
|
|
991
|
+
function isWildcard(pattern) {
|
|
992
|
+
return pattern.includes("*") || pattern.includes(">");
|
|
993
|
+
}
|
|
994
|
+
__name(isWildcard, "isWildcard");
|
|
995
|
+
|
|
996
|
+
// src/event-queue/implementations/redis-streams/trace-context.ts
|
|
997
|
+
var otelApiPromise2 = null;
|
|
998
|
+
async function loadOtelApi2() {
|
|
999
|
+
if (otelApiPromise2) return otelApiPromise2;
|
|
1000
|
+
otelApiPromise2 = (async () => {
|
|
1001
|
+
try {
|
|
1002
|
+
const mod = await import('./esm-M2M7HGV4.js');
|
|
1003
|
+
return mod;
|
|
1004
|
+
} catch {
|
|
1005
|
+
logger.debug("[redis-streams/trace-context] @opentelemetry/api not available; tracing disabled");
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
})();
|
|
1009
|
+
return otelApiPromise2;
|
|
1010
|
+
}
|
|
1011
|
+
__name(loadOtelApi2, "loadOtelApi");
|
|
1012
|
+
var TRACE_KEYS = [
|
|
1013
|
+
"traceparent",
|
|
1014
|
+
"tracestate",
|
|
1015
|
+
"baggage"
|
|
1016
|
+
];
|
|
1017
|
+
async function injectTraceFields() {
|
|
1018
|
+
const api = await loadOtelApi2();
|
|
1019
|
+
if (!api) return {};
|
|
1020
|
+
const carrier = {};
|
|
1021
|
+
try {
|
|
1022
|
+
api.propagation.inject(api.context.active(), carrier, {
|
|
1023
|
+
set(c, k, v) {
|
|
1024
|
+
c[k] = v;
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
logger.debug("[redis-streams/trace-context] inject failed", {
|
|
1029
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return carrier;
|
|
1033
|
+
}
|
|
1034
|
+
__name(injectTraceFields, "injectTraceFields");
|
|
1035
|
+
async function extractTraceContext(fields) {
|
|
1036
|
+
const api = await loadOtelApi2();
|
|
1037
|
+
if (!api) return void 0;
|
|
1038
|
+
const carrier = {};
|
|
1039
|
+
for (const key of TRACE_KEYS) {
|
|
1040
|
+
const value = fields[key];
|
|
1041
|
+
if (value) carrier[key] = value;
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
return api.propagation.extract(api.context.active(), carrier, {
|
|
1045
|
+
get(c, k) {
|
|
1046
|
+
return c[k];
|
|
1047
|
+
},
|
|
1048
|
+
keys(c) {
|
|
1049
|
+
return Object.keys(c);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
} catch {
|
|
1053
|
+
return void 0;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
__name(extractTraceContext, "extractTraceContext");
|
|
1057
|
+
async function runWithConsumerSpan2(subject, fields, handler) {
|
|
1058
|
+
const api = await loadOtelApi2();
|
|
1059
|
+
if (!api) return handler();
|
|
1060
|
+
try {
|
|
1061
|
+
const parentCtx = await extractTraceContext(fields);
|
|
1062
|
+
const tracer = api.trace.getTracer("@burdenoff/be-sdk/redis-streams");
|
|
1063
|
+
return await new Promise((resolve, reject) => {
|
|
1064
|
+
tracer.startActiveSpan(`redis-streams.consume ${subject}`, {
|
|
1065
|
+
kind: 5,
|
|
1066
|
+
attributes: {
|
|
1067
|
+
"messaging.system": "redis",
|
|
1068
|
+
"messaging.destination.name": subject
|
|
1069
|
+
}
|
|
1070
|
+
}, parentCtx, async (span) => {
|
|
1071
|
+
try {
|
|
1072
|
+
const result = await handler();
|
|
1073
|
+
span.setStatus({
|
|
1074
|
+
code: 1
|
|
1075
|
+
});
|
|
1076
|
+
span.end();
|
|
1077
|
+
resolve(result);
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
span.recordException(err);
|
|
1080
|
+
span.setStatus({
|
|
1081
|
+
code: 2,
|
|
1082
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1083
|
+
});
|
|
1084
|
+
span.end();
|
|
1085
|
+
reject(err);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
} catch {
|
|
1090
|
+
return handler();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
__name(runWithConsumerSpan2, "runWithConsumerSpan");
|
|
1094
|
+
|
|
1095
|
+
// src/event-queue/implementations/redis-streams/RedisStreamsEventConsumer.ts
|
|
1096
|
+
var DEFAULT_BLOCK_MS = 5e3;
|
|
1097
|
+
var DEFAULT_BATCH = 10;
|
|
1098
|
+
var REGISTRY_REFRESH_MS = 1e4;
|
|
1099
|
+
var DEFAULT_ACK_WAIT_MS = 3e4;
|
|
1100
|
+
var DEFAULT_MAX_DELIVER = 3;
|
|
1101
|
+
var SWEEP_INTERVAL_MS = 15e3;
|
|
1102
|
+
var DELAYQ_INTERVAL_MS = 1e3;
|
|
1103
|
+
var RedisStreamsSubscription = class RedisStreamsSubscription2 {
|
|
1104
|
+
static {
|
|
1105
|
+
__name(this, "RedisStreamsSubscription");
|
|
1106
|
+
}
|
|
1107
|
+
subject;
|
|
1108
|
+
unsubscribeFn;
|
|
1109
|
+
id = randomUUID();
|
|
1110
|
+
active = true;
|
|
1111
|
+
receivedCount = 0;
|
|
1112
|
+
processedCount = 0;
|
|
1113
|
+
failedCount = 0;
|
|
1114
|
+
lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1115
|
+
constructor(subject, unsubscribeFn) {
|
|
1116
|
+
this.subject = subject;
|
|
1117
|
+
this.unsubscribeFn = unsubscribeFn;
|
|
1118
|
+
}
|
|
1119
|
+
async unsubscribe() {
|
|
1120
|
+
if (!this.active) return;
|
|
1121
|
+
this.active = false;
|
|
1122
|
+
await this.unsubscribeFn();
|
|
1123
|
+
}
|
|
1124
|
+
isActive() {
|
|
1125
|
+
return this.active;
|
|
1126
|
+
}
|
|
1127
|
+
getStats() {
|
|
1128
|
+
return {
|
|
1129
|
+
messagesReceived: this.receivedCount,
|
|
1130
|
+
messagesProcessed: this.processedCount,
|
|
1131
|
+
messagesFailed: this.failedCount,
|
|
1132
|
+
lastActivity: this.lastActivity
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
recordReceived() {
|
|
1136
|
+
this.receivedCount++;
|
|
1137
|
+
this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1138
|
+
}
|
|
1139
|
+
recordProcessed() {
|
|
1140
|
+
this.processedCount++;
|
|
1141
|
+
this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1142
|
+
}
|
|
1143
|
+
recordFailed() {
|
|
1144
|
+
this.failedCount++;
|
|
1145
|
+
this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
var RedisStreamsEventConsumer = class {
|
|
1149
|
+
static {
|
|
1150
|
+
__name(this, "RedisStreamsEventConsumer");
|
|
1151
|
+
}
|
|
1152
|
+
connection;
|
|
1153
|
+
subscriptions = [];
|
|
1154
|
+
subscriptionLoops = /* @__PURE__ */ new Set();
|
|
1155
|
+
keyPrefix;
|
|
1156
|
+
consumerName;
|
|
1157
|
+
group;
|
|
1158
|
+
maxDeliver;
|
|
1159
|
+
ackWaitMs;
|
|
1160
|
+
maxConcurrent;
|
|
1161
|
+
consumedCount = 0;
|
|
1162
|
+
failedCount = 0;
|
|
1163
|
+
lastActivity = null;
|
|
1164
|
+
stopped = false;
|
|
1165
|
+
sweepTimer = null;
|
|
1166
|
+
delayqTimer = null;
|
|
1167
|
+
knownStreams = /* @__PURE__ */ new Map();
|
|
1168
|
+
constructor(config) {
|
|
1169
|
+
this.connection = new RedisStreamsConnection({
|
|
1170
|
+
type: "redis-streams",
|
|
1171
|
+
url: config.url ?? process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
1172
|
+
user: config.user,
|
|
1173
|
+
password: config.password,
|
|
1174
|
+
name: config.name,
|
|
1175
|
+
connectionTimeout: config.connectionTimeout
|
|
1176
|
+
});
|
|
1177
|
+
this.keyPrefix = config.keyPrefix ?? process.env["REDIS_STREAMS_KEY_PREFIX"] ?? "boff:events";
|
|
1178
|
+
this.group = config.consumer || config.name || "default-consumer";
|
|
1179
|
+
this.consumerName = `${this.group}:${hostname()}:${process.pid}`;
|
|
1180
|
+
this.maxDeliver = config.maxDeliver ?? DEFAULT_MAX_DELIVER;
|
|
1181
|
+
this.ackWaitMs = config.ackWait ? Math.max(1e3, Math.round(config.ackWait / 1e6)) : DEFAULT_ACK_WAIT_MS;
|
|
1182
|
+
this.maxConcurrent = config.maxConcurrent ?? 5;
|
|
1183
|
+
}
|
|
1184
|
+
async connect() {
|
|
1185
|
+
await this.connection.connect();
|
|
1186
|
+
if (!this.sweepTimer) {
|
|
1187
|
+
const t = setInterval(() => {
|
|
1188
|
+
void this.sweepPending().catch((err) => {
|
|
1189
|
+
logger.debug("[RedisStreamsEventConsumer] sweep error", {
|
|
1190
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
}, SWEEP_INTERVAL_MS);
|
|
1194
|
+
t.unref?.();
|
|
1195
|
+
this.sweepTimer = t;
|
|
1196
|
+
}
|
|
1197
|
+
if (!this.delayqTimer) {
|
|
1198
|
+
const t = setInterval(() => {
|
|
1199
|
+
void this.drainDelayQueues().catch(() => void 0);
|
|
1200
|
+
}, DELAYQ_INTERVAL_MS);
|
|
1201
|
+
t.unref?.();
|
|
1202
|
+
this.delayqTimer = t;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
async disconnect() {
|
|
1206
|
+
this.stopped = true;
|
|
1207
|
+
if (this.sweepTimer) clearInterval(this.sweepTimer);
|
|
1208
|
+
if (this.delayqTimer) clearInterval(this.delayqTimer);
|
|
1209
|
+
await this.unsubscribeAll();
|
|
1210
|
+
await this.connection.disconnect();
|
|
1211
|
+
}
|
|
1212
|
+
isConnected() {
|
|
1213
|
+
return this.connection.isConnected();
|
|
1214
|
+
}
|
|
1215
|
+
async healthCheck() {
|
|
1216
|
+
try {
|
|
1217
|
+
const t0 = Date.now();
|
|
1218
|
+
const pong = await this.connection.ping();
|
|
1219
|
+
return {
|
|
1220
|
+
status: pong === "PONG" ? "healthy" : "degraded",
|
|
1221
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1222
|
+
connected: this.isConnected(),
|
|
1223
|
+
latencyMs: Date.now() - t0,
|
|
1224
|
+
details: {
|
|
1225
|
+
type: "redis-streams",
|
|
1226
|
+
subscriptions: this.subscriptions.length
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
return {
|
|
1231
|
+
status: "unhealthy",
|
|
1232
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1233
|
+
connected: false,
|
|
1234
|
+
details: {
|
|
1235
|
+
type: "redis-streams"
|
|
1236
|
+
},
|
|
1237
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async getStatistics() {
|
|
1242
|
+
return Promise.resolve({
|
|
1243
|
+
messagesConsumed: this.consumedCount,
|
|
1244
|
+
messagesFailed: this.failedCount,
|
|
1245
|
+
messagesPublished: 0,
|
|
1246
|
+
activeConsumers: this.subscriptions.length,
|
|
1247
|
+
lastActivity: this.lastActivity?.toISOString() ?? null,
|
|
1248
|
+
details: {
|
|
1249
|
+
type: "redis-streams",
|
|
1250
|
+
group: this.group
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
async ensureStream(_config) {
|
|
1255
|
+
return Promise.resolve();
|
|
1256
|
+
}
|
|
1257
|
+
async ensureConsumer(_config) {
|
|
1258
|
+
return Promise.resolve();
|
|
1259
|
+
}
|
|
1260
|
+
async subscribe(opts) {
|
|
1261
|
+
await this.connect();
|
|
1262
|
+
const sub = new RedisStreamsSubscription(opts.subject, async () => {
|
|
1263
|
+
this.subscriptions = this.subscriptions.filter((s) => s !== sub);
|
|
1264
|
+
});
|
|
1265
|
+
this.subscriptions.push(sub);
|
|
1266
|
+
const startId = this.resolveStartId(opts.options?.deliverPolicy);
|
|
1267
|
+
const wildcard = isWildcard(opts.subject);
|
|
1268
|
+
const handlerFn = opts.handler;
|
|
1269
|
+
if (!wildcard) {
|
|
1270
|
+
const stream = streamKey(opts.subject, {
|
|
1271
|
+
prefix: this.keyPrefix
|
|
1272
|
+
});
|
|
1273
|
+
await this.ensureGroup(stream, startId);
|
|
1274
|
+
this.knownStreams.set(stream, opts.subject);
|
|
1275
|
+
this.spawnLoop(sub, stream, opts.subject, handlerFn);
|
|
1276
|
+
} else {
|
|
1277
|
+
const boundary = boundaryOf(opts.subject);
|
|
1278
|
+
const refresh = /* @__PURE__ */ __name(async () => {
|
|
1279
|
+
const client = this.connection.getClient();
|
|
1280
|
+
const subjects = await client.smembers(registryKey(boundary));
|
|
1281
|
+
for (const s of subjects) {
|
|
1282
|
+
if (!matchSubject(opts.subject, s)) continue;
|
|
1283
|
+
const stream = streamKey(s, {
|
|
1284
|
+
prefix: this.keyPrefix
|
|
1285
|
+
});
|
|
1286
|
+
if (this.knownStreams.has(stream)) continue;
|
|
1287
|
+
await this.ensureGroup(stream, startId);
|
|
1288
|
+
this.knownStreams.set(stream, s);
|
|
1289
|
+
this.spawnLoop(sub, stream, s, handlerFn);
|
|
1290
|
+
}
|
|
1291
|
+
}, "refresh");
|
|
1292
|
+
await refresh();
|
|
1293
|
+
const t = setInterval(() => {
|
|
1294
|
+
if (this.stopped || !sub.isActive()) {
|
|
1295
|
+
clearInterval(t);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
void refresh().catch(() => void 0);
|
|
1299
|
+
}, REGISTRY_REFRESH_MS);
|
|
1300
|
+
t.unref?.();
|
|
1301
|
+
}
|
|
1302
|
+
return sub;
|
|
1303
|
+
}
|
|
1304
|
+
async subscribeMultiple(subjects, handler, options) {
|
|
1305
|
+
const out = [];
|
|
1306
|
+
for (const subject of subjects) {
|
|
1307
|
+
out.push(await this.subscribe({
|
|
1308
|
+
subject,
|
|
1309
|
+
handler,
|
|
1310
|
+
options
|
|
1311
|
+
}));
|
|
1312
|
+
}
|
|
1313
|
+
return out;
|
|
1314
|
+
}
|
|
1315
|
+
async pull(opts) {
|
|
1316
|
+
await this.connect();
|
|
1317
|
+
const stream = streamKey(opts.subject, {
|
|
1318
|
+
prefix: this.keyPrefix
|
|
1319
|
+
});
|
|
1320
|
+
await this.ensureGroup(stream, "$");
|
|
1321
|
+
const client = this.connection.getClient();
|
|
1322
|
+
const raw = await client.xreadgroup("GROUP", this.group, this.consumerName, "COUNT", opts.batch ?? DEFAULT_BATCH, "BLOCK", opts.timeout ?? DEFAULT_BLOCK_MS, "STREAMS", stream, ">");
|
|
1323
|
+
if (!raw) return [];
|
|
1324
|
+
const out = [];
|
|
1325
|
+
for (const [streamName, entries] of raw) {
|
|
1326
|
+
for (const [id, rawFields] of entries) {
|
|
1327
|
+
const parsed = this.parseEntry(streamName, id, rawFields);
|
|
1328
|
+
out.push(this.toAcknowledgeable(streamName, id, parsed));
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return out;
|
|
1332
|
+
}
|
|
1333
|
+
async unsubscribeAll() {
|
|
1334
|
+
for (const sub of [
|
|
1335
|
+
...this.subscriptions
|
|
1336
|
+
]) {
|
|
1337
|
+
await sub.unsubscribe();
|
|
1338
|
+
}
|
|
1339
|
+
this.subscriptions = [];
|
|
1340
|
+
this.subscriptionLoops.clear();
|
|
1341
|
+
}
|
|
1342
|
+
getSubscriptions() {
|
|
1343
|
+
return [
|
|
1344
|
+
...this.subscriptions
|
|
1345
|
+
];
|
|
1346
|
+
}
|
|
1347
|
+
async processDLQ(handler) {
|
|
1348
|
+
await this.connect();
|
|
1349
|
+
const client = this.connection.getClient();
|
|
1350
|
+
const dlqGroup = `${this.group}-dlq`;
|
|
1351
|
+
for (const [stream] of this.knownStreams) {
|
|
1352
|
+
const dlq = dlqKey(stream);
|
|
1353
|
+
try {
|
|
1354
|
+
await client.xgroup("CREATE", dlq, dlqGroup, "0", "MKSTREAM");
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
const res = await client.xreadgroup("GROUP", dlqGroup, this.consumerName, "COUNT", 50, "BLOCK", 100, "STREAMS", dlq, ">");
|
|
1358
|
+
if (!res) continue;
|
|
1359
|
+
for (const [, entries] of res) {
|
|
1360
|
+
for (const [id, rawFields] of entries) {
|
|
1361
|
+
const parsed = this.parseEntry(dlq, id, rawFields);
|
|
1362
|
+
const evt = this.toAcknowledgeable(dlq, id, parsed, dlqGroup);
|
|
1363
|
+
try {
|
|
1364
|
+
await handler(evt);
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
logger.error("[RedisStreamsEventConsumer] DLQ handler error", {
|
|
1367
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
async listSubjects(_stream) {
|
|
1375
|
+
await this.connect();
|
|
1376
|
+
const client = this.connection.getClient();
|
|
1377
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1378
|
+
for (const subject of this.knownStreams.values()) {
|
|
1379
|
+
const members = await client.smembers(registryKey(boundaryOf(subject)));
|
|
1380
|
+
for (const m of members) seen.add(m);
|
|
1381
|
+
}
|
|
1382
|
+
return [
|
|
1383
|
+
...seen
|
|
1384
|
+
];
|
|
1385
|
+
}
|
|
1386
|
+
// ---- internals ----
|
|
1387
|
+
resolveStartId(policy) {
|
|
1388
|
+
switch (policy) {
|
|
1389
|
+
case "all":
|
|
1390
|
+
return "0";
|
|
1391
|
+
case "new":
|
|
1392
|
+
return "$";
|
|
1393
|
+
case "last":
|
|
1394
|
+
return "$";
|
|
1395
|
+
default:
|
|
1396
|
+
return "$";
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
async ensureGroup(stream, startId) {
|
|
1400
|
+
const client = this.connection.getClient();
|
|
1401
|
+
try {
|
|
1402
|
+
await client.xgroup("CREATE", stream, this.group, startId, "MKSTREAM");
|
|
1403
|
+
logger.debug("[RedisStreamsEventConsumer] group created", {
|
|
1404
|
+
stream,
|
|
1405
|
+
group: this.group,
|
|
1406
|
+
startId
|
|
1407
|
+
});
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1410
|
+
if (!msg.includes("BUSYGROUP")) {
|
|
1411
|
+
throw err;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
spawnLoop(sub, stream, subject, handler) {
|
|
1416
|
+
const loop = /* @__PURE__ */ __name(async () => {
|
|
1417
|
+
const client = await this.connection.makeBlockingClient();
|
|
1418
|
+
try {
|
|
1419
|
+
while (!this.stopped && sub.isActive()) {
|
|
1420
|
+
let raw = null;
|
|
1421
|
+
try {
|
|
1422
|
+
raw = await client.xreadgroup("GROUP", this.group, this.consumerName, "COUNT", this.maxConcurrent, "BLOCK", DEFAULT_BLOCK_MS, "STREAMS", stream, ">");
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1425
|
+
if (msg.includes("NOGROUP")) {
|
|
1426
|
+
await this.ensureGroup(stream, "$");
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
logger.warn("[RedisStreamsEventConsumer] read error", {
|
|
1430
|
+
stream,
|
|
1431
|
+
error: msg
|
|
1432
|
+
});
|
|
1433
|
+
await new Promise((res) => setTimeout(res, 1e3));
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (!raw) continue;
|
|
1437
|
+
for (const [, entries] of raw) {
|
|
1438
|
+
for (const [id, rawFields] of entries) {
|
|
1439
|
+
sub.recordReceived();
|
|
1440
|
+
this.consumedCount++;
|
|
1441
|
+
this.lastActivity = /* @__PURE__ */ new Date();
|
|
1442
|
+
const parsed = this.parseEntry(stream, id, rawFields);
|
|
1443
|
+
const evt = this.toAcknowledgeable(stream, id, parsed);
|
|
1444
|
+
try {
|
|
1445
|
+
await runWithConsumerSpan2(subject, parsed.fields, () => handler(evt));
|
|
1446
|
+
sub.recordProcessed();
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
sub.recordFailed();
|
|
1449
|
+
this.failedCount++;
|
|
1450
|
+
logger.error("[RedisStreamsEventConsumer] handler error", {
|
|
1451
|
+
subject,
|
|
1452
|
+
id,
|
|
1453
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1454
|
+
});
|
|
1455
|
+
try {
|
|
1456
|
+
await evt.nak();
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
} finally {
|
|
1464
|
+
try {
|
|
1465
|
+
client.disconnect();
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}, "loop");
|
|
1470
|
+
void loop().catch((err) => {
|
|
1471
|
+
logger.error("[RedisStreamsEventConsumer] loop crashed", {
|
|
1472
|
+
stream,
|
|
1473
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
parseEntry(stream, id, rawFields) {
|
|
1478
|
+
const fields = {};
|
|
1479
|
+
for (let i = 0; i + 1 < rawFields.length; i += 2) {
|
|
1480
|
+
const key = rawFields[i];
|
|
1481
|
+
const val = rawFields[i + 1];
|
|
1482
|
+
if (key === void 0 || val === void 0) continue;
|
|
1483
|
+
fields[key] = val;
|
|
1484
|
+
}
|
|
1485
|
+
let data;
|
|
1486
|
+
try {
|
|
1487
|
+
data = JSON.parse(fields["payload"] ?? "null");
|
|
1488
|
+
} catch {
|
|
1489
|
+
data = fields["payload"];
|
|
1490
|
+
}
|
|
1491
|
+
let metadata;
|
|
1492
|
+
try {
|
|
1493
|
+
metadata = JSON.parse(fields["metadata"] ?? "{}");
|
|
1494
|
+
} catch {
|
|
1495
|
+
metadata = {
|
|
1496
|
+
correlationId: fields["correlation-id"] ?? ""
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
if (!metadata.correlationId) metadata.correlationId = fields["correlation-id"] ?? "";
|
|
1500
|
+
return {
|
|
1501
|
+
id,
|
|
1502
|
+
stream,
|
|
1503
|
+
subject: fields["subject"] ?? "",
|
|
1504
|
+
data,
|
|
1505
|
+
metadata,
|
|
1506
|
+
timestamp: fields["timestamp"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1507
|
+
fields,
|
|
1508
|
+
deliveryCount: 1
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
toAcknowledgeable(stream, id, parsed, group = this.group) {
|
|
1512
|
+
const connection = this.connection;
|
|
1513
|
+
let acked = false;
|
|
1514
|
+
return {
|
|
1515
|
+
id,
|
|
1516
|
+
subject: parsed.subject,
|
|
1517
|
+
data: parsed.data,
|
|
1518
|
+
metadata: parsed.metadata,
|
|
1519
|
+
timestamp: parsed.timestamp,
|
|
1520
|
+
async ack() {
|
|
1521
|
+
if (acked) return;
|
|
1522
|
+
acked = true;
|
|
1523
|
+
const client = connection.getClient();
|
|
1524
|
+
await client.xack(stream, group, id);
|
|
1525
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1526
|
+
},
|
|
1527
|
+
async nak(delay = 0) {
|
|
1528
|
+
if (acked) return;
|
|
1529
|
+
acked = true;
|
|
1530
|
+
const client = connection.getClient();
|
|
1531
|
+
if (delay > 0) {
|
|
1532
|
+
const score = Date.now() + delay;
|
|
1533
|
+
const payload = JSON.stringify(parsed.fields);
|
|
1534
|
+
await client.zadd(delayQueueKey(stream), score, `${id}|${payload}`);
|
|
1535
|
+
await client.xack(stream, group, id);
|
|
1536
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
async term() {
|
|
1540
|
+
if (acked) return;
|
|
1541
|
+
acked = true;
|
|
1542
|
+
const client = connection.getClient();
|
|
1543
|
+
const dlq = dlqKey(stream);
|
|
1544
|
+
const flat = [];
|
|
1545
|
+
for (const [k, v] of Object.entries(parsed.fields)) flat.push(k, v);
|
|
1546
|
+
await client.xadd(dlq, "MAXLEN", "~", 1e4, "*", ...flat);
|
|
1547
|
+
await client.xack(stream, group, id);
|
|
1548
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1549
|
+
},
|
|
1550
|
+
isRedelivered() {
|
|
1551
|
+
return parsed.deliveryCount > 1;
|
|
1552
|
+
},
|
|
1553
|
+
getDeliveryAttempt() {
|
|
1554
|
+
return parsed.deliveryCount;
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
async sweepPending() {
|
|
1559
|
+
if (this.stopped || this.knownStreams.size === 0) return;
|
|
1560
|
+
const client = this.connection.getClient();
|
|
1561
|
+
for (const [stream] of this.knownStreams) {
|
|
1562
|
+
try {
|
|
1563
|
+
const claimed = await client.xautoclaim(stream, this.group, this.consumerName, this.ackWaitMs, "0", "COUNT", 50);
|
|
1564
|
+
if (!claimed) continue;
|
|
1565
|
+
const entries = claimed[1] ?? [];
|
|
1566
|
+
for (const [id, rawFields] of entries) {
|
|
1567
|
+
const pending = await client.xpending(stream, this.group, "IDLE", 0, "-", "+", 1, this.consumerName);
|
|
1568
|
+
const deliveryCount = Array.isArray(pending) && pending[0] ? pending[0][3] : 1;
|
|
1569
|
+
if (deliveryCount > this.maxDeliver) {
|
|
1570
|
+
const flat = [];
|
|
1571
|
+
for (let i = 0; i + 1 < rawFields.length; i += 2) {
|
|
1572
|
+
flat.push(rawFields[i], rawFields[i + 1]);
|
|
1573
|
+
}
|
|
1574
|
+
flat.push("dlq-reason", "max-deliver-exceeded");
|
|
1575
|
+
flat.push("dlq-deliveries", String(deliveryCount));
|
|
1576
|
+
await client.xadd(dlqKey(stream), "MAXLEN", "~", 1e4, "*", ...flat);
|
|
1577
|
+
await client.xack(stream, this.group, id);
|
|
1578
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1579
|
+
logger.warn("[RedisStreamsEventConsumer] message moved to DLQ", {
|
|
1580
|
+
stream,
|
|
1581
|
+
id,
|
|
1582
|
+
deliveryCount
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1588
|
+
if (msg.includes("NOGROUP")) continue;
|
|
1589
|
+
logger.debug("[RedisStreamsEventConsumer] sweep iteration error", {
|
|
1590
|
+
stream,
|
|
1591
|
+
error: msg
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async drainDelayQueues() {
|
|
1597
|
+
if (this.stopped || this.knownStreams.size === 0) return;
|
|
1598
|
+
const client = this.connection.getClient();
|
|
1599
|
+
for (const [stream] of this.knownStreams) {
|
|
1600
|
+
const lockKey = delayLockKey(stream);
|
|
1601
|
+
const got = await client.set(lockKey, this.consumerName, "NX", "EX", 30).catch(() => null);
|
|
1602
|
+
if (got !== "OK") continue;
|
|
1603
|
+
try {
|
|
1604
|
+
const due = await client.zrangebyscore(delayQueueKey(stream), 0, Date.now(), "LIMIT", 0, 50);
|
|
1605
|
+
for (const member of due) {
|
|
1606
|
+
const split = member.indexOf("|");
|
|
1607
|
+
if (split < 0) continue;
|
|
1608
|
+
const payload = member.slice(split + 1);
|
|
1609
|
+
let fields = {};
|
|
1610
|
+
try {
|
|
1611
|
+
fields = JSON.parse(payload);
|
|
1612
|
+
} catch {
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
const flat = [];
|
|
1616
|
+
for (const [k, v] of Object.entries(fields)) flat.push(k, v);
|
|
1617
|
+
await client.xadd(stream, "MAXLEN", "~", 1e5, "*", ...flat);
|
|
1618
|
+
await client.zrem(delayQueueKey(stream), member);
|
|
1619
|
+
}
|
|
1620
|
+
} finally {
|
|
1621
|
+
await client.del(lockKey).catch(() => void 0);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
var RedisStreamsEventPublisher = class {
|
|
1627
|
+
static {
|
|
1628
|
+
__name(this, "RedisStreamsEventPublisher");
|
|
1629
|
+
}
|
|
1630
|
+
config;
|
|
1631
|
+
connection;
|
|
1632
|
+
publishCount = 0;
|
|
1633
|
+
lastPublishTime = null;
|
|
1634
|
+
maxLen;
|
|
1635
|
+
dedupTtl;
|
|
1636
|
+
keyPrefix;
|
|
1637
|
+
constructor(config) {
|
|
1638
|
+
this.config = config;
|
|
1639
|
+
this.connection = new RedisStreamsConnection(config);
|
|
1640
|
+
this.maxLen = config.maxLen ?? parseInt(process.env["REDIS_STREAMS_MAXLEN"] ?? "100000", 10);
|
|
1641
|
+
this.dedupTtl = config.dedupTtlSeconds ?? parseInt(process.env["REDIS_STREAMS_DEDUP_TTL"] ?? "120", 10);
|
|
1642
|
+
this.keyPrefix = config.keyPrefix ?? process.env["REDIS_STREAMS_KEY_PREFIX"] ?? "boff:events";
|
|
1643
|
+
}
|
|
1644
|
+
async connect() {
|
|
1645
|
+
await this.connection.connect();
|
|
1646
|
+
}
|
|
1647
|
+
async disconnect() {
|
|
1648
|
+
await this.connection.disconnect();
|
|
1649
|
+
}
|
|
1650
|
+
isConnected() {
|
|
1651
|
+
return this.connection.isConnected();
|
|
1652
|
+
}
|
|
1653
|
+
async healthCheck() {
|
|
1654
|
+
try {
|
|
1655
|
+
const t0 = Date.now();
|
|
1656
|
+
const pong = await this.connection.ping();
|
|
1657
|
+
return {
|
|
1658
|
+
status: pong === "PONG" ? "healthy" : "degraded",
|
|
1659
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1660
|
+
connected: this.isConnected(),
|
|
1661
|
+
latencyMs: Date.now() - t0,
|
|
1662
|
+
details: {
|
|
1663
|
+
type: "redis-streams",
|
|
1664
|
+
url: this.config.url
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
return {
|
|
1669
|
+
status: "unhealthy",
|
|
1670
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1671
|
+
connected: false,
|
|
1672
|
+
details: {
|
|
1673
|
+
type: "redis-streams"
|
|
1674
|
+
},
|
|
1675
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
async getStatistics() {
|
|
1680
|
+
return {
|
|
1681
|
+
messagesPublished: this.publishCount,
|
|
1682
|
+
messagesConsumed: 0,
|
|
1683
|
+
messagesFailed: 0,
|
|
1684
|
+
activePublishers: 1,
|
|
1685
|
+
activeConsumers: 0,
|
|
1686
|
+
lastActivity: this.lastPublishTime?.toISOString() ?? null,
|
|
1687
|
+
details: {
|
|
1688
|
+
type: "redis-streams"
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
async ensureStream(_config) {
|
|
1693
|
+
return Promise.resolve();
|
|
1694
|
+
}
|
|
1695
|
+
async publish(options) {
|
|
1696
|
+
return this.publishInternal(options);
|
|
1697
|
+
}
|
|
1698
|
+
async publishInternal(opts) {
|
|
1699
|
+
await this.connect();
|
|
1700
|
+
const client = this.connection.getClient();
|
|
1701
|
+
const stream = streamKey(opts.subject, {
|
|
1702
|
+
prefix: this.keyPrefix
|
|
1703
|
+
});
|
|
1704
|
+
const msgId = opts.options?.messageId ?? randomUUID();
|
|
1705
|
+
const correlationId = opts.metadata?.correlationId ?? randomUUID();
|
|
1706
|
+
const enableDedup = this.config.enableDeduplication === true || opts.options?.messageId !== void 0;
|
|
1707
|
+
if (enableDedup) {
|
|
1708
|
+
const ok = await client.set(dedupKey(stream, msgId), "1", "NX", "EX", this.dedupTtl);
|
|
1709
|
+
if (ok !== "OK") {
|
|
1710
|
+
logger.debug("[RedisStreamsEventPublisher] dedup hit; skipping publish", {
|
|
1711
|
+
subject: opts.subject,
|
|
1712
|
+
msgId
|
|
1713
|
+
});
|
|
1714
|
+
return msgId;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const trace = await injectTraceFields();
|
|
1718
|
+
const fields = {
|
|
1719
|
+
"msg-id": msgId,
|
|
1720
|
+
subject: opts.subject,
|
|
1721
|
+
"correlation-id": correlationId,
|
|
1722
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1723
|
+
priority: opts.options?.priority ?? this.config.defaultPriority ?? "normal",
|
|
1724
|
+
source: opts.metadata?.source ?? this.config.name ?? "unknown",
|
|
1725
|
+
payload: JSON.stringify(opts.data ?? null),
|
|
1726
|
+
metadata: JSON.stringify(opts.metadata ?? {}),
|
|
1727
|
+
...trace
|
|
1728
|
+
};
|
|
1729
|
+
if (opts.metadata?.userId) fields["user-id"] = String(opts.metadata.userId);
|
|
1730
|
+
if (opts.metadata?.workspaceId) fields["workspace-id"] = String(opts.metadata.workspaceId);
|
|
1731
|
+
if (opts.metadata?.tenantId) fields["tenant-id"] = String(opts.metadata.tenantId);
|
|
1732
|
+
const flat = [];
|
|
1733
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1734
|
+
flat.push(k, v);
|
|
1735
|
+
}
|
|
1736
|
+
const id = await client.xadd(stream, "MAXLEN", "~", this.maxLen, "*", ...flat);
|
|
1737
|
+
void client.sadd(registryKey(boundaryOf(opts.subject)), opts.subject).catch(() => void 0);
|
|
1738
|
+
this.publishCount++;
|
|
1739
|
+
this.lastPublishTime = /* @__PURE__ */ new Date();
|
|
1740
|
+
logger.debug("[RedisStreamsEventPublisher] published", {
|
|
1741
|
+
subject: opts.subject,
|
|
1742
|
+
stream,
|
|
1743
|
+
id
|
|
1744
|
+
});
|
|
1745
|
+
return id;
|
|
1746
|
+
}
|
|
1747
|
+
async publishBatch(events) {
|
|
1748
|
+
const out = [];
|
|
1749
|
+
for (const e of events) {
|
|
1750
|
+
out.push(await this.publishInternal(e));
|
|
1751
|
+
}
|
|
1752
|
+
return out;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Emulated request/response: writer XADDs onto its own subject, then
|
|
1756
|
+
* blocks on a one-shot reply stream keyed by correlationId.
|
|
1757
|
+
*/
|
|
1758
|
+
async publishWithAck(opts) {
|
|
1759
|
+
await this.connect();
|
|
1760
|
+
const client = this.connection.getClient();
|
|
1761
|
+
const correlationId = opts.metadata?.correlationId ?? randomUUID();
|
|
1762
|
+
const reply = replyKey(correlationId);
|
|
1763
|
+
await this.publishInternal({
|
|
1764
|
+
subject: opts.subject,
|
|
1765
|
+
data: opts.data,
|
|
1766
|
+
metadata: {
|
|
1767
|
+
...opts.metadata,
|
|
1768
|
+
correlationId,
|
|
1769
|
+
replyTo: reply
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
const timeoutMs = opts.timeout ?? 5e3;
|
|
1773
|
+
const t0 = Date.now();
|
|
1774
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
1775
|
+
const entries = await client.xrange(reply, "-", "+");
|
|
1776
|
+
if (Array.isArray(entries) && entries.length > 0) {
|
|
1777
|
+
const last = entries[entries.length - 1];
|
|
1778
|
+
if (!last) break;
|
|
1779
|
+
const rawFields = last[1];
|
|
1780
|
+
const payloadIdx = rawFields.indexOf("payload");
|
|
1781
|
+
const payload = payloadIdx >= 0 ? rawFields[payloadIdx + 1] : "{}";
|
|
1782
|
+
await client.del(reply);
|
|
1783
|
+
return JSON.parse(payload ?? "{}");
|
|
1784
|
+
}
|
|
1785
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
1786
|
+
}
|
|
1787
|
+
await client.del(reply).catch(() => void 0);
|
|
1788
|
+
throw new Error(`[RedisStreamsEventPublisher] publishWithAck timed out for subject=${opts.subject}`);
|
|
1789
|
+
}
|
|
1790
|
+
/** Internal helper used by the consumer's reply path. */
|
|
1791
|
+
async sendReply(correlationId, data) {
|
|
1792
|
+
await this.connect();
|
|
1793
|
+
const client = this.connection.getClient();
|
|
1794
|
+
const key = replyKey(correlationId);
|
|
1795
|
+
await client.xadd(key, "*", "payload", JSON.stringify(data ?? null));
|
|
1796
|
+
await client.expire(key, 60);
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
|
|
924
1800
|
// src/event-queue/factory/EventQueueFactory.ts
|
|
1801
|
+
function resolveQueueType() {
|
|
1802
|
+
const raw = (process.env["EVENT_QUEUE_TYPE"] ?? process.env["QUEUE_TYPE"] ?? "").toLowerCase();
|
|
1803
|
+
if (raw === "redis-streams" || raw === "redis" || raw === "streams") return QueueType.REDIS_STREAMS;
|
|
1804
|
+
if (raw === "nats") return QueueType.NATS;
|
|
1805
|
+
if (!process.env["NATS_URL"] && !process.env["NATS_SERVERS"] && process.env["REDIS_URL"]) {
|
|
1806
|
+
return QueueType.REDIS_STREAMS;
|
|
1807
|
+
}
|
|
1808
|
+
return QueueType.NATS;
|
|
1809
|
+
}
|
|
1810
|
+
__name(resolveQueueType, "resolveQueueType");
|
|
925
1811
|
var EventQueueFactory = class _EventQueueFactory {
|
|
926
1812
|
static {
|
|
927
1813
|
__name(this, "EventQueueFactory");
|
|
@@ -937,6 +1823,9 @@ var EventQueueFactory = class _EventQueueFactory {
|
|
|
937
1823
|
if (config.type === QueueType.NATS) {
|
|
938
1824
|
return new NatsEventPublisher(config);
|
|
939
1825
|
}
|
|
1826
|
+
if (config.type === QueueType.REDIS_STREAMS) {
|
|
1827
|
+
return new RedisStreamsEventPublisher(config);
|
|
1828
|
+
}
|
|
940
1829
|
throw new Error(`Unsupported queue type: ${config.type}`);
|
|
941
1830
|
}
|
|
942
1831
|
/**
|
|
@@ -952,14 +1841,28 @@ var EventQueueFactory = class _EventQueueFactory {
|
|
|
952
1841
|
if (config.type === QueueType.NATS) {
|
|
953
1842
|
return new NatsEventConsumer(config);
|
|
954
1843
|
}
|
|
1844
|
+
if (config.type === QueueType.REDIS_STREAMS) {
|
|
1845
|
+
return new RedisStreamsEventConsumer(config);
|
|
1846
|
+
}
|
|
955
1847
|
throw new Error(`Unsupported queue type: ${config.type}`);
|
|
956
1848
|
}
|
|
957
1849
|
/**
|
|
958
1850
|
* Create a publisher from environment variables
|
|
959
1851
|
*/
|
|
960
1852
|
static createPublisherFromEnv() {
|
|
1853
|
+
const type = resolveQueueType();
|
|
1854
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
1855
|
+
return _EventQueueFactory.createPublisher({
|
|
1856
|
+
type,
|
|
1857
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
1858
|
+
password: process.env["REDIS_PASSWORD"],
|
|
1859
|
+
name: process.env["QUEUE_CLIENT_NAME"] ?? process.env["SERVICE_NAME"] ?? "be-sdk-publisher",
|
|
1860
|
+
enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
|
|
1861
|
+
defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] ?? "normal"
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
961
1864
|
const config = {
|
|
962
|
-
type
|
|
1865
|
+
type,
|
|
963
1866
|
url: process.env["NATS_URL"] || "nats://localhost:4222",
|
|
964
1867
|
user: process.env["NATS_USER"],
|
|
965
1868
|
password: process.env["NATS_PASSWORD"],
|
|
@@ -979,8 +1882,22 @@ var EventQueueFactory = class _EventQueueFactory {
|
|
|
979
1882
|
* Create a consumer from environment variables
|
|
980
1883
|
*/
|
|
981
1884
|
static createConsumerFromEnv(stream, consumer) {
|
|
1885
|
+
const type = resolveQueueType();
|
|
1886
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
1887
|
+
return _EventQueueFactory.createConsumer({
|
|
1888
|
+
type,
|
|
1889
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
1890
|
+
password: process.env["REDIS_PASSWORD"],
|
|
1891
|
+
name: process.env["QUEUE_CLIENT_NAME"] ?? process.env["SERVICE_NAME"] ?? consumer,
|
|
1892
|
+
stream,
|
|
1893
|
+
consumer,
|
|
1894
|
+
maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
|
|
1895
|
+
maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
|
|
1896
|
+
ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
982
1899
|
const config = {
|
|
983
|
-
type
|
|
1900
|
+
type,
|
|
984
1901
|
url: process.env["NATS_URL"] || "nats://localhost:4222",
|
|
985
1902
|
user: process.env["NATS_USER"],
|
|
986
1903
|
password: process.env["NATS_PASSWORD"],
|
|
@@ -1018,8 +1935,19 @@ var EventQueueFactory = class _EventQueueFactory {
|
|
|
1018
1935
|
static createPublisherForLayer(layer, serviceName) {
|
|
1019
1936
|
const envPrefix = layer.toUpperCase();
|
|
1020
1937
|
const defaults = DEFAULT_LAYER_RESOURCES[layer];
|
|
1938
|
+
const type = resolveQueueType();
|
|
1939
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
1940
|
+
return _EventQueueFactory.createPublisher({
|
|
1941
|
+
type,
|
|
1942
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
1943
|
+
password: process.env["REDIS_PASSWORD"],
|
|
1944
|
+
name: serviceName,
|
|
1945
|
+
enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
|
|
1946
|
+
defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] || "normal"
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1021
1949
|
const config = {
|
|
1022
|
-
type
|
|
1950
|
+
type,
|
|
1023
1951
|
url: process.env[`${envPrefix}_NATS_URL`] || process.env["NATS_URL"] || defaults.natsUrl,
|
|
1024
1952
|
user: process.env["NATS_USER"],
|
|
1025
1953
|
password: process.env["NATS_PASSWORD"],
|
|
@@ -1061,8 +1989,22 @@ var EventQueueFactory = class _EventQueueFactory {
|
|
|
1061
1989
|
static createConsumerForLayer(layer, serviceName, stream, consumer) {
|
|
1062
1990
|
const envPrefix = layer.toUpperCase();
|
|
1063
1991
|
const defaults = DEFAULT_LAYER_RESOURCES[layer];
|
|
1992
|
+
const type = resolveQueueType();
|
|
1993
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
1994
|
+
return _EventQueueFactory.createConsumer({
|
|
1995
|
+
type,
|
|
1996
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
1997
|
+
password: process.env["REDIS_PASSWORD"],
|
|
1998
|
+
name: serviceName,
|
|
1999
|
+
stream,
|
|
2000
|
+
consumer,
|
|
2001
|
+
maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
|
|
2002
|
+
maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
|
|
2003
|
+
ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
1064
2006
|
const config = {
|
|
1065
|
-
type
|
|
2007
|
+
type,
|
|
1066
2008
|
url: process.env[`${envPrefix}_NATS_URL`] || process.env["NATS_URL"] || defaults.natsUrl,
|
|
1067
2009
|
user: process.env["NATS_USER"],
|
|
1068
2010
|
password: process.env["NATS_PASSWORD"],
|
|
@@ -1089,6 +2031,6 @@ var EventQueueFactory = class _EventQueueFactory {
|
|
|
1089
2031
|
}
|
|
1090
2032
|
};
|
|
1091
2033
|
|
|
1092
|
-
export { EventQueueFactory, NatsEventConsumer, NatsEventPublisher };
|
|
1093
|
-
//# sourceMappingURL=chunk-
|
|
1094
|
-
//# sourceMappingURL=chunk-
|
|
2034
|
+
export { EventQueueFactory, NatsEventConsumer, NatsEventPublisher, RedisStreamsEventConsumer, RedisStreamsEventPublisher, dlqKey, streamKey };
|
|
2035
|
+
//# sourceMappingURL=chunk-EMSON3UD.js.map
|
|
2036
|
+
//# sourceMappingURL=chunk-EMSON3UD.js.map
|