Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 3.0 #250

Merged
merged 11 commits into from
Nov 7, 2024
39 changes: 34 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Changelog

### 2.0.1 (not yet released)

### 3.0.0

- **[BREAKING]** Replace `iterall` use with native `Symbol.asyncIterator`. `PubSubEngine.asyncIterator` is now `PubSubEngine.asyncIterableIterator`. <br/>
[@n1ru4l](https://github.com/n1ru4l) in [#232](https://github.com/apollographql/graphql-subscriptions/pull/232)
- Add an optional generic type map to `PubSub`. <br/>
[@cursorsdottsx](https://github.com/cursorsdottsx) in [#245](https://github.com/apollographql/graphql-subscriptions/pull/245)
- Support `readonly` arrays of event names. <br/>
[@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. <br/>
[@maclockard](https://github.com/maclockard) in [#220](https://github.com/apollographql/graphql-subscriptions/pull/220)
- `withFilter` TypeScript improvements. <br/>
[@HofmannZ](https://github.com/HofmannZ) in [#230](https://github.com/apollographql/graphql-subscriptions/pull/230)
- `withFilter` returns `AsyncIterableIterator` for compatibility with Apollo Server subscriptions. <br/>
[@tninesling](https://github.com/tninesling) in [#276](https://github.com/apollographql/graphql-subscriptions/pull/276)

### 2.0.0

Expand All @@ -25,77 +35,96 @@

### 1.0.0

- BREAKING CHANGE: Changed return type of `publish`. <br/>
- BREAKING CHANGE: Changed return type of `publish`. <br/>
[@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. <br/>
newer tslint config. <br/>
[@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. <br/>
has been updated to use `@types/graphql` 14. <br/>
[@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 `[email protected]` to `peerDependencies`.

### 0.5.6

- Add `[email protected]` to `peerDependencies`.

### 0.5.5

- FilterFn can return a Promise<boolean>
- 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
130 changes: 80 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ 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"`
* `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

To begin with GraphQL subscriptions, start by defining a GraphQL `Subscription` type in your schema:

```graphql
type Subscription {
somethingChanged: Result
somethingChanged: Result
}

type Result {
id: String
id: String
}
```

Expand All @@ -47,31 +47,51 @@ 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();
```

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 "graphql-subscriptions";

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.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`.

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" }});
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.
Expand All @@ -84,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<any>` : A function that returns `AsyncIterator` you got from your `pubsub.asyncIterator`.

- `asyncIteratorFn: (rootValue, args, context, info) => AsyncIterator<any>` : A function that returns `AsyncIterator` you got from your `pubsub.asyncIterableIterator`.
- `filterFn: (payload, variables, context, info) => boolean | Promise<boolean>` - A filter function, executed with the payload (the published value), variables, context and operation info, must return `boolean` or `Promise<boolean>` 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.
Expand All @@ -112,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: {
Expand All @@ -139,59 +168,64 @@ 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.

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: {
somethingChanged: {
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<any>, staticFields: Object): Function => {
return (rootValue: any, args: any, context: any, info: any): AsyncIterator<any> => {

export const withStaticFields = (
asyncIterator: AsyncIterator<any>,
staticFields: Object
): Function => {
return (
rootValue: any,
args: any,
context: any,
info: any
): AsyncIterableIterator<any> => {
return {
next() {
return asyncIterator.next().then(({ value, done }) => {
Expand All @@ -210,7 +244,7 @@ export const withStaticFields = (asyncIterator: AsyncIterator<any>, staticFields
throw(error) {
return Promise.reject(error);
},
[$$asyncIterator]() {
[Symbol.asyncIterator]() {
return this;
},
};
Expand All @@ -220,14 +254,10 @@ export const withStaticFields = (asyncIterator: AsyncIterator<any>, 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
Expand Down
Loading