diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..deee6ebd --- /dev/null +++ b/website/README.md @@ -0,0 +1,3 @@ +# README + +Building the website requires that the documentation be generated from the root project into the `src/pages/docs` directory _first_. This can be done by `yarn gendocs` from the root of the repo. \ No newline at end of file diff --git a/website/src/pages/get-started.mdx b/website/src/pages/get-started.mdx index 577fdb24..4f69d440 100644 --- a/website/src/pages/get-started.mdx +++ b/website/src/pages/get-started.mdx @@ -16,6 +16,7 @@ npm i graphql-ws ```ts import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; +import { Repeater } from "@repeaterjs/repeater"; /** * Construct a GraphQL schema and define the necessary resolvers. @@ -43,9 +44,12 @@ export const schema = new GraphQLSchema({ greetings: { type: GraphQLString, subscribe: async function* () { - for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) { - yield { greetings: hi }; - } + const greetings = ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']; + return new Repeater(async (push, stop) => { + const interval = setInterval(() => greetings.length > 0 ? push(greetings.shift()) : null, 1000); + await stop; + clearInterval(interval); + }); }, }, }, diff --git a/website/src/pages/recipes.mdx b/website/src/pages/recipes.mdx index 45dc891c..9256c30d 100644 --- a/website/src/pages/recipes.mdx +++ b/website/src/pages/recipes.mdx @@ -922,6 +922,102 @@ wsServer.on('connection', (socket, request) => { }); ``` +### Server usage with a custom event emitter + +You may have a customer event production system that you need to tie into that requires you to write your own event emitter. When using graphql-ws, your `subscribe()` function *can* return an [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) implementation to do this. Javascript has language-level support for `AsyncGenerator`s through the `async function*()`/`yield` syntax, and it easy easy to work with. However, it is not recommended that you use it in real-world applications. This is due to a constraint of `AsyncGenerators` in general and understanding why will take a little explanation. + +The consumer of an `AsyncGenerator` (in this case, `graphql-ws`) awaits the [`next()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/next) function, which will return the value `yield`ed in your function. If the generator function is *also* `await`ing a promise before `yield`ing it, nothing else can change the state of the `AsyncGenerator` until the promise returns. + +The reason this is an issue is that `graphql-ws` will react to client "unsubscribe" requests by calling [`return()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/return). This method will cause the `AsyncGenerator` to return a value immediately from the the `yield` statement, but if your function is currently `await`ing a promise, the call to `return()` will be blocked until that promise resolves. Until then, your event emitter will not be able to run any cleanup operations and none of its allocated objects will be garbage collected. + +Below is a toy implementation that can be used as a template for rolling your own emitter (you may also use a library that does this for you, like [Repeater.js](https://repeater.js.org/)). + + +```ts +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; + +/** + * Construct a GraphQL schema and define the necessary resolvers. + * + * type Query { + * hello: String + * } + * type Subscription { + * greetings: String + * } + */ +export const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + hello: { + type: GraphQLString, + resolve: () => 'world', + }, + }, + }), + subscription: new GraphQLObjectType({ + name: 'Subscription', + fields: { + greetings: { + type: GraphQLString, + subscribe: async function () { + return new EventProvider().iterator(); + }, + }, + }, + }), +}); + +/** + * A toy event provider implementation. Will return a series of greetings with a small + * delay in between each one. + */ +class EventProvider { + private readonly events = ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']; + private promiseReject: ((v?: any) => void) | null = null; + private closed = false; + + close() { + if (this.closed) { return; } + if (this.promiseReject) { this.promiseReject('closed'); } + this.closed = true; + } + + isClosed() { return this.closed || this.events.length === 0; } + + async event() { + return await new Promise(async (resolve, reject) => { + this.promiseReject = reject; + setTimeout(() => resolve(this.events.shift()), 100); + }); + } + + iterator() { + const self = this; + return { + [Symbol.asyncIterator]() { return this; }, + async next(): { + try { + if (!self.isClosed()) { + return { value: await self.event(), done: false }; + } + } catch (e) { + return { value: undefined, done: true }; + } + }, + async return(v?: any) { + self.close(); + return { value: v, done: true }; + }, + async throw(v?: any) { + return { value: undefined, done: true }; + } + }; + } +} +``` + ### Server usage with [Cloudflare Workers](https://workers.cloudflare.com/) [Please check the `worker-graphql-ws-template` repo out.](https://github.com/enisdenjo/cloudflare-worker-graphql-ws-template)