UNPKG

@graphql-hive/pubsub

Version:
147 lines (143 loc) 4.42 kB
'use strict'; var repeater = require('@repeaterjs/repeater'); var disposablestack = require('@whatwg-node/disposablestack'); var promiseHelpers = require('@whatwg-node/promise-helpers'); class NATSJetStreamPubSub { #disposed = false; #js; #subjectPrefix; #stream; #activeConsumers = /* @__PURE__ */ new Map(); constructor(js, options) { this.#js = js; this.#subjectPrefix = options.subjectPrefix; this.#stream = options.stream; if (String(this.#subjectPrefix || "").trim() === "") { throw new Error("NATSJetStreamPubSub requires a non-empty subjectPrefix"); } if (String(this.#stream || "").trim() === "") { throw new Error("NATSJetStreamPubSub requires a non-empty stream"); } } #topicToSubject(topic) { return `${this.#subjectPrefix}:${String(topic)}`; } /** Cursors are opaque outside of this adapter, they're simply the stream's message sequence. */ #parseCursor(cursor) { const seq = Number(cursor); if (!Number.isInteger(seq) || seq <= 0) { throw new Error(`Invalid cursor "${cursor}"`); } return seq; } async subscribedTopics() { const distinctTopics = []; for (const topic of this.#activeConsumers.values()) { if (!distinctTopics.includes(topic)) { distinctTopics.push(topic); } } return distinctTopics; } publish(topic, data) { if (this.#disposed) { throw new Error("PubSub is disposed, cannot publish data"); } return this.#js.publish(this.#topicToSubject(topic), JSON.stringify(data)).then(() => void 0); } /** * An ordered consumer is a fresh ephemeral consumer, exactly what we want for a resumable * per-subscription cursor: no shared/durable state between subscribers. */ #createConsumer(topic, cursor) { return this.#js.consumers.get(this.#stream, { filter_subjects: [this.#topicToSubject(topic)], ...cursor === void 0 ? { deliver_policy: "new" } : { deliver_policy: "by_start_sequence", opt_start_seq: this.#parseCursor(cursor) + 1 } }); } async #consume(topic, cursor, callback, finished, closed) { const consumer = await this.#createConsumer(topic, cursor); const messages = await consumer.consume({ callback }); const stop = async () => { if (this.#activeConsumers.delete(stop)) { try { await messages.close(); } finally { finished?.(); } } }; if (this.#disposed) { await messages.close(); finished?.(); throw new Error("PubSub is disposed"); } this.#activeConsumers.set(stop, topic); if (closed) { void messages.closed().then((error) => error ? closed.reject(error) : closed.resolve()); } return stop; } subscribe(topic, optionsOrListener) { if (this.#disposed) { throw new Error("PubSub is disposed, cannot subscribe to topics"); } if (typeof optionsOrListener === "function") { const listenerRef = { ref: optionsOrListener }; const stop = this.#consume(topic, void 0, (msg) => { try { listenerRef.ref?.(msg.json()); } catch { } }); return promiseHelpers.fakePromise(stop).then((stop2) => async () => { listenerRef.ref = null; await stop2(); }); } return new repeater.Repeater( async (push, stopped) => { const consumerClosed = promiseHelpers.createDeferredPromise(); const stop = await this.#consume( topic, optionsOrListener?.cursor, (msg) => { try { const item = { data: msg.json(), cursor: String(msg.seq) }; void push(item); } catch (error) { consumerClosed.reject(error); } }, stopped, consumerClosed ); try { await Promise.race([stopped, consumerClosed.promise]); } finally { consumerClosed.resolve(); await stop(); } } ); } async dispose() { this.#disposed = true; await Promise.all( Array.from(this.#activeConsumers.keys()).map((stop) => stop()) ); this.#activeConsumers.clear(); } [disposablestack.DisposableSymbols.asyncDispose]() { return this.dispose(); } } exports.NATSJetStreamPubSub = NATSJetStreamPubSub;