UNPKG

@graphql-hive/pubsub

Version:
141 lines (137 loc) 4.34 kB
'use strict'; var repeater = require('@repeaterjs/repeater'); var disposablestack = require('@whatwg-node/disposablestack'); var promiseHelpers = require('@whatwg-node/promise-helpers'); class RedisPubSub { #disposed = false; #quitOnDispose; #subscribers = /* @__PURE__ */ new Map(); #subscribersSetKey; #redis; #channelPrefix; #boundHandleMessage; constructor(redis, options) { this.#redis = redis; this.#channelPrefix = options.channelPrefix; if (String(this.#channelPrefix || "").trim() === "") { throw new Error("RedisPubSub requires a non-empty channelPrefix"); } this.#subscribersSetKey = `subscribers:${this.#channelPrefix}`; this.#quitOnDispose = !options.noQuitOnDispose; this.#boundHandleMessage = this.#handleMessage.bind(this); this.#redis.sub.on("message", this.#boundHandleMessage); } #topicToChannel(topic) { return `${this.#channelPrefix}:${String(topic)}`; } #topicFromChannel(channel) { return channel.replace(`${this.#channelPrefix}:`, ""); } async subscribedTopics() { if (this.#disposed) { throw new Error("PubSub is disposed, cannot get subscribed topics"); } const distinctTopics = Array.from(this.#subscribers.keys()); for (const otherTopic of await this.#redis.pub.smembers( this.#subscribersSetKey )) { if (!distinctTopics.includes(otherTopic)) { distinctTopics.push(otherTopic); } } return distinctTopics; } publish(topic, data) { if (this.#disposed) { throw new Error("PubSub is disposed, cannot publish data"); } return new Promise((resolve, reject) => { this.#redis.pub.publish( this.#topicToChannel(topic), JSON.stringify(data), (e) => { if (e) reject(e); else resolve(); } ); }); } #handleMessage(channel, message) { const topic = this.#topicFromChannel(channel); const data = JSON.parse(message); const listeners = this.#subscribers.get(topic); if (listeners) { for (const l of listeners.keys()) { l(data); } } } subscribe(topic, listener) { if (this.#disposed) { throw new Error("PubSub is disposed, cannot subscribe to topics"); } let listeners = this.#subscribers.get(topic); if (!listeners) { listeners = /* @__PURE__ */ new Map(); this.#subscribers.set(topic, listeners); } const setArgs = [ this.#subscribersSetKey, String(topic) ]; if (!listener) { return new repeater.Repeater(async (push, stop) => { await this.#redis.sub.subscribe(this.#topicToChannel(topic)); await this.#redis.pub.sadd(...setArgs); const { promise: settled, resolve: done } = promiseHelpers.createDeferredPromise(); listeners.set(push, () => (stop(), settled)); await stop; listeners.delete(push); if (listeners.size === 0) { this.#subscribers.delete(topic); } try { await this.#redis.pub.srem(...setArgs); await this.#redis.sub.unsubscribe(this.#topicToChannel(topic)); } finally { done(); } }); } const listenerRef = new WeakRef(listener); const unsubscribe = async () => { const l = listenerRef.deref(); if (l) listeners.delete(l); if (listeners.size === 0) { this.#subscribers.delete(topic); } await this.#redis.pub.srem(...setArgs); await this.#redis.sub.unsubscribe(this.#topicToChannel(topic)); }; listeners.set(listener, unsubscribe); return (async () => { await this.#redis.sub.subscribe(this.#topicToChannel(topic)); await this.#redis.pub.sadd(...setArgs); return unsubscribe; })(); } async dispose() { this.#disposed = true; this.#redis.sub.off("message", this.#boundHandleMessage); for (const sub of this.#subscribers.values()) { await Promise.all(Array.from(sub.values()).map((stop) => stop())); sub.clear(); } this.#subscribers.clear(); if (this.#quitOnDispose) { await Promise.allSettled([ this.#redis.pub.quit(), this.#redis.sub.quit() ]); } } [disposablestack.DisposableSymbols.asyncDispose]() { return this.dispose(); } } exports.RedisPubSub = RedisPubSub;