Skip to content

Commit

Permalink
Merge pull request #250 from apollographql/release-3.0
Browse files Browse the repository at this point in the history
Release 3.0
  • Loading branch information
tninesling authored Nov 7, 2024
2 parents b880397 + 5bd7720 commit 0805f27
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 117 deletions.
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 `graphql@0.13` to `peerDependencies`.

### 0.5.6

- Add `graphql@0.12` 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

0 comments on commit 0805f27

Please sign in to comment.