RFC: Generic websockets infrastructure implementation
Scope
As the demand for real-time features starts to increase (e.g. locking resources, user online indicator, collaboration in document editing) we should introduce a robust mechanism of handling a variety of different websocket
connections. In order for this to be possible, modification as well as additions should be implemented.
1. Generic Implementation
The end goal here is to provide an easy mechanism that will allow users of the Coko server to declare on their project side (configuration) if they want to instantiate web sockets between their clients and their server.
As a configuration parameter developers will be able to declare the pathname where a specific websockets
server should listen and Coko server will instantiate and expose them for further use in the project's backend. e.g const specificPurposeWebSockets = [{path:'locks'},{path:'test'}]
In the existing server's initialization we should implement something like the following:
const createdWS = {};
specificPurposeWebSockets.forEach(({ path }) => {
createdWS[path] = new WebSocketServer({ noServer: true });
});
server.on("upgrade", function upgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;
specificPurposeWebSockets.forEach(({ path }) => {
if (pathname === `/${path}`) {
return createdWS[path].handleUpgrade(request, socket, head, (ws) => {
createdWS[path].emit("connection", ws, request, request.client);
});
}
});
});
The above snippet will allow us to provided multiple websocket
servers via using a single http
server as described here https://github.com/websockets/ws/tree/966f9d47cd0ff5aa9db0b2aa262f9819d3f4d414#multiple-servers-sharing-a-single-https-server
Additionally, token based client authentication logic should be implemented inside the upgrade
event listener of the http
server in order for unauthorized socket initializations to be discarded e.g https://github.com/websockets/ws/tree/966f9d47cd0ff5aa9db0b2aa262f9819d3f4d414#client-authentication
The createdWS
object will be exposed by the Coko server and developers will be able to listen for all the regular events of a websocket
as presented below
createdWS["locks"].on("connection", (ws, request, client) => {
console.log("ready", client.readyState);
ws.on("message", function message(data) {
console.log(`Received message ${data} from user ${client}`);
ws.send(data);
});
ws.on("close", () => {
console.log("ws close");
});
ws.on("error", (err) => {
console.log("error", err);
});
});
createdWS["test"].on("connection", (ws, request, client) => {
ws.on("message", function message(data) {
console.log(`Received message ${data} from user ${client}`);
ws.send(data);
});
ws.on("close", () => {
console.log("ws close");
});
ws.on("error", (err) => {
console.log("error", err);
});
});
This will allow developers to declare their custom methods based on their use-cases in each event listener of their declared websockets
.
graphql
's subscriptions
2. Special case of Modifications should be implemented in the existing source that offers graphql
subscriptions. Based on Apollo documentation we should switch from our existing library subscriptions-transport-ws
to graphql-ws
. This will affect the implementation in https://gitlab.coko.foundation/pubsweet/pubsweet/-/blob/master/packages/server/src/graphql/subscriptions.js in order to make use of the new library as well as https://gitlab.coko.foundation/pubsweet/pubsweet/-/blob/master/packages/client/src/components/Root.js
-
For the server we should implement the changes based on these examples https://github.com/enisdenjo/graphql-ws#multi-ws and https://github.com/enisdenjo/graphql-ws#apollo-server-express
-
For the client the modification should be based on https://github.com/enisdenjo/graphql-ws#apollo-client, https://github.com/enisdenjo/graphql-ws#retry-non-close-events, https://github.com/enisdenjo/graphql-ws#retry-strategy
Furthermore, due to this limitation https://github.com/GraphQLCollege/graphql-postgres-subscriptions/issues/13 of our graphql-postgres-subscriptions
dependency we should definitely discuss switching that library in favor of https://github.com/davidyaha/graphql-redis-subscriptions as suggested here https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries
The specific websocket
server which will be dedicated to graphql
should not be exported as its functionality should be considered given for anyone who is using coko server with graphql
feature set to on
.
yjs
support
3. Special case of More info will be added here as things clear out. The implementation should be based on https://github.com/yjs/y-websocket/blob/master/bin/utils.js.
A decision should be made if the implementation should be provided as a configurable, (what aspects makes sense to be configurable?), black box from Coko server OR fall under the generic case 1 (above) where Coko server will just initiate a certain websocket
server (by declaring a specific path) and pass it down in order for developers to register listeners and handlers on their project's scope.