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: add websocket support for subscriptions via crossws #65

Merged
merged 8 commits into from
Jul 9, 2024
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
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,54 @@ app.use(
Then run your h3 server as usual, e.g. with `npx --yes listhen -w --open ./app.ts`.
Visit http://localhost:3000/api in your browser to access the Apollo Sandbox.

## Subscriptions with WebSockets

This package also supports subscriptions over WebSockets. To enable this feature, you need to install the `graphql-ws` package:

```sh
# npm
npm install graphql-ws

# yarn
yarn add graphql-ws

# pnpm
pnpm add graphql-ws
```

Then you can add a WebSocket handler to your `h3` app using the `defineGraphqlWebSocketHandler` or `defineGraphqlWebSocket` functions from this package. Here is an example that combines the HTTP and WebSocket handlers in a single app.

```js
import { createApp } from 'h3'
import { ApolloServer } from '@apollo/server'
import {
startServerAndCreateH3Handler,
defineGraphqlWebSocketHandler,
} from '@as-integrations/h3'
import { makeExecutableSchema } from '@graphql-tools/schema'

// Define your schema and resolvers
const typeDefs = `...`
const resolvers = {
/*...*/
}
const schema = makeExecutableSchema({ typeDefs, resolvers })

const apollo = new ApolloServer({ schema })

export const app = createApp()
app.use(
'/api',
startServerAndCreateH3Handler(apollo, {
websocket: defineGraphqlWebSocketHandler({ schema }),
}),
)
```

Then you can connect to the WebSocket endpoint using the Apollo Sandbox or any other client that supports the `graphql-ws` protocol.

See the [WebSocket example](./examples/websocket.ts) for a complete example.

## 💻 Development

- Clone this repository
Expand Down
20 changes: 20 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# WebSocket

This example demonstrates how to use the `graphql-ws` package to enable WebSockets in Apollo Server via `h3`.
It is based on the [Apollo Server WebSocket example](https://www.apollographql.com/docs/apollo-server/data/subscriptions#basic-runnable-example).

Run the following command to start the server and open the Apollo Sandbox in the browser:

```sh
pnpm example:websocket
```

The server is configured to listen for WebSocket connections on the `/ws` path and to serve the Apollo Sandbox (and the GraphQL http endpoint) on the `/ws` path. (It is currently not possible to serve both the WebSocket and the HTTP endpoint on the root path, see [this h3 issue](https://github.com/unjs/h3/issues/719).)

Then test the WebSocket connection by running the following query:

```graphql
subscription Subscription {
numberIncremented
}
```
110 changes: 110 additions & 0 deletions examples/websocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// This is an adaption of the "official" example of how to use subscriptions with Apollo server
// https://github.com/apollographql/docs-examples/tree/main/apollo-server/v4/subscriptions-graphql-ws
import { ApolloServer } from '@apollo/server'
import { PubSub } from 'graphql-subscriptions'
import { createApp, defineWebSocketHandler } from 'h3'
import { startServerAndCreateH3Handler } from '../src'
import { defineGraphqlWebSocket } from '../src/websocket'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'

const pubsub = new PubSub()

// A number that we'll increment over time to simulate subscription events
let currentNumber = 0

// Schema definition
const typeDefs = `#graphql
type Query {
currentNumber: Int
}

type Subscription {
numberIncremented: Int
}
`

// Resolver map
const resolvers = {
Query: {
currentNumber() {
return currentNumber
},
},
Subscription: {
numberIncremented: {
subscribe: () => pubsub.asyncIterator(['NUMBER_INCREMENTED']),
},
},
}

// Create schema, which will be used separately by ApolloServer and
// the WebSocket server.
const schema = makeExecutableSchema({ typeDefs, resolvers })

// Create an h3 app; we will attach the WebSocket
// server and the ApolloServer to it.
export const app = createApp()

// Set up ApolloServer.
const apollo = new ApolloServer({
schema,
plugins: [
ApolloServerPluginLandingPageLocalDefault({
// This is needed to be able to change the ws endpoint
embed: { endpointIsEditable: true },
}),
],
})

//app.use('/', startServerAndCreateH3Handler(apollo))
app.use(
// TODO: For some reason it doesn't work with the root path
// see discussion at https://github.com/unjs/h3/issues/719
'/ws',
startServerAndCreateH3Handler(apollo, {
websocket: {
...defineGraphqlWebSocket({ schema }),
error(peer, error) {
console.error('[ws] error', peer, error)
// In a real app, you would want to properly log this error
},
// For debugging:
// message(peer, message) {
// console.error('[ws] message', peer, message)
// },
// open(peer) {
// console.error('[ws] open', peer)
// },
// upgrade(req) {
// console.error('[ws] upgrade', req)
// },
// close(peer, details) {
// console.error('[ws] close', peer, details)
// }
},
}),
)

// Alternatively, you can use the following to define only the WebSocket server without ApolloServer.
app.use(
'/_ws',
defineWebSocketHandler({
...defineGraphqlWebSocket({ schema }),
error(peer, error) {
console.log('[ws] error', peer, error)
},
}),
)

// In the background, increment a number every second and notify subscribers when it changes.
function incrementNumber() {
currentNumber++
pubsub
.publish('NUMBER_INCREMENTED', { numberIncremented: currentNumber })
.catch(console.error)
setTimeout(incrementNumber, 1000)
}

// Start incrementing
incrementNumber()
16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dist"
],
"scripts": {
"prepare": "unbuild --stub",
"build": "unbuild",
"test": "vitest dev",
"test:integration": "jest",
Expand All @@ -28,25 +29,38 @@
"lint:eslint": "eslint --report-unused-disable-directives .",
"lint:prettier": "prettier --check --ignore-path .gitignore . '!pnpm-lock.yaml'",
"example:simple": "listhen -w --open ./examples/simple.ts",
"example:websocket": "listhen -w --ws --open ./examples/websocket.ts",
"prepack": "unbuild",
"release": "pnpm test && standard-version && git push --follow-tags && pnpm publish"
},
"peerDependencies": {
"@apollo/server": "^4.1.1",
"h3": "^1.8.0"
"h3": "^1.11.0",
"graphql": "^16.0.0",
"graphql-ws": "^5.0.0",
"crossws": "^0.2.4"
},
"peerDependenciesMeta": {
"graphql-ws": {
"optional": true
}
},
"devDependencies": {
"@apollo/server": "^4.10.4",
"@apollo/server-integration-testsuite": "^4.10.4",
"@apollo/utils.withrequired": "^3.0.0",
"@graphql-tools/schema": "^10.0.3",
"@jest/globals": "^29.7.0",
"@typescript-eslint/parser": "^7.16.0",
"crossws": "^0.2.4",
"@vitest/coverage-v8": "^2.0.1",
"eslint": "^9.6.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-unjs": "^0.3.2",
"eslint-plugin-unused-imports": "^4.0.0",
"graphql": "^16.9.0",
"graphql-subscriptions": "^2.0.0",
"graphql-ws": "^5.15.0",
"h3": "^1.12.0",
"jest": "^29.7.0",
"listhen": "^1.7.2",
Expand Down
Loading
Loading