@shirayukikitsune/graphql-kafkajs-subscriptions
Version:
An implementation for the Apollo PubSubEngine using the KafkaJS as backend.
129 lines • 6.62 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KafkaPubSub = void 0;
const graphql_subscriptions_1 = require("graphql-subscriptions");
const kafkajs_1 = require("kafkajs");
const pubsub_async_iterator_1 = require("./pubsub-async-iterator");
const rxjs_1 = require("rxjs");
class KafkaPubSub extends graphql_subscriptions_1.PubSubEngine {
constructor(config) {
super();
this.config = config;
this.subscriptionMap = {};
this.subscriptionRefCount = {};
this.subscriptions = {};
this.idGenerator = KafkaPubSub.subscriptionIdGenerator();
this.kafka = new kafkajs_1.Kafka(config.global);
this.log = this.config.log;
}
async publish(triggerName, payload) {
var _a, _b, _c, _d, _e;
if (!this.producer) {
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug({ topic: triggerName }, 'Creating producer instance');
await this.createProducer();
(_b = this.log) === null || _b === void 0 ? void 0 : _b.debug({ topic: triggerName }, 'Producer instance created');
}
const message = typeof payload === 'string' || 'buffer' in payload ? { value: payload } : payload;
(_c = this.log) === null || _c === void 0 ? void 0 : _c.debug({ topic: triggerName }, 'Sending message');
const metadata = await ((_d = this.producer) === null || _d === void 0 ? void 0 : _d.send({
topic: triggerName,
messages: [message],
}));
(_e = this.log) === null || _e === void 0 ? void 0 : _e.debug({ metadata, topic: triggerName }, 'Message sent');
}
async subscribe(triggerName, onMessage, options) {
var _a, _b, _c;
await this.createConsumer();
const subscriptionId = this.idGenerator.next().value;
let subject;
if (!this.subscriptionMap[triggerName] || this.subscriptionMap[triggerName].closed) {
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug({ topic: triggerName, subscriptionId }, 'No consumers for topic. Creating new subject.');
subject = new rxjs_1.Subject();
this.subscriptionMap[triggerName] = subject;
this.subscriptionRefCount[triggerName] = 1;
(_b = this.log) === null || _b === void 0 ? void 0 : _b.info({ topic: triggerName, subscriptionId }, 'Subject created');
await this.connectConsumer({ topic: triggerName, fromBeginning: options === null || options === void 0 ? void 0 : options.fromBeginning });
}
else {
(_c = this.log) === null || _c === void 0 ? void 0 : _c.debug({ topic: triggerName, subscriptionId }, 'Subject found, reusing');
subject = this.subscriptionMap[triggerName];
this.subscriptionRefCount[triggerName]++;
}
this.subscriptions[subscriptionId] = [
subject.subscribe({
next: value => onMessage(value),
}),
triggerName,
];
return Promise.resolve(subscriptionId);
}
asyncIterator(triggers, options = {}) {
return new pubsub_async_iterator_1.PubSubAsyncIterator(this, triggers, options);
}
unsubscribe(subscriptionId) {
var _a, _b, _c;
(_a = this.log) === null || _a === void 0 ? void 0 : _a.info({ subscriptionId }, 'Closing subscription');
const [subscription, topic] = this.subscriptions[subscriptionId];
subscription.unsubscribe();
const log = (_b = this.log) === null || _b === void 0 ? void 0 : _b.child({ subscriptionId, topic }, true);
log === null || log === void 0 ? void 0 : log.info('Decreasing topic RefCount');
if (--this.subscriptionRefCount[topic] === 0) {
log === null || log === void 0 ? void 0 : log.debug('Deleting subscription RefCount');
delete this.subscriptionRefCount[topic];
log === null || log === void 0 ? void 0 : log.debug('Finding topic for subscription');
const ref = Object.entries(this.subscriptionMap).find(e => e[0] === topic);
if (ref) {
log === null || log === void 0 ? void 0 : log.debug('Closing subject');
ref[1].complete();
ref[1].unsubscribe();
log === null || log === void 0 ? void 0 : log.debug('Deleting subscription map entry');
delete this.subscriptionMap[ref[0]];
}
if (Object.keys(this.subscriptionRefCount).length === 0) {
log === null || log === void 0 ? void 0 : log.info('No subscribers left, pausing consumer');
(_c = this.consumer) === null || _c === void 0 ? void 0 : _c.stop();
}
}
}
async createProducer() {
this.producer = this.kafka.producer(this.config.producer);
await this.producer.connect();
}
async createConsumer() {
var _a, _b, _c;
if (this.consumer) {
return;
}
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug('Creating consumer instance');
this.consumer = this.kafka.consumer(this.config.consumer);
(_b = this.log) === null || _b === void 0 ? void 0 : _b.debug('Connecting consumer');
await this.consumer.connect();
(_c = this.log) === null || _c === void 0 ? void 0 : _c.debug('Consumer instance created');
}
async connectConsumer(options) {
var _a, _b, _c;
(_a = this.log) === null || _a === void 0 ? void 0 : _a.info('Connecting consumer');
await ((_b = this.consumer) === null || _b === void 0 ? void 0 : _b.subscribe(options));
(_c = this.consumer) === null || _c === void 0 ? void 0 : _c.run({
eachMessage: async (payload) => {
var _a;
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug(payload, 'Incoming message');
const subject = this.subscriptionMap[payload.topic];
if (subject && !subject.closed) {
if (payload.message.value) {
const data = JSON.parse(payload.message.value.toString());
subject.next(data);
}
}
},
});
}
static *subscriptionIdGenerator() {
let index = 0;
while (true) {
yield ++index;
}
}
}
exports.KafkaPubSub = KafkaPubSub;
//# sourceMappingURL=kafka-pub-sub.js.map