UNPKG

@graphql-hive/pubsub

Version:
443 lines (315 loc) 15.7 kB
# @graphql-hive/pubsub ## 2.2.1 ### Patch Changes - [`96a19f2`](https://github.com/graphql-hive/gateway/commit/96a19f2dc63be7ff1c9a1c6d2d239d153b82153b) Thanks [@enisdenjo](https://github.com/enisdenjo)! - Accept a `JetStreamClient` in `NATSJetStreamPubSub` so the optional NATS dependencies remain type-only imports ```diff -import { NATSJetStreamPubSub } from '@graphql-hive/pubsub/nats-jetstream'; +import { NATSJetStreamPubSub } from '@graphql-hive/pubsub/nats-jetstream'; +import { jetstream } from '@nats-io/jetstream'; -const pubsub = new NATSJetStreamPubSub(nats, options); +const pubsub = new NATSJetStreamPubSub(jetstream(nats), options); ``` ## 2.2.0 ### Minor Changes - [#2473](https://github.com/graphql-hive/gateway/pull/2473) [`6f2c3b6`](https://github.com/graphql-hive/gateway/commit/6f2c3b64b05f6ba17928fd098915c2167f3daedc) Thanks [@enisdenjo](https://github.com/enisdenjo)! - Add `NATSJetStreamPubSub`, a NATS JetStream transport with optional cursor-based replay Subscribe like any other Hive PubSub, or pass subscribe options to receive each message with an opaque `cursor`. Pass that cursor on a later subscription to resume right after it and recover events missed while disconnected. Without a cursor, only messages published after the subscription starts are delivered. The JetStream stream is not created or configured by this transport; it must already exist and capture the subjects used by the pubsub (`${subjectPrefix}:${topic}`). ```ts import { connect } from '@nats-io/transport-node'; import { NATSJetStreamPubSub } from '@graphql-hive/pubsub/nats-jetstream'; const nats = await connect({ servers: 'localhost:4222' }); const pubsub = new NATSJetStreamPubSub(nats, { subjectPrefix: 'my-service', stream: 'EVENTS', }); // live-only: no cursor, only messages published after subscribe starts const live = pubsub.subscribe('personCreated', { cursor: undefined }); const first = await live.next(); // first.value => { data: { id: '1' }, cursor: '...' } await live.return?.(); // published while disconnected is retained by JetStream await pubsub.publish('personCreated', { id: '2' }); // resume right after the last seen cursor — gets { id: '2' }, not a repeat of '1' for await (const { data, cursor } of pubsub.subscribe('personCreated', { cursor: first.value.cursor, })) { // persist `cursor` so the next reconnect can resume again } ``` ### Patch Changes - [#2473](https://github.com/graphql-hive/gateway/pull/2473) [`6f2c3b6`](https://github.com/graphql-hive/gateway/commit/6f2c3b64b05f6ba17928fd098915c2167f3daedc) Thanks [@enisdenjo](https://github.com/enisdenjo)! - dependencies updates: - Added dependency [`@nats-io/jetstream@^3` ↗︎](https://www.npmjs.com/package/@nats-io/jetstream/v/3.0.0) (to `peerDependencies`) ## 2.1.1 ### Patch Changes - [#1473](https://github.com/graphql-hive/gateway/pull/1473) [`838ffec`](https://github.com/graphql-hive/gateway/commit/838ffecb2ad3d4ef6bbb65607a56302cb45e2f14) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - Updated dependency [`@whatwg-node/promise-helpers@^1.3.2` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.3.2) (from `^1.3.0`, in `dependencies`) ## 2.1.0 ### Minor Changes - [#1441](https://github.com/graphql-hive/gateway/pull/1441) [`2b3946f`](https://github.com/graphql-hive/gateway/commit/2b3946f418b0fb018ca792ff6a2c14fef7abb01d) Thanks [@enisdenjo](https://github.com/enisdenjo)! - Close the client connection on NATS and Redis pubsubs on dispose This will gracefully dispose the pubsub on gateway shutdown. There is an option to disable this behaviour `noCloseOnDispose` and `noQuitOnDispose` respectively. ## 2.0.0 ### Major Changes - [#956](https://github.com/graphql-hive/gateway/pull/956) [`46d2661`](https://github.com/graphql-hive/gateway/commit/46d26615c2c3c5f936c1d1bca1d03b025c1ce86a) Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - Drop Node 18 support Least supported Node version is now v20. - [#956](https://github.com/graphql-hive/gateway/pull/956) [`46d2661`](https://github.com/graphql-hive/gateway/commit/46d26615c2c3c5f936c1d1bca1d03b025c1ce86a) Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - Complete API redesign with async support and distributed Redis PubSub ## Redesigned interface ```ts import type { DisposableSymbols } from '@whatwg-node/disposablestack'; import type { MaybePromise } from '@whatwg-node/promise-helpers'; export type TopicDataMap = { [topic: string]: any /* data */ }; export type PubSubListener< Data extends TopicDataMap, Topic extends keyof Data, > = (data: Data[Topic]) => void; export interface PubSub<M extends TopicDataMap = TopicDataMap> { /** * Publish {@link data} for a {@link topic}. * @returns `void` or a `Promise` that resolves when the data has been successfully published */ publish<Topic extends keyof M>( topic: Topic, data: M[Topic], ): MaybePromise<void>; /** * A distinct list of all topics that are currently subscribed to. * Can be a promise to accomodate distributed systems where subscribers exist on other * locations and we need to know about all of them. */ subscribedTopics(): MaybePromise<Iterable<keyof M>>; /** * Subscribe and listen to a {@link topic} receiving its data. * * If the {@link listener} is provided, it will be called whenever data is emitted for the {@link topic}, * * @returns an unsubscribe function or a `Promise<unsubscribe function>` that resolves when the subscription is successfully established. the unsubscribe function returns `void` or a `Promise` that resolves on successful unsubscribe and subscription cleanup * * If the {@link listener} is not provided, * * @returns an `AsyncIterable` that yields data for the given {@link topic} */ subscribe<Topic extends keyof M>(topic: Topic): AsyncIterable<M[Topic]>; subscribe<Topic extends keyof M>( topic: Topic, listener: PubSubListener<M, Topic>, ): MaybePromise<() => MaybePromise<void>>; /** * Closes active subscriptions and disposes of all resources. Publishing and subscribing after disposal * is not possible and will throw an error if attempted. */ dispose(): MaybePromise<void>; /** @see {@link dispose} */ [DisposableSymbols.asyncDispose](): Promise<void>; } ``` ## New `NATSPubSub` for a NATS-powered pubsub ```sh npm i @nats-io/transport-node ``` ```ts filename="gateway.config.ts" import { defineConfig } from '@graphql-hive/gateway'; import { NATSPubSub } from '@graphql-hive/pubsub/nats'; import { connect } from '@nats-io/transport-node'; export const gatewayConfig = defineConfig({ maskedErrors: false, pubsub: new NATSPubSub( await connect(), { // we make sure to use the same prefix for all gateways to share the same channels and pubsub. // meaning, all gateways using this channel prefix will receive and publish to the same topics subjectPrefix: 'my-app', }, ), }); ``` ## New `RedisPubSub` for a Redis-powered pubsub ```sh npm i ioredis ``` ```ts import { RedisPubSub } from '@graphql-hive/pubsub/redis'; import Redis from 'ioredis'; /** * When a Redis connection enters "subscriber mode" (after calling SUBSCRIBE), it can only execute * subscriber commands (SUBSCRIBE, UNSUBSCRIBE, etc.). Meaning, it cannot execute other commands like PUBLISH. * To avoid this, we use two separate Redis clients: one for publishing and one for subscribing. */ const pub = new Redis(); const sub = new Redis(); const pubsub = new RedisPubSub( { pub, sub }, // if the chanel prefix is the shared between services, the topics will be shared as well // this means that if you have multiple services using the same channel prefix, they will // receive each other's messages { channelPrefix: 'my-app' } ); ``` ## Migrating The main migration effort involves: 1. Updating import statements 2. Adding `await` to async operations 3. Replacing subscription ID pattern with unsubscribe functions 4. Replacing `asyncIterator()` with overloaded `subscribe()` 5. Choosing between `MemPubSub` and `RedisPubSub` implementations 6. Using the `PubSub` interface instead of `HivePubSub` Before: ```typescript import { PubSub, HivePubSub } from '@graphql-hive/pubsub'; interface TopicMap { userCreated: { id: string; name: string }; orderPlaced: { orderId: string; amount: number }; } const pubsub: HivePubSub<TopicMap> = new PubSub(); // Subscribe const subId = pubsub.subscribe('userCreated', (user) => { console.log('User created:', user.name); }); // Publish pubsub.publish('userCreated', { id: '1', name: 'John' }); // Async iteration (async () => { for await (const order of pubsub.asyncIterator('orderPlaced')) { console.log('Order placed:', order.orderId); } })(); // Get topics const topics = pubsub.getEventNames(); // Unsubcribe pubsub.unsubscribe(subId); // Dispose/destroy the pubsub pubsub.dispose(); ``` After: ```typescript import { MemPubSub, PubSub } from '@graphql-hive/pubsub'; interface TopicMap { userCreated: { id: string; name: string }; orderPlaced: { orderId: string; amount: number }; } const pubsub: PubSub<TopicMap> = new MemPubSub(); // Subscribe const unsubscribe = await pubsub.subscribe('userCreated', (user) => { console.log('User created:', user.name); }); // Publish await pubsub.publish('userCreated', { id: '1', name: 'John' }); // Async iteration (async () => { for await (const order of pubsub.subscribe('orderPlaced')) { console.log('Order placed:', order.orderId); } })(); // Get topics const topics = await pubsub.subscribedTopics(); // Unsubscribe await unsubscribe(); // Dispose/destroy the pubsub await pubsub.dispose(); ``` ### Interface renamed from `HivePubSub` to just `PubSub` ```diff - import { HivePubSub } from '@graphql-hive/pubsub'; + import { PubSub } from '@graphql-hive/pubsub'; ``` ### `subscribedTopics()` method signature change This method is now required and supports async operations. ```diff - subscribedTopics?(): Iterable; // Optional + subscribedTopics(): MaybePromise<Iterable>; // Required, supports async ``` ### `publish()` method signature change Publishing can now be async and may return a promise. ```diff - publish<Topic extends keyof Data>(topic: Topic, data: Data[Topic]): void; + publish<Topic extends keyof M>(topic: Topic, data: M[Topic]): MaybePromise<void>; ``` Migrating existing code: ```diff - pubsub.publish('topic', data); + await pubsub.publish('topic', data); ``` ### `subscribe()` method signature change Subscribe now returns an unsubscribe function instead of a subscription ID. ```diff subscribe<Topic extends keyof Data>( topic: Topic, listener: PubSubListener<Data, Topic> - ): number; // Returns subscription ID + ): MaybePromise<() => MaybePromise<void>>; // Returns unsubscribe function ``` Migrating existing code: ```diff - const subId = pubsub.subscribe('topic', (data) => { - console.log(data); - }); - pubsub.unsubscribe(subId); + const unsubscribe = await pubsub.subscribe('topic', (data) => { + console.log(data); + }); + await unsubscribe(); ``` ### `dispose()` method signature change Disposal is now required and supports async operations. ```diff - dispose?(): void; // Optional + dispose(): MaybePromise<void>; // Required, supports async ``` Migrating existing code: ```diff - pubsub.dispose(); + await pubsub.dispose(); ``` ### Removed `getEventNames()` method This deprecated method was removed. Use `subscribedTopics()` instead. ```diff - getEventNames(): Iterable<keyof Data>; ``` Migrating existing code: ```diff - const topics = pubsub.getEventNames(); + const topics = await pubsub.subscribedTopics(); ``` ### Removed `unsubscribe()` method The centralized unsubscribe method was removed. Each subscription now returns its own unsubscribe function. ```diff - unsubscribe(subId: number): void; ``` Migrating existing code by using the unsubscribe function returned by `subscribe()` instead: ```diff - const subId = pubsub.subscribe('topic', listener); - pubsub.unsubscribe(subId); + const unsubscribe = await pubsub.subscribe('topic', listener); + await unsubscribe(); ``` ### Removed `asyncIterator()` Method The separate async iterator method was removed. Call `subscribe()` without a listener to get an async iterable. ```diff - asyncIterator<Topic extends keyof Data>(topic: Topic): AsyncIterable<Data[Topic]>; ``` Migrating existing code: ```diff - for await (const data of pubsub.asyncIterator('topic')) { + for await (const data of pubsub.subscribe('topic')) { console.log(data); } ``` ### `MemPubSub` is the in-memory pubsub implementation The generic `PubSub` class was replaced with implementation specific `MemPubSub` for an in-memory pubsub. ```diff - import { PubSub } from '@graphql-hive/pubsub'; - const pubsub = new PubSub(); + import { MemPubSub } from '@graphql-hive/pubsub'; + const pubsub = new MemPubSub(); ``` ### Patch Changes - [#956](https://github.com/graphql-hive/gateway/pull/956) [`46d2661`](https://github.com/graphql-hive/gateway/commit/46d26615c2c3c5f936c1d1bca1d03b025c1ce86a) Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - dependencies updates: - Added dependency [`@whatwg-node/promise-helpers@^1.3.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.3.0) (to `dependencies`) - Added dependency [`@nats-io/nats-core@^3` ↗︎](https://www.npmjs.com/package/@nats-io/nats-core/v/3.0.0) (to `peerDependencies`) - Added dependency [`ioredis@^5` ↗︎](https://www.npmjs.com/package/ioredis/v/5.0.0) (to `peerDependencies`) - [#956](https://github.com/graphql-hive/gateway/pull/956) [`46d2661`](https://github.com/graphql-hive/gateway/commit/46d26615c2c3c5f936c1d1bca1d03b025c1ce86a) Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - dependencies updates: - Added dependency [`@whatwg-node/promise-helpers@^1.3.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.3.0) (to `dependencies`) - Added dependency [`@nats-io/nats-core@^3` ↗︎](https://www.npmjs.com/package/@nats-io/nats-core/v/3.0.0) (to `peerDependencies`) - Added dependency [`ioredis@^5` ↗︎](https://www.npmjs.com/package/ioredis/v/5.0.0) (to `peerDependencies`) - [#956](https://github.com/graphql-hive/gateway/pull/956) [`46d2661`](https://github.com/graphql-hive/gateway/commit/46d26615c2c3c5f936c1d1bca1d03b025c1ce86a) Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - Export TopicDataMap type for easier external implementations ## 1.0.0 ### Major Changes - [#933](https://github.com/graphql-hive/gateway/pull/933) [`a374bfc`](https://github.com/graphql-hive/gateway/commit/a374bfcf4309f5953b8c8304fba8e079b6f6b6dc) Thanks [@enisdenjo](https://github.com/enisdenjo)! - Introduce Hive Gateway PubSub with hardened memory safety