I'm developing a real-time chat app using Apollo GraphQL, and I want to keep the clients updated about the other client's status.
Mean that I need to do something with the onConnect and OnDisconnect events from the subscriptions object (which defined in app.js) and send a subscription event to the clients whenever a user connected or disconnected.
I could not find an explanation on how to do this, and I would appreciate any help.
you have to use onDisconnect property with context.initPromise like this:
onDisconnect: async (webSocket, context) => {
console.log('what is the context?: ', context);
try {
const initialContext = await context.initPromise;
if (
initialContext &&
typeof initialContext === 'object' &&
Reflect.has(initialContext, 'user')
) {
console.log('initialContext: ', initialContext);
}
} catch (err) {
console.log('error', err); // TypeError:
}
}
},
link and link2 for reference.
warning: this is currently working on my local build but not on my heroku production build
Related
I really love graphql + rtk query but I cant get the graphql subscriptions working.
I almost directly copied the streaming update example from the redux documentation. But I get the error subscriptions are not supported over HTTP, use websockets instead.
I dont know how to solve this, any help? Can barely find any documentation about graphql subscriptions + rtk query
userStatus: builder.query<
UserStatusSubscriptionSubscription,
{
event_id: string;
user_id: string;
}
>({
query: ({ event_id, user_id }) => ({
document: gql
subscription UserStatusSubscription(
$event_id: uuid!
$user_id: String!
) {
eetschema_event_by_pk(id: $event_id) {
event_attendees(where: { user_id: { _eq: $user_id } }) {
status
event_id
user_id
}
}
}
,
variables: { event_id, user_id },
}),
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
// create a websocket connection when the cache subscription starts
const ws = new WebSocket("ws://localhost:8080");
try {
// wait for the initial query to resolve before proceeding
await cacheDataLoaded;
// when data is received from the socket connection to the server,
// if it is a message and for the appropriate channel,
// update our query result with the received message
const listener = (event: MessageEvent) => {
const data = JSON.parse(event.data);
console.log("This is the data from the subscription!", data);
if (data.channel !== arg) return;
updateCachedData((draft) => {
draft = data;
});
};
ws.addEventListener("message", listener);
} catch {
// no-op in case cacheEntryRemoved resolves before cacheDataLoaded,
// in which case cacheDataLoaded will throw
}
// cacheEntryRemoved will resolve when the cache subscription is no longer active
await cacheEntryRemoved;
// perform cleanup steps once the cacheEntryRemoved promise resolves
ws.close();
},
}),
What I found out from the Discord chat that there currently is no support for GraphQL Subscriptions. Discord chat link found on the official website
No subscription support at the moment, but you can build something like that using the chat example from the docs
I cite phryneas (he/him) — 06/11/2021
There is also a github issue, which was closed saying this is not possible and you should use a library like urql or apollo. link
I hope this helps, was looking for an answer for ages.
I have an existing Express ApolloServer . I added subscription to that . I can see when I fire the subscription from Playground, the resolve method is called . But, the subscribe method is never called
const { PubSub, withFilter } = require ('apollo-server');
const pubsub = new PubSub();
const SOMETHING_CHANGED_TOPIC = 'something_changed';
const mySubscription = {
Subscription: {
somethingChanged: {
resolve: root => {
console.log('subscription server resolve', { root })
return root
},
subscribe: () => {
console.log('I AM HERE IN SUBSCRIPTION', pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC))
return pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC)
}
}
}
};
module.exports = { mySubscription}
I can see the console.log('subscription server resolve', { root }) getting printed although root is undefined. But the similar console.log('````') in subscribe is not executed .
You need to call pubsub.publish somewhere (usually in one of your resolvers) to trigger the subscription.
https://www.apollographql.com/docs/apollo-server/data/subscriptions/#subscriptions-example
I had a similar issue, there were a couple things I needed to achieve to accomplish this.
I had to remove my resolve() function to get it working. For some reason, having the resolve() function defined caused my subscription not to work.
I also had failed to follow the
https://www.apollographql.com/docs/react/data/subscriptions/#setting-up-the-transport. I was trying to request my subscription over the http link instead of over a ws link.
In general, a good test to see where the issue lies is to try to subscribe to your message using the GraphQL sandbox at http://localhost:4000/graphql (or wherever your sandbox is setup to run at when you start your server). If the sandbox subscribes successfully, the issue lies in your client code. If the sandbox fails to subscribe, the issue lies in your server code.
Please let me know if you are still having the issue and I will try to help.
I create a websocket and then a custom route. Before publishing I need to select an integration for $disconnect and $default, for both I choose Mock (I have also tried default Lambda functions), this allows me to publish.
I then use wscat to call
wscat -c wss://t0p5b2xpm3.execute-api.us-east-1.amazonaws.com/prod
the socket connects successfully,then i try to call the route
{"action":"echo", "data":"test response body"}
and get the following error.
{"message": "Internal server error", "connectionId":"aDH97cQJoAMCI8Q=", "requestId":"aDIAhGE8oAMFoEg="}
anyone have any ideas please?
thanks,
Matt
For a lambda function, you need to return a statusCode of 200 for $connect, $disconnect, $default, and custom routes. A trivial javascript example:
module.exports.handler = async (event) => {
return {
statusCode: 200,
};
};
exports.handler = async (event) => {
// TODO implement
let time = "undefined";
let state = "undefined";
if (event.body !== null && event.body !== undefined) {
let body = JSON.parse(event.body);
if (body.time && body.state)
{
time = body.time;
state = body.state;
}
}
const response = {
statusCode: 200,
body: JSON.stringify({time:time, state: state}),
};
return response;
};
In case of API-Gateway you won't be able to modify the response as json as in Get and Post, so you can use this code to fetch time and state from the coming json object. Connect using wscat -c link and then send your json as {"time":"22:32","state":"LA"}
Try to put in $default to make sure that you are receiving the sending the right way.
Good luck!
I'm looking into implementing a "subscription" type using server-sent events as the backing api.
What I'm struggling with is the interface, to be more precise, the http layer of such operation.
The problem:
Using the native EventSource does not support:
Specifying an HTTP method, "GET" is used by default.
Including a payload (The GraphQL query)
While #1 is irrefutable, #2 can be circumvented using query parameters.
Query parameters have a limit of ~2000 chars (can be debated)
which makes relying solely on them feels too fragile.
The solution I'm thinking of is to create a dedicated end-point for each possible event.
For example: A URI for an event representing a completed transaction between parties:
/graphql/transaction-status/$ID
Will translate to this query in the server:
subscription TransactionStatusSubscription {
status(id: $ID) {
ready
}
}
The issues with this approach is:
Creating a handler for each URI-to-GraphQL translation is to be added.
Deploy a new version of the server
Loss of the flexibility offered by GraphQL -> The client should control the query
Keep track of all the end-points in the code base (back-end, front-end, mobile)
There are probably more issues I'm missing.
Is there perhaps a better approach that you can think of?
One the would allow a better approach at providing the request payload using EventSource?
Subscriptions in GraphQL are normally implemented using WebSockets, not SSE. Both Apollo and Relay support using subscriptions-transport-ws client-side to listen for events. Apollo Server includes built-in support for subscriptions using WebSockets. If you're just trying to implement subscriptions, it would be better to utilize one of these existing solutions.
That said, there's a library for utilizing SSE for subscriptions here. It doesn't look like it's maintained anymore, but you can poke around the source code to get some ideas if you're bent on trying to get SSE to work. Looking at the source, it looks like the author got around the limitations you mention above by initializing each subscription with a POST request that returns a subscription id.
As of now you have multiple Packages for GraphQL subscription over SSE.
graphql-sse
Provides both client and server for using GraphQL subscription over SSE. This package has a dedicated handler for subscription.
Here is an example usage with express.
import express from 'express'; // yarn add express
import { createHandler } from 'graphql-sse';
// Create the GraphQL over SSE handler
const handler = createHandler({ schema });
// Create an express app serving all methods on `/graphql/stream`
const app = express();
app.use('/graphql/stream', handler);
app.listen(4000);
console.log('Listening to port 4000');
#graphql-sse/server
Provides a server handler for GraphQL subscription. However, the HTTP handling is up to u depending of the framework you use.
Disclaimer: I am the author of the #graphql-sse packages
Here is an example with express.
import express, { RequestHandler } from "express";
import {
getGraphQLParameters,
processSubscription,
} from "#graphql-sse/server";
import { schema } from "./schema";
const app = express();
app.use(express.json());
app.post(path, async (req, res, next) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
const { operationName, query, variables } = getGraphQLParameters(request);
if (!query) {
return next();
}
const result = await processSubscription({
operationName,
query,
variables,
request: req,
schema,
});
if (result.type === RESULT_TYPE.NOT_SUBSCRIPTION) {
return next();
} else if (result.type === RESULT_TYPE.ERROR) {
result.headers.forEach(({ name, value }) => res.setHeader(name, value));
res.status(result.status);
res.json(result.payload);
} else if (result.type === RESULT_TYPE.EVENT_STREAM) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
});
result.subscribe((data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
req.on('close', () => {
result.unsubscribe();
});
}
});
Clients
The two packages mentioned above have companion clients. Because of the limitation of the EventSource API, both packages implement a custom client that provides options for sending HTTP Headers, payload with post, what the EvenSource API does not support. The graphql-sse comes together with it client while the #graphql-sse/server has companion clients in a separate packages.
graphql-sse client example
import { createClient } from 'graphql-sse';
const client = createClient({
// singleConnection: true, use "single connection mode" instead of the default "distinct connection mode"
url: 'http://localhost:4000/graphql/stream',
});
// query
const result = await new Promise((resolve, reject) => {
let result;
client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});
// subscription
const onNext = () => {
/* handle incoming values */
};
let unsubscribe = () => {
/* complete the subscription */
};
await new Promise((resolve, reject) => {
unsubscribe = client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
;
#graphql-sse/client
A companion of the #graphql-sse/server.
Example
import {
SubscriptionClient,
SubscriptionClientOptions,
} from '#graphql-sse/client';
const subscriptionClient = SubscriptionClient.create({
graphQlSubscriptionUrl: 'http://some.host/graphl/subscriptions'
});
const subscription = subscriptionClient.subscribe(
{
query: 'subscription { greetings }',
}
)
const onNext = () => {
/* handle incoming values */
};
const onError = () => {
/* handle incoming errors */
};
subscription.susbscribe(onNext, onError)
#gaphql-sse/apollo-client
A companion package of the #graph-sse/server package for Apollo Client.
import { split, HttpLink, ApolloClient, InMemoryCache } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { ServerSentEventsLink } from '#graphql-sse/apollo-client';
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
const sseLink = new ServerSentEventsLink({
graphQlSubscriptionUrl: 'http://localhost:4000/graphql',
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
sseLink,
httpLink
);
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
If you're using Apollo, they support automatic persisted queries (abbreviated APQ in the docs). If you're not using Apollo, the implementation shouldn't be too bad in any language. I'd recommend following their conventions just so your clients can use Apollo if they want.
The first time any client makes an EventSource request with a hash of the query, it'll fail, then retry the request with the full payload to a regular GraphQL endpoint. If APQ is enabled on the server, subsequent GET requests from all clients with query parameters will execute as planned.
Once you've solved that problem, you just have to make a server-sent events transport for GraphQL (should be easy considering the subscribe function just returns an AsyncIterator)
I'm looking into doing this at my company because some frontend developers like how easy EventSource is to deal with.
There are two things at play here: the SSE connection and the GraphQL endpoint. The endpoint has a spec to follow, so just returning SSE from a subscription request is not done and needs a GET request anyway. So the two have to be separate.
How about letting the client open an SSE channel via /graphql-sse, which creates a channel token. Using this token the client can then request subscriptions and the events will arrive via the chosen channel.
The token could be sent as the first event on the SSE channel, and to pass the token to the query, it can be provided by the client in a cookie, a request header or even an unused query variable.
Alternatively, the server can store the last opened channel in session storage (limiting the client to a single channel).
If no channel is found, the query fails. If the channel closes, the client can open it again, and either pass the token in the query string/cookie/header or let the session storage handle it.
I would like to instantiate a turnContext to be used in integration testing. How would I be able to instantiate one without calling on the processActivity() method of the adapter?
I am looking at the documentation but it shows that I would need the request of the post call as the parameter. I would like my testing to be independant of the post call. I would then assume that I would need to instantiate the request? How would I go about doing so?
Image of documentation
This is a bit hard to answer without knowing how you are planning to use the code. That being said, it's not that hard to create a new turnContext and also bypass the processActivity(). Given how you are referencing turnContext and processActivity(), I'm assuming you are using the Node SDK. Implementing in C# wouldn't be too different.
Here are two options, both utilizing the creation of a new adapter, however you can also pass in an already established turnContext, if desired:
Use .createContext in server.post in the index.js file, or
Maintain the processActivity() method in the server.post. This calls a new "onTurn" method in the bot.js file. In doing so, this allows you to control when and how the new "onTurn" is accessed.
Option 1: In the index.js file, you will want to create a new adapter or make a copy of the first depending on your needs:
const adapter = new BotFrameworkAdapter({
appId: endpointConfig.appId || process.env.MicrosoftAppId,
appPassword: endpointConfig.appPassword || process.env.MicrosoftAppPassword
});
const newAdapter = adapter;
or
const adapter = new BotFrameworkAdapter({
appId: endpointConfig.appId || process.env.MicrosoftAppId,
appPassword: endpointConfig.appPassword || process.env.MicrosoftAppPassword
});
const newAdapter = new BotFrameworkAdapter({
appId: endpointConfig.appId || process.env.MicrosoftAppId,
appPassword: endpointConfig.appPassword || process.env.MicrosoftAppPassword
});
Include the onTurnError code to catch errors:
// Catch-all for errors.
adapter.onTurnError = async (context, error) => {
console.error(`\n [onTurnError]: ${ error }`);
await context.sendActivity(`Oops. Something went wrong!`);
};
// Catch-all for errors.
newAdapter.onTurnError = async (context, error) => {
console.error(`\n [onTurnError]: ${ error }`);
await context.sendActivity(`Oops. Something went wrong!`);
};
Then, set the new adapters and create the new turnContext:
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (turnContext) => {
await bot.onTurn(turnContext);
});
newAdapter.createContext(req, res);
});
Options 2: In the index.js file, building off of the above code, set the adapters to await the individual "onTurn" methods:
// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (turnContext) => {
await bot.onTurn(turnContext);
});
newAdapter.processActivity(req, res, async (turnContext) => {
await bot.newOnTurn(turnContext);
});
});
In the bot.js file, you will have your two "onTurn" methods. In this example, the different "onTurn" methods are called based on whether a message is sent or I am deleting user data (I am sending this event via the Emulator => Conversation menu item). What you decide to match on is up to you.
async newOnTurn(turnContext) {
if (turnContext.activity.type === ActivityTypes.DeleteUserData) {
const dc = await this.dialogs.createContext(turnContext);
await dc.context.sendActivity(`Looks like you deleted some user data.`);
}
}
async onTurn(turnContext) {
if (turnContext.activity.type === ActivityTypes.Message) {
const dc = await this.dialogs.createContext(turnContext);
await dc.context.sendActivity(`Looks like you sent a message.`);
}
}
Hope of help!