diff --git a/package.json b/package.json
index 5f53d067a..0cbbaab41 100644
--- a/package.json
+++ b/package.json
@@ -37,13 +37,13 @@
"graphiql": "4.1.2",
"graphql": "16.13.2",
"graphql-tag": "2.12.6",
+ "graphql-ws": "6.0.4",
"lodash-es": "4.18.1",
"markdown-it": "14.1.1",
"mitt": "3.0.1",
"nprogress": "1.0.0-1",
"preact": "10.27.3",
"simple-icons": "16.10.0",
- "subscriptions-transport-ws": "0.11.0",
"svg-pan-zoom": "3.6.2",
"vue": "3.5.33",
"vue-i18n": "11.1.12",
diff --git a/src/graphql/index.js b/src/graphql/index.js
index ab3591d0e..fc8ab2523 100644
--- a/src/graphql/index.js
+++ b/src/graphql/index.js
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-import { SubscriptionClient } from 'subscriptions-transport-ws'
+import { createClient } from 'graphql-ws'
import {
ApolloClient,
ApolloLink,
@@ -23,13 +23,12 @@ import {
InMemoryCache,
split
} from '@apollo/client/core'
+import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { getMainDefinition } from '@apollo/client/utilities'
-import { WebSocketLink } from '@apollo/client/link/ws'
import { setContext } from '@apollo/client/link/context'
import { store } from '@/store/index'
import { createUrl, getXSRFHeaders } from '@/utils/urls'
-
-/** @typedef {import('subscriptions-transport-ws').ClientOptions} ClientOptions */
+import { uniqueId } from 'lodash-es'
/**
* Create the HTTP and WebSocket URLs for an ApolloClient.
@@ -50,45 +49,32 @@ export function createGraphQLUrls () {
* Create a subscription client.
*
* @private
- * @param {string} wsUrl - WebSocket subscription URL
- * @param {ClientOptions} options - SubscriptionClient options
- * @param {*} wsImpl - WebSocket implementation (native by default)
- * @return {SubscriptionClient} a subscription client
+ * @param {string} url - WebSocket subscription URL
+ * @return {import('graphql-ws').Client} a subscription client
*/
-export function createSubscriptionClient (wsUrl, options = {}, wsImpl = null) {
- /** @type {ClientOptions} */
- const opts = {
- reconnect: true,
+export function createSubscriptionClient (url) {
+ return createClient({
+ url,
lazy: false,
- // Raise initial connection timeout from 1->3 secs to try to mitigate slow connection problem
- // https://github.com/cylc/cylc-ui/issues/1200
- minTimeout: 3e3,
- ...options,
- }
- const subscriptionClient = new SubscriptionClient(wsUrl, opts, wsImpl)
- // these are the available hooks in the subscription client lifecycle
- subscriptionClient.onConnecting(() => {
- store.commit('SET_OFFLINE', true)
- })
- subscriptionClient.onConnected(() => {
- store.commit('SET_OFFLINE', false)
- })
- subscriptionClient.onReconnecting(() => {
- store.commit('SET_OFFLINE', true)
- })
- subscriptionClient.onReconnected(() => {
- store.commit('SET_OFFLINE', false)
- })
- subscriptionClient.onDisconnected(() => {
- store.commit('SET_OFFLINE', true)
+ on: {
+ connecting (isRetry) {
+ if (isRetry) console.warn('Retrying WS connection')
+ store.commit('SET_OFFLINE', true)
+ },
+ connected (socket, payload, wasRetry) {
+ store.commit('SET_OFFLINE', false)
+ },
+ closed (event) {
+ store.commit('SET_OFFLINE', true)
+ },
+ error (error) {
+ console.error(error)
+ store.commit('SET_OFFLINE', true)
+ // TODO: store.commit('SET_ALERT') with the error details?
+ },
+ },
+ generateID: (payload) => uniqueId(payload.operationName)
})
- // TODO: at the moment the error displays an Event object, but the browser also displays the problem, as well as the offline indicator
- // would be nice to find a better error message using the error object
- // subscriptionClient.onError((error) => {
- // console.error(error)
- // store.commit('SET_ALERT', new Alert(error, 'error'))
- // })
- return subscriptionClient
}
/**
@@ -107,7 +93,7 @@ export function createSubscriptionClient (wsUrl, options = {}, wsImpl = null) {
*
* @public
* @param {string} httpUrl
- * @param {SubscriptionClient|null} subscriptionClient
+ * @param {?import('graphql-ws').Client} subscriptionClient
* @returns {ApolloClient} an ApolloClient
*/
export function createApolloClient (httpUrl, subscriptionClient) {
@@ -116,7 +102,7 @@ export function createApolloClient (httpUrl, subscriptionClient) {
})
const wsLink = subscriptionClient !== null
- ? new WebSocketLink(subscriptionClient)
+ ? new GraphQLWsLink(subscriptionClient)
: new ApolloLink() // return an empty link, useful for testing, offline mode, etc
const link = split(
diff --git a/src/services/mock/websockets.cjs b/src/services/mock/websockets.cjs
index 1334d9b0d..c84f4460f 100644
--- a/src/services/mock/websockets.cjs
+++ b/src/services/mock/websockets.cjs
@@ -15,55 +15,48 @@
* along with this program. If not, see .
*/
+const { MessageType } = require('graphql-ws')
const { isArray } = require('lodash')
const graphql = require('./graphql.cjs')
-/**
- * Create a WebSockets response.
- *
- * @param {string} id - Subscription ID
- * @param {string} type - Message type (e.g. data, connection_init, etc, see GraphQL spec)
- * @param {Object} [data] - Response data, optional
- * @returns {string}
- */
-function wsResponse (id, type, data = null) {
- const response = {
- id,
- type
- }
- // connection ack does not include a payload
- if (data) {
- response.payload = {
- data
- }
- }
- return JSON.stringify(response)
-}
-
/**
* Send a WebSockets reply message(s), given the query message (received from client).
*
+ * @see https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
+ *
* @param {WebSocket} ws
* @param {string} msg - JSON encoded client message
*/
async function sendWSResponse (ws, msg) {
+ /** @type {import('graphql-ws').Message} */
const parsed = JSON.parse(msg)
if (parsed) {
- if (parsed.type === 'connection_init') {
- return ws.send(wsResponse(parsed.id, 'connection_ack'))
- } else if (parsed.type === 'stop') {
- return ws.send(wsResponse(parsed.id, 'complete'))
- } else if (parsed.type === 'start') {
- const operationName = (
- parsed.payload.operationName || graphql.getOperationName(parsed.payload.query)
- )
- const responseData = await graphql.getGraphQLQueryResponse(operationName, parsed.payload.variables)
- for (const item of isArray(responseData) ? responseData : [responseData]) {
- ws.send(wsResponse(parsed.id, 'data', item))
+ switch (parsed.type) {
+ case MessageType.ConnectionInit:
+ return ws.send(JSON.stringify({ type: MessageType.ConnectionAck }))
+ case MessageType.Ping:
+ return ws.send(JSON.stringify({ type: MessageType.Pong }))
+ case MessageType.Pong:
+ case MessageType.Complete:
+ return
+ case MessageType.Subscribe: {
+ const operationName = (
+ parsed.payload.operationName || graphql.getOperationName(parsed.payload.query)
+ )
+ const responseData = await graphql.getGraphQLQueryResponse(operationName, parsed.payload.variables)
+ for (const item of isArray(responseData) ? responseData : [responseData]) {
+ ws.send(JSON.stringify({
+ id: parsed.id,
+ type: MessageType.Next,
+ payload: { data: item },
+ }))
+ }
+ return
+ }
+ default: {
+ throw new Error(`Invalid message type for graphql-transport-ws: ${parsed.type}`)
}
- return
}
- throw new Error(`Unknown message type ${parsed.type}`)
}
throw new Error(`Failed to parse msg: ${msg}`)
}
diff --git a/src/services/workflow.service.js b/src/services/workflow.service.js
index b8cfcf4d1..88f3cf7d8 100644
--- a/src/services/workflow.service.js
+++ b/src/services/workflow.service.js
@@ -41,7 +41,7 @@ import CylcTreeCallback from '@/services/treeCallback'
/** @typedef {import('graphql').DocumentNode} DocumentNode */
/** @typedef {import('graphql').IntrospectionInputType} IntrospectionInputType */
-/** @typedef {import('subscriptions-transport-ws').SubscriptionClient} SubscriptionClient */
+/** @typedef {import('graphql-ws').Client} SubscriptionClient */
/** @typedef {import('@/utils/aotf').Mutation} Mutation */
/** @typedef {import('@/utils/aotf').MutationResponse} MutationResponse */
/** @typedef {import('@/utils/aotf').Query} Query */
diff --git a/yarn.lock b/yarn.lock
index 5f0e526c3..6c953f2ed 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3348,13 +3348,6 @@ __metadata:
languageName: node
linkType: hard
-"backo2@npm:^1.0.2":
- version: 1.0.2
- resolution: "backo2@npm:1.0.2"
- checksum: 10c0/a9e825a6a38a6d1c4a94476eabc13d6127dfaafb0967baf104affbb67806ae26abbb58dab8d572d2cd21ef06634ff57c3ad48dff14b904e18de1474cc2f22bf3
- languageName: node
- linkType: hard
-
"balanced-match@npm:^1.0.0":
version: 1.0.2
resolution: "balanced-match@npm:1.0.2"
@@ -4184,6 +4177,7 @@ __metadata:
graphiql: "npm:4.1.2"
graphql: "npm:16.13.2"
graphql-tag: "npm:2.12.6"
+ graphql-ws: "npm:6.0.4"
istanbul-lib-coverage: "npm:3.2.2"
jsdom: "npm:29.0.2"
json-server: "npm:0.17.4"
@@ -4198,7 +4192,6 @@ __metadata:
simple-icons: "npm:16.10.0"
sinon: "npm:21.1.2"
standard: "npm:17.1.2"
- subscriptions-transport-ws: "npm:0.11.0"
svg-pan-zoom: "npm:3.6.2"
vite: "npm:7.3.2"
vite-plugin-eslint: "npm:1.8.1"
@@ -5444,13 +5437,6 @@ __metadata:
languageName: node
linkType: hard
-"eventemitter3@npm:^3.1.0":
- version: 3.1.2
- resolution: "eventemitter3@npm:3.1.2"
- checksum: 10c0/c67262eccbf85848b7cc6d4abb6c6e34155e15686db2a01c57669fd0d44441a574a19d44d25948b442929e065774cbe5003d8e77eed47674fbf876ac77887793
- languageName: node
- linkType: hard
-
"execa@npm:4.1.0":
version: 4.1.0
resolution: "execa@npm:4.1.0"
@@ -6248,6 +6234,25 @@ __metadata:
languageName: node
linkType: hard
+"graphql-ws@npm:6.0.4":
+ version: 6.0.4
+ resolution: "graphql-ws@npm:6.0.4"
+ peerDependencies:
+ "@fastify/websocket": ^10 || ^11
+ graphql: ^15.10.1 || ^16
+ uWebSockets.js: ^20
+ ws: ^8
+ peerDependenciesMeta:
+ "@fastify/websocket":
+ optional: true
+ uWebSockets.js:
+ optional: true
+ ws:
+ optional: true
+ checksum: 10c0/ed17502300c702d42820ca2acc593d82acbcbec91fa93e588dc008d07d7b6914b4b22062f1ee181cff6ac62f69ea0052555ee75f270601311b943a6b7ef709dc
+ languageName: node
+ linkType: hard
+
"graphql@npm:16.13.2":
version: 16.13.2
resolution: "graphql@npm:16.13.2"
@@ -7033,13 +7038,6 @@ __metadata:
languageName: node
linkType: hard
-"iterall@npm:^1.2.1":
- version: 1.3.0
- resolution: "iterall@npm:1.3.0"
- checksum: 10c0/40de624e5fe937c4c0e511981b91caea9ff2142bfc0316cccc8506eaa03aa253820cc17c5bc5f0a98706c7268a373e5ebee9af9a0c8a359730cf7c05938b57b5
- languageName: node
- linkType: hard
-
"iterator.prototype@npm:^1.1.4":
version: 1.1.5
resolution: "iterator.prototype@npm:1.1.5"
@@ -10444,21 +10442,6 @@ __metadata:
languageName: node
linkType: hard
-"subscriptions-transport-ws@npm:0.11.0":
- version: 0.11.0
- resolution: "subscriptions-transport-ws@npm:0.11.0"
- dependencies:
- backo2: "npm:^1.0.2"
- eventemitter3: "npm:^3.1.0"
- iterall: "npm:^1.2.1"
- symbol-observable: "npm:^1.0.4"
- ws: "npm:^5.2.0 || ^6.0.0 || ^7.0.0"
- peerDependencies:
- graphql: ^15.7.2 || ^16.0.0
- checksum: 10c0/697441333e59b6932bff51212e29f8dcac477badb067971bd94c30c5f3f7a2e2ea72fb1a21f3c1abbf32774da01515aa24739e620be45f6d576784bd96fd10da
- languageName: node
- linkType: hard
-
"supports-color@npm:8.1.1, supports-color@npm:^8.1.1":
version: 8.1.1
resolution: "supports-color@npm:8.1.1"
@@ -10571,13 +10554,6 @@ __metadata:
languageName: node
linkType: hard
-"symbol-observable@npm:^1.0.4":
- version: 1.2.0
- resolution: "symbol-observable@npm:1.2.0"
- checksum: 10c0/009fee50798ef80ed4b8195048288f108b03de162db07493f2e1fd993b33fafa72d659e832b584da5a2427daa78e5a738fb2a9ab027ee9454252e0bedbcd1fdc
- languageName: node
- linkType: hard
-
"symbol-observable@npm:^4.0.0":
version: 4.0.0
resolution: "symbol-observable@npm:4.0.0"
@@ -11839,7 +11815,7 @@ __metadata:
languageName: node
linkType: hard
-"ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0, ws@npm:^7.4.6":
+"ws@npm:^7.4.6":
version: 7.5.10
resolution: "ws@npm:7.5.10"
peerDependencies: