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

feat: RedisEventTarget #1299

Merged
merged 10 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-countries-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-yoga/typed-event-target': minor
---

Initial release of this package. It contains an EventTarget implementation with generic typings.
5 changes: 5 additions & 0 deletions .changeset/new-lobsters-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-yoga/subscription': minor
---

Use `@graphql-yoga/typed-event-target` as a dependency for the EventTarget implementation.
5 changes: 5 additions & 0 deletions .changeset/silly-lions-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-yoga/redis-event-target': minor
---

Initial release of this package. It contains an EventTarget implementation based upon Redis Pub/Sub using ioredis.
32 changes: 32 additions & 0 deletions examples/redis-pub-sub/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Redis Pub/Sub Example

## Usage instructions

Start Redis with Docker

```bash
docker run -p "6379:6379" redis:7.0.2
```

Start two server instances running on different ports

```bash
PORT=4000 yarn workspace example-redis-pub-sub start
PORT=4001 yarn workspace example-redis-pub-sub start
```

Visit and set up the subscription by pressing the Play button.

```bash
http://127.0.0.1:4000/graphql?query=subscription+%7B%0A++message%0A%7D
```

Visit and execute the mutation by pressing the Play button.

```bash
http://127.0.0.1:4001/graphql?query=mutation+%7B%0A++sendMessage%28message%3A+%22Yo+we+share+a+redis+instance.%22%29%0A%7D
```

See your subscription update appear on `127.0.0.1:4000`, even though you executed the mutation on a different Node.js server instance running on `127.0.0.1:4001`.

The magic of Redis. 🪄✨
28 changes: 28 additions & 0 deletions examples/redis-pub-sub/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "example-redis-pub-sub",
"version": "0.0.0",
"private": true,
"description": "",
"scripts": {
"dev": "cross-env NODE_ENV=development ts-node-dev --exit-child --respawn src/main.ts",
"start": "ts-node src/main.ts",
"check": "tsc --pretty --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "16.11.7",
"cross-env": "7.0.3",
"ts-node": "10.8.1",
"ts-node-dev": "1.1.8",
"typescript": "4.7.4"
},
"dependencies": {
"@graphql-yoga/node": "2.12.0",
"@graphql-yoga/redis-event-target": "0.0.0",
"graphql": "16.5.0",
"ioredis": "5.0.6"
},
"module": "commonjs"
}
50 changes: 50 additions & 0 deletions examples/redis-pub-sub/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createServer, createPubSub } from '@graphql-yoga/node'
import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'
import Redis from 'ioredis'

const publishClient = new Redis()
const subscribeClient = new Redis()

const pubSub = createPubSub<{
message: [string]
}>({
eventTarget: createRedisEventTarget({
publishClient,
subscribeClient,
}),
})

const server = createServer<{ pubSub: typeof pubSub }>({
context: () => ({ pubSub }),
port: parseInt(process.env.PORT || '4000', 10),
schema: {
typeDefs: /* GraphQL */ `
type Query {
_: Boolean
}

type Subscription {
message: String!
}

type Mutation {
sendMessage(message: String!): Boolean
}
`,
resolvers: {
Subscription: {
message: {
subscribe: (_, __, context) => context.pubSub.subscribe('message'),
resolve: (message) => message,
},
},
Mutation: {
sendMessage(_, { message }, context) {
context.pubSub.publish('message', message)
},
},
},
},
})

server.start()
10 changes: 10 additions & 0 deletions examples/redis-pub-sub/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "commonjs" /* Specify what module code is generated. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
2 changes: 1 addition & 1 deletion examples/sveltekit/__tests__/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import puppeteer from 'puppeteer';
let browser: puppeteer.Browser;
let page: puppeteer.Page;

describe('SvelteKit integration', () => {
describe.skip('SvelteKit integration', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping because it is also broken on master. See #1378

Copy link
Contributor

@charlypoly charlypoly Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dotansimha @n1ru4l the SvelteKit integration has been broken in the following PR that introduced breaking changes in yoga/common: #1364

beforeAll(async () => {
browser = await puppeteer.launch({
// If you wanna run tests with open browser
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"workspaces": [
"packages/*",
"packages/plugins/*",
"packages/event-target/*",
"examples/**/*",
"benchmark/*",
"website",
Expand Down
5 changes: 5 additions & 0 deletions packages/event-target/redis-event-target/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @graphql-yoga/redis-event-target

Do distributed GraphQL subscriptions over Redis.

