@nestjs/microservices
Version:
Nest - modern, fast, powerful node.js web framework (@microservices)
210 lines (209 loc) • 8.42 kB
JavaScript
import { NO_MESSAGE_HANDLER, REDIS_DEFAULT_HOST, REDIS_DEFAULT_PORT, } from '../constants.js';
import { RedisContext } from '../ctx-host/index.js';
import { Transport } from '../enums/index.js';
import { Server } from './server.js';
import { isUndefined } from '@nestjs/common/internal';
/**
* @publicApi
*/
export class ServerRedis extends Server {
options;
transportId = Transport.REDIS;
subClient;
pubClient;
isManuallyClosed = false;
wasInitialConnectionSuccessful = false;
pendingEventListeners = [];
constructor(options) {
super();
this.options = options;
this.initializeSerializer(options);
this.initializeDeserializer(options);
}
async listen(callback) {
try {
this.subClient = await this.createRedisClient();
this.pubClient = await this.createRedisClient();
[this.subClient, this.pubClient].forEach((client, index) => {
const type = index === 0 ? 'sub' : 'pub';
this.registerErrorListener(client);
this.registerReconnectListener(client);
this.registerReadyListener(client);
this.registerEndListener(client);
this.pendingEventListeners.forEach(({ event, callback }) => client.on(event, (...args) => callback(type, ...args)));
});
this.pendingEventListeners = [];
this.start(callback);
}
catch (err) {
callback(err);
}
}
start(callback) {
void Promise.all([this.subClient.connect(), this.pubClient.connect()])
.then(() => this.bindEvents(this.subClient, this.pubClient))
.then(() => callback?.())
.catch(callback);
}
async bindEvents(subClient, pubClient) {
subClient.on(this.options?.wildcards ? 'pmessage' : 'message', this.getMessageHandler(pubClient).bind(this));
const subscribePatterns = [...this.messageHandlers.keys()];
// Awaited so that a rejected subscription (e.g., an ACL without access to
// the channel) fails `listen()` instead of going unhandled.
await Promise.all(subscribePatterns.map(pattern => {
const { isEventHandler } = this.messageHandlers.get(pattern);
const channel = isEventHandler
? pattern
: this.getRequestPattern(pattern);
return this.options?.wildcards
? subClient.psubscribe(channel)
: subClient.subscribe(channel);
}));
}
async close() {
this.isManuallyClosed = true;
this.pubClient && (await this.quitClient(this.pubClient));
this.subClient && (await this.quitClient(this.subClient));
this.pendingEventListeners = [];
}
// `quit()` rejects when the command cannot be sent (e.g., while
// reconnecting with the offline queue disabled), which would leave the
// other client open. Close the connection right away instead.
async quitClient(client) {
try {
await client.quit();
}
catch {
client.disconnect();
}
}
async createRedisClient() {
const clientInfoTag = this.getOptionsProp(this.options, 'clientInfoTag');
const redisPackage = await this.loadPackage('ioredis', ServerRedis.name, () => import('ioredis'));
const RedisClient = redisPackage.default || redisPackage;
return new RedisClient({
port: REDIS_DEFAULT_PORT,
host: REDIS_DEFAULT_HOST,
...this.getClientOptions(),
...(clientInfoTag && { clientInfoTag }),
lazyConnect: true,
});
}
getMessageHandler(pub) {
return this.options?.wildcards
? (channel, pattern, buffer) => this.handleMessage(channel, buffer, pub, pattern).catch(err => this.handleError(err))
: (channel, buffer) => this.handleMessage(channel, buffer, pub, channel).catch(err => this.handleError(err));
}
async handleMessage(channel, buffer, pub, pattern) {
const rawMessage = this.parseMessage(buffer);
const packet = await this.deserializer.deserialize(rawMessage, { channel });
const redisCtx = new RedisContext([pattern, packet.metadata]);
if (isUndefined(packet.id)) {
return this.handleEvent(channel, packet, redisCtx);
}
// With wildcards, `channel` is the subscribed pattern (e.g. "users.*") and
// `pattern` is the concrete channel the request was published to. The
// client waits for the reply on the concrete one, so reply there.
const publish = this.getPublisher(pub, pattern, packet.id, redisCtx);
const handler = this.getHandlerByPattern(channel);
if (!handler) {
const status = 'error';
const noHandlerPacket = {
id: packet.id,
status,
err: NO_MESSAGE_HANDLER,
};
return publish(noHandlerPacket);
}
return this.handleRequest(redisCtx, async () => this.transformToObservable(await handler(packet.data, redisCtx)), publish);
}
getPublisher(pub, pattern, id, ctx) {
return (response) => {
Object.assign(response, { id });
const outgoingResponse = this.serializer.serialize(response);
return pub.publish(this.getReplyPattern(pattern), JSON.stringify(outgoingResponse));
};
}
parseMessage(content) {
try {
return JSON.parse(content);
}
catch (e) {
return content;
}
}
getRequestPattern(pattern) {
return pattern;
}
getReplyPattern(pattern) {
return `${pattern}.reply`;
}
registerErrorListener(client) {
client.on("error" /* RedisEventsMap.ERROR */, (err) => this.logger.error(err));
}
registerReconnectListener(client) {
client.on("reconnecting" /* RedisEventsMap.RECONNECTING */, () => {
if (this.isManuallyClosed) {
return;
}
this._status$.next("reconnecting" /* RedisStatus.RECONNECTING */);
if (this.wasInitialConnectionSuccessful) {
this.logger.log('Reconnecting to Redis...');
}
});
}
registerReadyListener(client) {
client.on("ready" /* RedisEventsMap.READY */, () => {
this._status$.next("connected" /* RedisStatus.CONNECTED */);
this.logger.log('Connected to Redis. Subscribing to channels...');
if (!this.wasInitialConnectionSuccessful) {
this.wasInitialConnectionSuccessful = true;
}
});
}
registerEndListener(client) {
client.on('end', () => {
if (this.isManuallyClosed) {
return;
}
this._status$.next("disconnected" /* RedisStatus.DISCONNECTED */);
this.logger.error('Disconnected from Redis. No further reconnection attempts will be made.');
});
}
getClientOptions() {
const retryStrategy = (times) => this.createRetryStrategy(times);
return {
...(this.options || {}),
retryStrategy,
};
}
createRetryStrategy(times) {
if (this.isManuallyClosed) {
return undefined;
}
if (!this.getOptionsProp(this.options, 'retryAttempts')) {
this.logger.error('Redis connection closed and retry attempts not specified');
return;
}
if (times > this.getOptionsProp(this.options, 'retryAttempts', 0)) {
this.logger.error(`Retry time exhausted`);
return;
}
return this.getOptionsProp(this.options, 'retryDelay', 5000);
}
unwrap() {
if (!this.pubClient || !this.subClient) {
throw new Error('Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.');
}
return [this.pubClient, this.subClient];
}
on(event, callback) {
if (this.subClient && this.pubClient) {
this.subClient.on(event, (...args) => callback('sub', ...args));
this.pubClient.on(event, (...args) => callback('pub', ...args));
}
else {
this.pendingEventListeners.push({ event, callback });
}
}
}