-
Notifications
You must be signed in to change notification settings - Fork 575
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
feat: RedisEventTarget #1299
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ae66ecc
feat: add redis event target draft
n1ru4l 13c0a7a
run tests on all EventTarget implementations
n1ru4l 7a73181
remove comments
n1ru4l 4bf57e8
fix: fallback error message
n1ru4l 12691fd
issa module
n1ru4l 1d473e0
fix: polyfill event target on older platforms
n1ru4l fe9cabe
chore: remove unused dependency
n1ru4l db71635
chore: changeset and documentation
n1ru4l 002b6d5
chore: add redis pub sub example
n1ru4l 9fff3b7
chore: skip SvelteKit tests
n1ru4l File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. 🪄✨ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. */ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
73 changes: 73 additions & 0 deletions
73
packages/event-target/redis-event-target/__tests__/redis-event-target.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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