[Learn more about GraphQL Subscriptions.](https://www.graphql-yoga.com/docs/features/subscriptions)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { TypedEvent } from '@graphql-yoga/typed-event-target'
import Redis from 'ioredis-mock'
import { createRedisEventTarget } from '../src'

if (!globalThis.EventTarget || !globalThis.Event) {
require('event-target-polyfill')
}

describe('createRedisEventTarget', () => {
it('can listen to a simple publish', (done) => {
const eventTarget = createRedisEventTarget({
publishClient: new Redis({}),
subscribeClient: new Redis({}),
})

eventTarget.addEventListener('a', (event: TypedEvent) => {
expect(event.type).toEqual('a')
expect(event.data).toEqual({
hi: 1,
})
done()
})

const event = new Event('a') as TypedEvent
event.data = { hi: 1 }
eventTarget.dispatchEvent(event)
})

it('does not listen for events for which no lister is set up', (done) => {
const eventTarget = createRedisEventTarget({
publishClient: new Redis({}),
subscribeClient: new Redis({}),
})

eventTarget.addEventListener('a', (_event: TypedEvent) => {
done(new Error('This should not be invoked'))
})
eventTarget.addEventListener('b', (event: TypedEvent) => {
expect(event.type).toEqual('b')
expect(event.data).toEqual({
hi: 1,
})
done()
})

const event = new Event('b') as TypedEvent
event.data = { hi: 1 }
eventTarget.dispatchEvent(event)
})
it('distributes the event to all event listeners', (done) => {
const eventTarget = createRedisEventTarget({
publishClient: new Redis({}),
subscribeClient: new Redis({}),
})

let counter = 0
eventTarget.addEventListener('b', (_event: TypedEvent) => {
counter++
})
eventTarget.addEventListener('b', (_event: TypedEvent) => {
counter++
})

const event = new Event('b') as TypedEvent
event.data = { hi: 1 }
eventTarget.dispatchEvent(event)

setImmediate(() => {
expect(counter).toEqual(2)
done()
})
})
})
61 changes: 61 additions & 0 deletions packages/event-target/redis-event-target/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@graphql-yoga/redis-event-target",
"version": "0.0.0",
"description": "",
"repository": {
"type": "git",
"url": "https://github.com/dotansimha/graphql-yoga.git",
"directory": "packages/event-target/redis-event-target"
},
"scripts": {
"check": "tsc --pretty --noEmit"
},
"keywords": [
"pubsub",
"graphql",
"event",
"subscription"
],
"author": "Laurin Quast <laurinquast@googlemail.com>",
"license": "MIT",
"dependencies": {
"@graphql-yoga/typed-event-target": "^0.0.0"
},
"peerDependencies": {
"ioredis": "^5.0.6"
},
"devDependencies": {
"@types/ioredis-mock": "5.6.0",
"event-target-polyfill": "0.0.3",
"ioredis": "5.0.6",
"ioredis-mock": "8.2.2"
},
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"exports": {
".": {
"require": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
},
"default": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./package.json": "./package.json"
},
"typings": "dist/typings/index.d.ts",
"typescript": {
"definition": "dist/typings/index.d.ts"
},
"publishConfig": {
"directory": "dist",
"access": "public"
}
}
86 changes: 86 additions & 0 deletions packages/event-target/redis-event-target/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
type TypedEventTarget,
type EventAPI,
resolveGlobalConfig,
} from '@graphql-yoga/typed-event-target'
import type { Redis, Cluster } from 'ioredis'

export type CreateRedisEventTargetArgs = {
publishClient: Redis | Cluster
subscribeClient: Redis | Cluster
/**
* Event and EventTarget implementation.
* Providing this is mandatory for a Node.js versions below 16.
*/
event?: EventAPI
}

export function createRedisEventTarget<TEvent extends Event>(
args: CreateRedisEventTargetArgs,
): TypedEventTarget<TEvent> {
const { publishClient, subscribeClient } = args
const eventAPI = resolveGlobalConfig(args.event)

const callbacksForTopic = new Map<string, Set<(event: TEvent) => void>>()

function onMessage(channel: string, message: string) {
const callbacks = callbacksForTopic.get(channel)
if (callbacks === undefined) {
return
}
const event = new eventAPI.Event(channel) as TEvent & {
data: unknown
}
event.data = JSON.parse(message)
for (const callback of callbacks) {
callback(event)
}
}

subscribeClient.on('message', onMessage)

function addCallback(topic: string, callback: (event: TEvent) => void) {
let callbacks = callbacksForTopic.get(topic)
if (callbacks === undefined) {
callbacks = new Set()
callbacksForTopic.set(topic, callbacks)

subscribeClient.subscribe(topic)
}
callbacks.add(callback)
}

function removeCallback(topic: string, callback: (event: TEvent) => void) {
let callbacks = callbacksForTopic.get(topic)
if (callbacks === undefined) {
return
}
callbacks.delete(callback)
if (callbacks.size > 0) {
return
}
callbacksForTopic.delete(topic)
subscribeClient.unsubscribe(topic)
}

return {
addEventListener(topic, callbackOrOptions) {
const callback =
'handleEvent' in callbackOrOptions
? callbackOrOptions.handleEvent
: callbackOrOptions
addCallback(topic, callback)
},
dispatchEvent(event: TEvent) {
publishClient.publish(event.type, JSON.stringify((event as any).data))
return true
},
removeEventListener(topic, callbackOrOptions) {
const callback =
'handleEvent' in callbackOrOptions
? callbackOrOptions.handleEvent
: callbackOrOptions
removeCallback(topic, callback)
},
}
}
7 changes: 7 additions & 0 deletions packages/event-target/typed-event-target/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @graphql-yoga/typed-event-target

This is an internal package.
Please don't use this package directly.
The package will do unexpected breaking changes.

[Learn more about GraphQL Subscriptions.](https://www.graphql-yoga.com/docs/features/subscriptions)
Loading