From d37084361f5b4a98663b43f4f7483911a5a02108 Mon Sep 17 00:00:00 2001 From: hwillson Date: Thu, 25 Nov 2021 12:12:14 -0500 Subject: [PATCH 1/9] Release 3.0 CHANGELOG placeholder Initial CHANGELOG placeholder for release 3.0. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42ee7f..0f96441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 3.0.0 (not yet released) + +- TODO + ### 2.0.1 (not yet released) - `withFilter` TypeScript improvements.
From 8e8d511beb2e20ec3ed50be2a95f51a751109841 Mon Sep 17 00:00:00 2001 From: "[Cursors]" <65373746+cursorsdottsx@users.noreply.github.com> Date: Thu, 25 Nov 2021 11:39:05 -0600 Subject: [PATCH 2/9] Add a(n optional) generic type map to PubSub. (#245) * Add a(n optional) generic type map to PubSub. The class PubSub now has a generic type parameter so its methods `publish` and `subscribe` can be **optionally** type-checked by TypeScript. * Added part for PubSub generic. * Slight README tweaks to align formatting / adjust grammar a bit * Changelog update Co-authored-by: hwillson --- CHANGELOG.md | 3 ++- README.md | 28 ++++++++++++++++++++++++---- src/pubsub.ts | 14 +++++++++++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f96441..6c2ee11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ### 3.0.0 (not yet released) -- TODO +- Add an optional generic type map to `PubSub`.
+ [@cursorsdottsx](https://github.com/cursorsdottsx) in [#245](https://github.com/apollographql/graphql-subscriptions/pull/245) ### 2.0.1 (not yet released) diff --git a/README.md b/README.md index bc063f2..8b5a309 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ To begin with GraphQL subscriptions, start by defining a GraphQL `Subscription` ```graphql type Subscription { - somethingChanged: Result + somethingChanged: Result } type Result { - id: String + id: String } ``` @@ -52,7 +52,27 @@ import { PubSub } from 'graphql-subscriptions'; export const pubsub = new PubSub(); ``` -Now, implement your Subscriptions type resolver, using the `pubsub.asyncIterator` to map the event you need: +If you're using TypeScript you can use the optional generic parameter for added type-safety: + +```ts +import { PubSub } from "apollo-server-express"; + +const pubsub = new PubSub<{ + EVENT_ONE: { data: number; }; + EVENT_TWO: { data: string; }; +}>(); + +pubsub.publish("EVENT_ONE", { data: 42 }); +pubsub.publish("EVENTONE", { data: 42 }); // ! ERROR +pubsub.publish("EVENT_ONE", { data: "42" }); // ! ERROR +pubsub.publish("EVENT_TWO", { data: "hello" }); + +pubsub.subscribe("EVENT_ONE", () => {}); +pubsub.subscribe("EVENTONE", () => {}); // ! ERROR +pubsub.subscribe("EVENT_TWO", () => {}); +``` + +Next implement your Subscriptions type resolver using the `pubsub.asyncIterator` to map the event you need: ```js const SOMETHING_CHANGED_TOPIC = 'something_changed'; @@ -68,7 +88,7 @@ export const resolvers = { > Subscriptions resolvers are not a function, but an object with `subscribe` method, that returns `AsyncIterable`. -Now, the GraphQL engine knows that `somethingChanged` is a subscription, and every time we use `pubsub.publish` over this topic - it will publish it using the transport we use: +The GraphQL engine now knows that `somethingChanged` is a subscription, and every time we use `pubsub.publish` it will publish content using our chosen transport layer: ```js pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: "123" }}); diff --git a/src/pubsub.ts b/src/pubsub.ts index 7972369..1525efa 100644 --- a/src/pubsub.ts +++ b/src/pubsub.ts @@ -5,7 +5,9 @@ export interface PubSubOptions { eventEmitter?: EventEmitter; } -export class PubSub extends PubSubEngine { +export class PubSub< + Events extends { [event: string]: unknown } = Record +> extends PubSubEngine { protected ee: EventEmitter; private subscriptions: { [key: string]: [string, (...args: any[]) => void] }; private subIdCounter: number; @@ -17,12 +19,18 @@ export class PubSub extends PubSubEngine { this.subIdCounter = 0; } - public publish(triggerName: string, payload: any): Promise { + public publish( + triggerName: K & string, + payload: Events[K] extends never ? any : Events[K] + ): Promise { this.ee.emit(triggerName, payload); return Promise.resolve(); } - public subscribe(triggerName: string, onMessage: (...args: any[]) => void): Promise { + public subscribe( + triggerName: K & string, + onMessage: (...args: any[]) => void + ): Promise { this.ee.addListener(triggerName, onMessage); this.subIdCounter = this.subIdCounter + 1; this.subscriptions[this.subIdCounter] = [triggerName, onMessage]; From e27eb2e97b92684079952aaddb7c8d1ed0ab3677 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 25 Nov 2021 18:52:49 +0100 Subject: [PATCH 3/9] feat: replace iterall with native Symbol.asyncIterator + fix return types (#232) * fix(typings): return AsyncIterableIterator instead of AsyncIterator BREAKING fixes the type annotation of the abstract class PubSubEngine. According to the TypeScript type-defintion a `PubSubAsyncIterator` instance is actually a `AsyncIterableIterator` instead of an `AsyncIterator`. The typing of `PubSubAsyncIterator` is way more convenient as it allows iterating over it with the `for await (const foo of iterator) { doSth() }` syntax, which is super handy for filtering or mapping (See https://gist.github.com/n1ru4l/127178705cc0942cad0e45d425e2eb63 for some example operators). * remove iterall * rename asyncIterator method to asyncIterableIterator. * Slight tweaks based on graphql-js 16 changes * Changelog update Co-authored-by: hwillson --- CHANGELOG.md | 2 + package.json | 4 +- ...r.ts => pubsub-async-iterable-iterator.ts} | 6 +-- src/pubsub-engine.ts | 6 +-- src/test/asyncIteratorSubscription.ts | 51 ++++++++++--------- src/test/tests.ts | 19 ++++--- src/with-filter.ts | 7 ++- 7 files changed, 49 insertions(+), 46 deletions(-) rename src/{pubsub-async-iterator.ts => pubsub-async-iterable-iterator.ts} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2ee11..bbca927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Add an optional generic type map to `PubSub`.
[@cursorsdottsx](https://github.com/cursorsdottsx) in [#245](https://github.com/apollographql/graphql-subscriptions/pull/245) +- Replace `iterall` use with native `Symbol.asyncIterator`.
+ [@n1ru4l](https://github.com/n1ru4l) in [#232](https://github.com/apollographql/graphql-subscriptions/pull/232) ### 2.0.1 (not yet released) diff --git a/package.json b/package.json index 57596f1..124685c 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,7 @@ "type": "git", "url": "https://github.com/apollostack/graphql-subscriptions.git" }, - "dependencies": { - "iterall": "^1.3.0" - }, + "dependencies": {}, "peerDependencies": { "graphql": "^15.7.2 || ^16.0.0" }, diff --git a/src/pubsub-async-iterator.ts b/src/pubsub-async-iterable-iterator.ts similarity index 96% rename from src/pubsub-async-iterator.ts rename to src/pubsub-async-iterable-iterator.ts index 31bca41..9284902 100644 --- a/src/pubsub-async-iterator.ts +++ b/src/pubsub-async-iterable-iterator.ts @@ -1,4 +1,3 @@ -import { $$asyncIterator } from 'iterall'; import { PubSubEngine } from './pubsub-engine'; /** @@ -33,7 +32,7 @@ import { PubSubEngine } from './pubsub-engine'; * @property pubsub @type {PubSubEngine} * The PubSubEngine whose events will be observed. */ -export class PubSubAsyncIterator implements AsyncIterator { +export class PubSubAsyncIterableIterator implements AsyncIterableIterator { private pullQueue: ((value: IteratorResult) => void)[]; private pushQueue: T[]; @@ -66,7 +65,7 @@ export class PubSubAsyncIterator implements AsyncIterator { return Promise.reject(error); } - public [$$asyncIterator]() { + public [Symbol.asyncIterator]() { return this; } @@ -119,5 +118,4 @@ export class PubSubAsyncIterator implements AsyncIterator { this.pubsub.unsubscribe(subscriptionId); } } - } diff --git a/src/pubsub-engine.ts b/src/pubsub-engine.ts index afe18d7..1b0575e 100644 --- a/src/pubsub-engine.ts +++ b/src/pubsub-engine.ts @@ -1,10 +1,10 @@ -import {PubSubAsyncIterator} from './pubsub-async-iterator'; +import {PubSubAsyncIterableIterator} from './pubsub-async-iterable-iterator'; export abstract class PubSubEngine { public abstract publish(triggerName: string, payload: any): Promise; public abstract subscribe(triggerName: string, onMessage: Function, options: Object): Promise; public abstract unsubscribe(subId: number); - public asyncIterator(triggers: string | string[]): AsyncIterator { - return new PubSubAsyncIterator(this, triggers); + public asyncIterableIterator(triggers: string | string[]): PubSubAsyncIterableIterator { + return new PubSubAsyncIterableIterator(this, triggers); } } diff --git a/src/test/asyncIteratorSubscription.ts b/src/test/asyncIteratorSubscription.ts index 81d98d4..d0f669d 100644 --- a/src/test/asyncIteratorSubscription.ts +++ b/src/test/asyncIteratorSubscription.ts @@ -5,11 +5,14 @@ import * as chaiAsPromised from 'chai-as-promised'; import { spy } from 'sinon'; import * as sinonChai from 'sinon-chai'; -import { createAsyncIterator, isAsyncIterable } from 'iterall'; import { PubSub } from '../pubsub'; import { withFilter, FilterFn } from '../with-filter'; import { ExecutionResult } from 'graphql'; +const isAsyncIterableIterator = (input: unknown): input is AsyncIterableIterator => { + return input != null && typeof input[Symbol.asyncIterator] === 'function'; +}; + chai.use(chaiAsPromised); chai.use(sinonChai); const expect = chai.expect; @@ -64,14 +67,13 @@ describe('GraphQL-JS asyncIterator', () => { } `); const pubsub = new PubSub(); - const origIterator = pubsub.asyncIterator(FIRST_EVENT); + const origIterator = pubsub.asyncIterableIterator(FIRST_EVENT); const schema = buildSchema(origIterator); - - const results = await subscribe({schema, document: query}) as AsyncIterator; + const results = await subscribe({ schema, document: query }) as AsyncIterableIterator; const payload1 = results.next(); - expect(isAsyncIterable(results)).to.be.true; + expect(isAsyncIterableIterator(results)).to.be.true; const r = payload1.then(res => { expect(res.value.data.testSubscription).to.equal('FIRST_EVENT'); @@ -90,13 +92,13 @@ describe('GraphQL-JS asyncIterator', () => { } `); const pubsub = new PubSub(); - const origIterator = pubsub.asyncIterator(FIRST_EVENT); + const origIterator = pubsub.asyncIterableIterator(FIRST_EVENT); const schema = buildSchema(origIterator, () => Promise.resolve(true)); - const results = await subscribe({schema, document: query}) as AsyncIterator; + const results = await subscribe({ schema, document: query }) as AsyncIterableIterator; const payload1 = results.next(); - expect(isAsyncIterable(results)).to.be.true; + expect(isAsyncIterableIterator(results)).to.be.true; const r = payload1.then(res => { expect(res.value.data.testSubscription).to.equal('FIRST_EVENT'); @@ -115,7 +117,7 @@ describe('GraphQL-JS asyncIterator', () => { `); const pubsub = new PubSub(); - const origIterator = pubsub.asyncIterator(FIRST_EVENT); + const origIterator = pubsub.asyncIterableIterator(FIRST_EVENT); let counter = 0; @@ -133,8 +135,8 @@ describe('GraphQL-JS asyncIterator', () => { const schema = buildSchema(origIterator, filterFn); - subscribe({schema, document: query}).then((results: AsyncGenerator | ExecutionResult) => { - expect(isAsyncIterable(results)).to.be.true; + Promise.resolve(subscribe({ schema, document: query })).then((results: AsyncIterableIterator | ExecutionResult) => { + expect(isAsyncIterableIterator(results)).to.be.true; (results as AsyncGenerator).next(); (results as AsyncGenerator).return(); @@ -155,7 +157,7 @@ describe('GraphQL-JS asyncIterator', () => { `); const pubsub = new PubSub(); - const origIterator = pubsub.asyncIterator(FIRST_EVENT); + const origIterator = pubsub.asyncIterableIterator(FIRST_EVENT); const returnSpy = spy(origIterator, 'return'); const schema = buildSchema(origIterator); @@ -172,20 +174,21 @@ describe('GraphQL-JS asyncIterator', () => { }); }); -describe('withFilter', () => { - - it('works properly with finite asyncIterators', async () => { - const isEven = (x: number) => x % 2 === 0; +function isEven(x: number) { + if (x === undefined) { + throw Error('Undefined value passed to filterFn'); + } + return x % 2 === 0; +} - const testFiniteAsyncIterator: AsyncIterator = createAsyncIterator([1, 2, 3, 4, 5, 6, 7, 8]); - // Work around https://github.com/leebyron/iterall/issues/48 - testFiniteAsyncIterator.throw = function (error) { - return Promise.reject(error); - }; - testFiniteAsyncIterator.return = function () { - return Promise.resolve({ value: undefined, done: true }); - }; +const testFiniteAsyncIterator: AsyncIterableIterator = (async function * () { + for (const value of [1, 2, 3, 4, 5, 6, 7, 8]) { + yield value; + } +})(); +describe('withFilter', () => { + it('works properly with finite asyncIterators', async () => { const filteredAsyncIterator = withFilter(() => testFiniteAsyncIterator, isEven)(); for (let i = 1; i <= 4; i++) { diff --git a/src/test/tests.ts b/src/test/tests.ts index 5190b31..a84d605 100644 --- a/src/test/tests.ts +++ b/src/test/tests.ts @@ -7,7 +7,10 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as sinonChai from 'sinon-chai'; import { PubSub } from '../pubsub'; -import { isAsyncIterable } from 'iterall'; + +const isAsyncIterableIterator = (input: unknown): input is AsyncIterableIterator => { + return input != null && typeof input[Symbol.asyncIterator] === 'function'; +}; chai.use(chaiAsPromised); chai.use(sinonChai); @@ -37,15 +40,15 @@ describe('AsyncIterator', () => { it('should expose valid asyncIterator for a specific event', () => { const eventName = 'test'; const ps = new PubSub(); - const iterator = ps.asyncIterator(eventName); + const iterator = ps.asyncIterableIterator(eventName); expect(iterator).to.not.be.undefined; - expect(isAsyncIterable(iterator)).to.be.true; + expect(isAsyncIterableIterator(iterator)).to.be.true; }); it('should trigger event on asyncIterator when published', done => { const eventName = 'test'; const ps = new PubSub(); - const iterator = ps.asyncIterator(eventName); + const iterator = ps.asyncIterableIterator(eventName); iterator.next().then(result => { expect(result).to.not.be.undefined; @@ -60,7 +63,7 @@ describe('AsyncIterator', () => { it('should not trigger event on asyncIterator when publishing other event', () => { const eventName = 'test2'; const ps = new PubSub(); - const iterator = ps.asyncIterator('test'); + const iterator = ps.asyncIterableIterator('test'); const spy = sinon.spy(); iterator.next().then(spy); @@ -71,7 +74,7 @@ describe('AsyncIterator', () => { it('register to multiple events', done => { const eventName = 'test2'; const ps = new PubSub(); - const iterator = ps.asyncIterator(['test', 'test2']); + const iterator = ps.asyncIterableIterator(['test', 'test2']); const spy = sinon.spy(); iterator.next().then(() => { @@ -85,7 +88,7 @@ describe('AsyncIterator', () => { it('should not trigger event on asyncIterator already returned', done => { const eventName = 'test'; const ps = new PubSub(); - const iterator = ps.asyncIterator(eventName); + const iterator = ps.asyncIterableIterator(eventName); iterator.next().then(result => { expect(result).to.deep.equal({ @@ -117,7 +120,7 @@ describe('AsyncIterator', () => { } } const ps = new TestPubSub(); - ps.asyncIterator(testEventName); + ps.asyncIterableIterator(testEventName); expect(ps.listenerCount(testEventName)).to.equal(0); }); diff --git a/src/with-filter.ts b/src/with-filter.ts index 2403a73..f5c93e5 100644 --- a/src/with-filter.ts +++ b/src/with-filter.ts @@ -1,10 +1,9 @@ -import { $$asyncIterator } from 'iterall'; export type FilterFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => boolean | Promise; export type ResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterator; -interface IterallAsyncIterator extends AsyncIterator { - [$$asyncIterator](): IterallAsyncIterator; +interface IterallAsyncIterator extends AsyncIterableIterator { + [Symbol.asyncIterator](): IterallAsyncIterator; } export type WithFilter = ( @@ -63,7 +62,7 @@ export function withFilter( throw(error) { return asyncIterator.throw(error); }, - [$$asyncIterator]() { + [Symbol.asyncIterator]() { return this; }, }; From 28934afcd66cfc7c09bd3512a79a683b36bce073 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 25 Nov 2021 18:18:45 +0000 Subject: [PATCH 4/9] fix: Support readonly arrays as event names (#234) * Failing case: pubsub asyncIterator should accept a readonly string[] * fix: Allow readonly triggers to be passed to PubSubEngine.asyncIterator * Changelog update Co-authored-by: hwillson --- CHANGELOG.md | 2 ++ src/pubsub-async-iterable-iterator.ts | 6 +++--- src/pubsub-engine.ts | 2 +- src/test/tests.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbca927..ea990db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ [@cursorsdottsx](https://github.com/cursorsdottsx) in [#245](https://github.com/apollographql/graphql-subscriptions/pull/245) - Replace `iterall` use with native `Symbol.asyncIterator`.
[@n1ru4l](https://github.com/n1ru4l) in [#232](https://github.com/apollographql/graphql-subscriptions/pull/232) +- Support `readonly` arrays of event names.
+ [@rh389](https://github.com/rh389) in [#234](https://github.com/apollographql/graphql-subscriptions/pull/234) ### 2.0.1 (not yet released) diff --git a/src/pubsub-async-iterable-iterator.ts b/src/pubsub-async-iterable-iterator.ts index 9284902..3e0f99e 100644 --- a/src/pubsub-async-iterable-iterator.ts +++ b/src/pubsub-async-iterable-iterator.ts @@ -16,7 +16,7 @@ import { PubSubEngine } from './pubsub-engine'; * A queue of PubSubEngine events waiting for next() calls to be made, which returns the queued events * for handling. This queue expands as PubSubEngine events arrive without next() calls occurring in-between. * - * @property eventsArray @type {string[]} + * @property eventsArray @type {readonly string[]} * An array of PubSubEngine event names that this PubSubAsyncIterator should watch. * * @property allSubscribed @type {Promise} @@ -36,12 +36,12 @@ export class PubSubAsyncIterableIterator implements AsyncIterableIterator private pullQueue: ((value: IteratorResult) => void)[]; private pushQueue: T[]; - private eventsArray: string[]; + private eventsArray: readonly string[]; private allSubscribed: Promise; private running: boolean; private pubsub: PubSubEngine; - constructor(pubsub: PubSubEngine, eventNames: string | string[]) { + constructor(pubsub: PubSubEngine, eventNames: string | readonly string[]) { this.pubsub = pubsub; this.pullQueue = []; this.pushQueue = []; diff --git a/src/pubsub-engine.ts b/src/pubsub-engine.ts index 1b0575e..06b984a 100644 --- a/src/pubsub-engine.ts +++ b/src/pubsub-engine.ts @@ -4,7 +4,7 @@ export abstract class PubSubEngine { public abstract publish(triggerName: string, payload: any): Promise; public abstract subscribe(triggerName: string, onMessage: Function, options: Object): Promise; public abstract unsubscribe(subId: number); - public asyncIterableIterator(triggers: string | string[]): PubSubAsyncIterableIterator { + public asyncIterableIterator(triggers: string | readonly string[]): PubSubAsyncIterableIterator { return new PubSubAsyncIterableIterator(this, triggers); } } diff --git a/src/test/tests.ts b/src/test/tests.ts index a84d605..a1e009a 100644 --- a/src/test/tests.ts +++ b/src/test/tests.ts @@ -74,7 +74,7 @@ describe('AsyncIterator', () => { it('register to multiple events', done => { const eventName = 'test2'; const ps = new PubSub(); - const iterator = ps.asyncIterableIterator(['test', 'test2']); + const iterator = ps.asyncIterator(['test', 'test2'] as const); const spy = sinon.spy(); iterator.next().then(() => { From 519904302323076d5ed2e05c53c8f3603c4a7ce7 Mon Sep 17 00:00:00 2001 From: hwillson Date: Thu, 25 Nov 2021 13:29:43 -0500 Subject: [PATCH 5/9] Start highlighting breaking changes in the CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea990db..0f34fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ ### 3.0.0 (not yet released) +- **[BREAKING]** Replace `iterall` use with native `Symbol.asyncIterator`.
+ [@n1ru4l](https://github.com/n1ru4l) in [#232](https://github.com/apollographql/graphql-subscriptions/pull/232) - Add an optional generic type map to `PubSub`.
[@cursorsdottsx](https://github.com/cursorsdottsx) in [#245](https://github.com/apollographql/graphql-subscriptions/pull/245) -- Replace `iterall` use with native `Symbol.asyncIterator`.
- [@n1ru4l](https://github.com/n1ru4l) in [#232](https://github.com/apollographql/graphql-subscriptions/pull/232) - Support `readonly` arrays of event names.
[@rh389](https://github.com/rh389) in [#234](https://github.com/apollographql/graphql-subscriptions/pull/234) From 94a9993daf8aea11738fbb4671d38d99bcc8837c Mon Sep 17 00:00:00 2001 From: Mac Lockard Date: Thu, 25 Nov 2021 11:15:09 -0800 Subject: [PATCH 6/9] Allow resolver fn to be async (#220) * Allow resolver fn to be async * Changelog updates Co-authored-by: hwillson --- CHANGELOG.md | 2 ++ README.md | 2 +- src/test/asyncIteratorSubscription.ts | 5 +++-- src/test/tests.ts | 2 +- src/with-filter.ts | 7 +++---- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f34fdd..b36128f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ [@cursorsdottsx](https://github.com/cursorsdottsx) in [#245](https://github.com/apollographql/graphql-subscriptions/pull/245) - Support `readonly` arrays of event names.
[@rh389](https://github.com/rh389) in [#234](https://github.com/apollographql/graphql-subscriptions/pull/234) +- Support returning a Promise of an `AsyncIterator` as the `withFilter` resolver function.
+ [@maclockard](https://github.com/maclockard) in [#220](https://github.com/apollographql/graphql-subscriptions/pull/220) ### 2.0.1 (not yet released) diff --git a/README.md b/README.md index 8b5a309..ca67ab9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You can use it with any GraphQL client and server (not only Apollo). If you are developing a project that uses this module with TypeScript: -* ensure that your `tsconfig.json` `lib` definition includes `"esnext.asynciterable"` +* ensure that your `tsconfig.json` `lib` definition includes `"es2018.asynciterable"` * `npm install @types/graphql` or `yarn add @types/graphql` ### Getting started with your first subscription diff --git a/src/test/asyncIteratorSubscription.ts b/src/test/asyncIteratorSubscription.ts index d0f669d..fcf8ac8 100644 --- a/src/test/asyncIteratorSubscription.ts +++ b/src/test/asyncIteratorSubscription.ts @@ -189,7 +189,7 @@ const testFiniteAsyncIterator: AsyncIterableIterator = (async function * describe('withFilter', () => { it('works properly with finite asyncIterators', async () => { - const filteredAsyncIterator = withFilter(() => testFiniteAsyncIterator, isEven)(); + const filteredAsyncIterator = await withFilter(() => testFiniteAsyncIterator, isEven)(); for (let i = 1; i <= 4; i++) { const result = await filteredAsyncIterator.next(); @@ -228,7 +228,8 @@ describe('withFilter', () => { }, }; - const filteredAsyncIterator = withFilter(() => asyncIterator, () => stopped)(); + const filteredAsyncIterator = + await withFilter(() => asyncIterator, () => stopped)(); global.gc(); const heapUsed = process.memoryUsage().heapUsed; diff --git a/src/test/tests.ts b/src/test/tests.ts index a1e009a..a22852c 100644 --- a/src/test/tests.ts +++ b/src/test/tests.ts @@ -74,7 +74,7 @@ describe('AsyncIterator', () => { it('register to multiple events', done => { const eventName = 'test2'; const ps = new PubSub(); - const iterator = ps.asyncIterator(['test', 'test2'] as const); + const iterator = ps.asyncIterableIterator(['test', 'test2'] as const); const spy = sinon.spy(); iterator.next().then(() => { diff --git a/src/with-filter.ts b/src/with-filter.ts index f5c93e5..584934b 100644 --- a/src/with-filter.ts +++ b/src/with-filter.ts @@ -1,6 +1,5 @@ - export type FilterFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => boolean | Promise; -export type ResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterator; +export type ResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterator | Promise>; interface IterallAsyncIterator extends AsyncIterableIterator { [Symbol.asyncIterator](): IterallAsyncIterator; @@ -15,8 +14,8 @@ export function withFilter( asyncIteratorFn: ResolverFn, filterFn: FilterFn ): ResolverFn { - return (rootValue: TSource, args: TArgs, context: TContext, info: any): IterallAsyncIterator => { - const asyncIterator = asyncIteratorFn(rootValue, args, context, info); + return async (rootValue: TSource, args: TArgs, context: TContext, info: any): Promise> => { + const asyncIterator = await asyncIteratorFn(rootValue, args, context, info); const getNextPromise = () => { return new Promise>((resolve, reject) => { From 8edd499e9f8aec4a74cb9f1b053ebe38315340aa Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Tue, 5 Nov 2024 15:38:21 -0600 Subject: [PATCH 7/9] Allow AsyncIterator as withFilter input, but update the output to be AsyncIterableIterator to match subscription type --- src/index.ts | 2 +- src/test/tests.ts | 2 +- src/with-filter.ts | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5d64e13..5b9a94d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export { PubSubEngine } from './pubsub-engine'; export { PubSub, PubSubOptions } from './pubsub'; -export { withFilter, ResolverFn, FilterFn } from './with-filter'; +export { withFilter, ResolverFn, ResolverIteratorFn, FilterFn } from './with-filter'; diff --git a/src/test/tests.ts b/src/test/tests.ts index a22852c..fcf5a9c 100644 --- a/src/test/tests.ts +++ b/src/test/tests.ts @@ -15,7 +15,7 @@ const isAsyncIterableIterator = (input: unknown): input is AsyncIterableIterator chai.use(chaiAsPromised); chai.use(sinonChai); const expect = chai.expect; -const assert = chai.assert; +const assert: Chai.AssertStatic = chai.assert; describe('PubSub', function() { it('can subscribe and is called when events happen', () => { diff --git a/src/with-filter.ts b/src/with-filter.ts index 584934b..2b11f23 100644 --- a/src/with-filter.ts +++ b/src/with-filter.ts @@ -1,17 +1,18 @@ export type FilterFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => boolean | Promise; -export type ResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterator | Promise>; +export type ResolverIteratorFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterator | Promise>; +export type ResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterableIterator | Promise>; interface IterallAsyncIterator extends AsyncIterableIterator { [Symbol.asyncIterator](): IterallAsyncIterator; } export type WithFilter = ( - asyncIteratorFn: ResolverFn, + asyncIteratorFn: ResolverIteratorFn, filterFn: FilterFn ) => ResolverFn; export function withFilter( - asyncIteratorFn: ResolverFn, + asyncIteratorFn: ResolverIteratorFn, filterFn: FilterFn ): ResolverFn { return async (rootValue: TSource, args: TArgs, context: TContext, info: any): Promise> => { From f24bd1ab27d47a5849adc7493b767549ecc9a17a Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Tue, 5 Nov 2024 15:44:49 -0600 Subject: [PATCH 8/9] Keep existing ResolverFn type the same --- src/index.ts | 2 +- src/with-filter.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5b9a94d..ba3b9a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export { PubSubEngine } from './pubsub-engine'; export { PubSub, PubSubOptions } from './pubsub'; -export { withFilter, ResolverFn, ResolverIteratorFn, FilterFn } from './with-filter'; +export { withFilter, ResolverFn, FilterFn, IterableResolverFn } from './with-filter'; diff --git a/src/with-filter.ts b/src/with-filter.ts index 2b11f23..c881533 100644 --- a/src/with-filter.ts +++ b/src/with-filter.ts @@ -1,20 +1,20 @@ export type FilterFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => boolean | Promise; -export type ResolverIteratorFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterator | Promise>; -export type ResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterableIterator | Promise>; +export type ResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterator | Promise>; +export type IterableResolverFn = (rootValue?: TSource, args?: TArgs, context?: TContext, info?: any) => AsyncIterableIterator | Promise>; interface IterallAsyncIterator extends AsyncIterableIterator { [Symbol.asyncIterator](): IterallAsyncIterator; } export type WithFilter = ( - asyncIteratorFn: ResolverIteratorFn, + asyncIteratorFn: ResolverFn, filterFn: FilterFn -) => ResolverFn; +) => IterableResolverFn; export function withFilter( - asyncIteratorFn: ResolverIteratorFn, + asyncIteratorFn: ResolverFn, filterFn: FilterFn -): ResolverFn { +): IterableResolverFn { return async (rootValue: TSource, args: TArgs, context: TContext, info: any): Promise> => { const asyncIterator = await asyncIteratorFn(rootValue, args, context, info); From 5bd77206b11f83166451826ada2223c56438b67f Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Thu, 7 Nov 2024 14:25:59 -0600 Subject: [PATCH 9/9] Update changelog and fix readme examples to match new API --- CHANGELOG.md | 34 +++++++++++---- README.md | 116 ++++++++++++++++++++++++++++----------------------- 2 files changed, 89 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b36128f..88cec9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -### 3.0.0 (not yet released) +### 3.0.0 -- **[BREAKING]** Replace `iterall` use with native `Symbol.asyncIterator`.
+- **[BREAKING]** Replace `iterall` use with native `Symbol.asyncIterator`. `PubSubEngine.asyncIterator` is now `PubSubEngine.asyncIterableIterator`.
[@n1ru4l](https://github.com/n1ru4l) in [#232](https://github.com/apollographql/graphql-subscriptions/pull/232) - Add an optional generic type map to `PubSub`.
[@cursorsdottsx](https://github.com/cursorsdottsx) in [#245](https://github.com/apollographql/graphql-subscriptions/pull/245) @@ -10,11 +10,10 @@ [@rh389](https://github.com/rh389) in [#234](https://github.com/apollographql/graphql-subscriptions/pull/234) - Support returning a Promise of an `AsyncIterator` as the `withFilter` resolver function.
[@maclockard](https://github.com/maclockard) in [#220](https://github.com/apollographql/graphql-subscriptions/pull/220) - -### 2.0.1 (not yet released) - - `withFilter` TypeScript improvements.
[@HofmannZ](https://github.com/HofmannZ) in [#230](https://github.com/apollographql/graphql-subscriptions/pull/230) +- `withFilter` returns `AsyncIterableIterator` for compatibility with Apollo Server subscriptions.
+ [@tninesling](https://github.com/tninesling) in [#276](https://github.com/apollographql/graphql-subscriptions/pull/276) ### 2.0.0 @@ -36,77 +35,96 @@ ### 1.0.0 -- BREAKING CHANGE: Changed return type of `publish`.
+- BREAKING CHANGE: Changed return type of `publish`.
[@grantwwu](https://github.com/grantwwu) in [#162](https://github.com/apollographql/graphql-subscriptions/pull/162) - Bump versions of various devDependencies to fix security issues, use - newer tslint config.
+ newer tslint config.
[@grantwwu](https://github.com/grantwwu) in [#163](https://github.com/apollographql/graphql-subscriptions/pull/163) - Allows `graphql` 14 as a peer dep, forces `graphql` 14 as a dev dep, and - has been updated to use `@types/graphql` 14.
+ has been updated to use `@types/graphql` 14.
[@hwillson](https://github.com/hwillson) in [#172](https://github.com/apollographql/graphql-subscriptions/pull/172) ### 0.5.8 + - Bump iterall version ### 0.5.7 + - Add `graphql@0.13` to `peerDependencies`. ### 0.5.6 + - Add `graphql@0.12` to `peerDependencies`. ### 0.5.5 + - FilterFn can return a Promise - Allow passing in a custom `EventEmitter` to `PubSub` ### 0.5.4 + - Better define `withFilter` return type [PR #111](https://github.com/apollographql/graphql-subscriptions/pull/111) ### 0.5.3 + - Require iterall ^1.1.3 to address unhandled exceptions ### 0.5.2 + - Require iterall ^1.1.2 to address memory leak [Issue #97] (https://github.com/apollographql/graphql-subscriptions/issues/97) - Remove `@types/graphql` dependency. [PR #105] (https://github.com/apollographql/graphql-subscriptions/pull/105) ### 0.5.1 + - `withFilter` now called with `(rootValue, args, context, info)` [PR #103] (https://github.com/apollographql/graphql-subscriptions/pull/103) ### 0.5.0 + - BREAKING CHANGE: Removed deprecated code. [PR #104] (https://github.com/apollographql/graphql-subscriptions/pull/104) - BREAKING CHANGE: Minimum GraphQL version bumped to 0.10.X. [PR #104] (https://github.com/apollographql/graphql-subscriptions/pull/104) ### 0.4.4 + - Avoid infinite loop after the last consumer unsubscribes, [Issue #81](https://github.com/apollographql/graphql-subscriptions/issues/81) [PR #84](https://github.com/apollographql/graphql-subscriptions/pull/84) ### 0.4.3 + - Properly propagate return() and throw() through withFilter [PR #74](https://github.com/apollographql/graphql-subscriptions/pull/74) ### 0.4.2 + - Fixed issue with `withFilter` causing to use the same iterator [PR #69](https://github.com/apollographql/graphql-subscriptions/pull/69) ### 0.4.1 + - Fixed exports issue with TypeScript [PR #65](https://github.com/apollographql/graphql-subscriptions/pull/65) ### 0.4.0 + - Added `asyncIterator(channelName: string)` to `PubSub` implementation [PR #60](https://github.com/apollographql/graphql-subscriptions/pull/60) - Added `withFilter` to allow `AsyncIterator` filtering [PR #60](https://github.com/apollographql/graphql-subscriptions/pull/60) - Deprecate `SubscriptionManager` [PR #60](https://github.com/apollographql/graphql-subscriptions/pull/60) - Fixed `withFilter` issue caused multiple subscribers to execute with the same AsyncIterator [PR #69](https://github.com/apollographql/graphql-subscriptions/pull/69) ### 0.3.1 + - Add support for `defaultValue`, fixes [#49](https://github.com/apollographql/graphql-subscriptions/issues/49) (https://github.com/apollographql/graphql-subscriptions/pull/50) ### 0.3.0 + - Allow `setupFunctions` to be async (return `Promise`) (https://github.com/apollographql/graphql-subscriptions/pull/41) - Refactor promise chaining in pubsub engine (https://github.com/apollographql/graphql-subscriptions/pull/41) - Fixed a possible bug with managing subscriptions internally (https://github.com/apollographql/graphql-subscriptions/pull/29) - Return the `Promise` from `onMessage` of PubSub engine (https://github.com/apollographql/graphql-subscriptions/pull/33) ### 0.2.3 + - update `graphql` dependency to 0.9.0 ### 0.2.2 + - made `graphql` a peer dependency and updated it to 0.8.2 ### v 0.2.1 + - Fixed a bug that caused subscriptions without operationName to fail diff --git a/README.md b/README.md index 7c6a907..7843d89 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ You can use it with any GraphQL client and server (not only Apollo). If you are developing a project that uses this module with TypeScript: -* ensure that your `tsconfig.json` `lib` definition includes `"es2018.asynciterable"` -* `npm install @types/graphql` or `yarn add @types/graphql` +- ensure that your `tsconfig.json` `lib` definition includes `"es2018.asynciterable"` +- `npm install @types/graphql` or `yarn add @types/graphql` ### Getting started with your first subscription @@ -47,7 +47,7 @@ Now, let's create a simple `PubSub` instance - it is a simple pubsub implementat to the `PubSub` constructor. ```js -import { PubSub } from 'graphql-subscriptions'; +import { PubSub } from "graphql-subscriptions"; export const pubsub = new PubSub(); ``` @@ -55,35 +55,35 @@ export const pubsub = new PubSub(); If you're using TypeScript you can use the optional generic parameter for added type-safety: ```ts -import { PubSub } from "apollo-server-express"; +import { PubSub } from "graphql-subscriptions"; const pubsub = new PubSub<{ - EVENT_ONE: { data: number; }; - EVENT_TWO: { data: string; }; + EVENT_ONE: { data: number }; + EVENT_TWO: { data: string }; }>(); pubsub.publish("EVENT_ONE", { data: 42 }); -pubsub.publish("EVENTONE", { data: 42 }); // ! ERROR -pubsub.publish("EVENT_ONE", { data: "42" }); // ! ERROR +pubsub.publish("EVENTONE", { data: 42 }); // ! ERROR +pubsub.publish("EVENT_ONE", { data: "42" }); // ! ERROR pubsub.publish("EVENT_TWO", { data: "hello" }); pubsub.subscribe("EVENT_ONE", () => {}); -pubsub.subscribe("EVENTONE", () => {}); // ! ERROR +pubsub.subscribe("EVENTONE", () => {}); // ! ERROR pubsub.subscribe("EVENT_TWO", () => {}); ``` -Next implement your Subscriptions type resolver using the `pubsub.asyncIterator` to map the event you need: +Next implement your Subscriptions type resolver using the `pubsub.asyncIterableIterator` to map the event you need: ```js -const SOMETHING_CHANGED_TOPIC = 'something_changed'; +const SOMETHING_CHANGED_TOPIC = "something_changed"; export const resolvers = { Subscription: { somethingChanged: { - subscribe: () => pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC), + subscribe: () => pubsub.asyncIterableIterator(SOMETHING_CHANGED_TOPIC), }, }, -} +}; ``` > Subscriptions resolvers are not a function, but an object with `subscribe` method, that returns `AsyncIterable`. @@ -91,7 +91,7 @@ export const resolvers = { The GraphQL engine now knows that `somethingChanged` is a subscription, and every time we use `pubsub.publish` it will publish content using our chosen transport layer: ```js -pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: "123" }}); +pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: "123" } }); ``` > Note that the default PubSub implementation is intended for demo purposes. It only works if you have a single instance of your server and doesn't scale beyond a couple of connections. @@ -104,25 +104,29 @@ When publishing data to subscribers, we need to make sure that each subscriber g To do so, we can use `withFilter` helper from this package, which wraps `AsyncIterator` with a filter function, and lets you control each publication for each user. `withFilter` API: -- `asyncIteratorFn: (rootValue, args, context, info) => AsyncIterator` : A function that returns `AsyncIterator` you got from your `pubsub.asyncIterator`. + +- `asyncIteratorFn: (rootValue, args, context, info) => AsyncIterator` : A function that returns `AsyncIterator` you got from your `pubsub.asyncIterableIterator`. - `filterFn: (payload, variables, context, info) => boolean | Promise` - A filter function, executed with the payload (the published value), variables, context and operation info, must return `boolean` or `Promise` indicating if the payload should pass to the subscriber. For example, if `somethingChanged` would also accept a variable with the ID that is relevant, we can use the following code to filter according to it: ```js -import { withFilter } from 'graphql-subscriptions'; +import { withFilter } from "graphql-subscriptions"; -const SOMETHING_CHANGED_TOPIC = 'something_changed'; +const SOMETHING_CHANGED_TOPIC = "something_changed"; export const resolvers = { Subscription: { somethingChanged: { - subscribe: withFilter(() => pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC), (payload, variables) => { - return payload.somethingChanged.id === variables.relevantId; - }), + subscribe: withFilter( + () => pubsub.asyncIterableIterator(SOMETHING_CHANGED_TOPIC), + (payload, variables) => { + return payload.somethingChanged.id === variables.relevantId; + } + ), }, }, -} +}; ``` > Note that when using `withFilter`, you don't need to wrap your return value with a function. @@ -132,25 +136,30 @@ export const resolvers = { You can map multiple channels into the same subscription, for example when there are multiple events that trigger the same subscription in the GraphQL engine. ```js -const SOMETHING_UPDATED = 'something_updated'; -const SOMETHING_CREATED = 'something_created'; -const SOMETHING_REMOVED = 'something_removed'; +const SOMETHING_UPDATED = "something_updated"; +const SOMETHING_CREATED = "something_created"; +const SOMETHING_REMOVED = "something_removed"; export const resolvers = { Subscription: { somethingChanged: { - subscribe: () => pubsub.asyncIterator([ SOMETHING_UPDATED, SOMETHING_CREATED, SOMETHING_REMOVED ]), + subscribe: () => + pubsub.asyncIterableIterator([ + SOMETHING_UPDATED, + SOMETHING_CREATED, + SOMETHING_REMOVED, + ]), }, }, -} -```` +}; +``` ### Payload Manipulation You can also manipulate the published payload, by adding `resolve` methods to your subscription: ```js -const SOMETHING_UPDATED = 'something_updated'; +const SOMETHING_UPDATED = "something_updated"; export const resolvers = { Subscription: { @@ -159,27 +168,27 @@ export const resolvers = { // Manipulate and return the new value return payload.somethingChanged; }, - subscribe: () => pubsub.asyncIterator(SOMETHING_UPDATED), + subscribe: () => pubsub.asyncIterableIterator(SOMETHING_UPDATED), }, }, -} -```` +}; +``` -Note that `resolve` methods execute *after* `subscribe`, so if the code in `subscribe` depends on a manipulated payload field, you will need to factor out the manipulation and call it from both `subscribe` and `resolve`. +Note that `resolve` methods execute _after_ `subscribe`, so if the code in `subscribe` depends on a manipulated payload field, you will need to factor out the manipulation and call it from both `subscribe` and `resolve`. ### Usage with callback listeners Your database might have callback-based listeners for changes, for example something like this: -```JS +```js const listenToNewMessages = (callback) => { - return db.table('messages').listen(newMessage => callback(newMessage)); -} + return db.table("messages").listen((newMessage) => callback(newMessage)); +}; // Kick off the listener -listenToNewMessages(message => { +listenToNewMessages((message) => { console.log(message); -}) +}); ``` The `callback` function would be called every time a new message is saved in the database. Unfortunately, that doesn't play very well with async iterators out of the box because callbacks are push-based, where async iterators are pull-based. @@ -187,7 +196,7 @@ The `callback` function would be called every time a new message is saved in the We recommend using the [`callback-to-async-iterator`](https://github.com/withspectrum/callback-to-async-iterator) module to convert your callback-based listener into an async iterator: ```js -import asyncify from 'callback-to-async-iterator'; +import asyncify from "callback-to-async-iterator"; export const resolvers = { Subscription: { @@ -195,23 +204,28 @@ export const resolvers = { subscribe: () => asyncify(listenToNewMessages), }, }, -} -```` +}; +``` ### Custom `AsyncIterator` Wrappers -The value you should return from your `subscribe` resolver must be an `AsyncIterator`. +The value you should return from your `subscribe` resolver must be an `AsyncIterable`. -You can use this value and wrap it with another `AsyncIterator` to implement custom logic over your subscriptions. +You can wrap an `AsyncIterator` with custom logic for your subscriptions. For compatibility with APIs that require `AsyncIterator` or `AsyncIterable`, your wrapper can return an `AsyncIterableIterator` to comply with both. For example, the following implementation manipulates the payload by adding some static fields: ```typescript -import { $$asyncIterator } from 'iterall'; - -export const withStaticFields = (asyncIterator: AsyncIterator, staticFields: Object): Function => { - return (rootValue: any, args: any, context: any, info: any): AsyncIterator => { - +export const withStaticFields = ( + asyncIterator: AsyncIterator, + staticFields: Object +): Function => { + return ( + rootValue: any, + args: any, + context: any, + info: any + ): AsyncIterableIterator => { return { next() { return asyncIterator.next().then(({ value, done }) => { @@ -230,7 +244,7 @@ export const withStaticFields = (asyncIterator: AsyncIterator, staticFields throw(error) { return Promise.reject(error); }, - [$$asyncIterator]() { + [Symbol.asyncIterator]() { return this; }, }; @@ -240,14 +254,10 @@ export const withStaticFields = (asyncIterator: AsyncIterator, staticFields > You can also take a look at `withFilter` for inspiration. -For more information about `AsyncIterator`: -- [TC39 Proposal](https://github.com/tc39/proposal-async-iteration) -- [iterall](https://github.com/leebyron/iterall) -- [IxJS](https://github.com/ReactiveX/IxJS) - ### PubSub Implementations It can be easily replaced with some other implementations of [PubSubEngine abstract class](https://github.com/apollographql/graphql-subscriptions/blob/master/src/pubsub-engine.ts). Here are a few of them: + - Use Redis with https://github.com/davidyaha/graphql-redis-subscriptions - Use Google PubSub with https://github.com/axelspringer/graphql-google-pubsub - Use MQTT enabled broker with https://github.com/aerogear/graphql-mqtt-subscriptions