Skip to content
Draft
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
70 changes: 28 additions & 42 deletions src/graphql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { SubscriptionClient } from 'subscriptions-transport-ws'
import { createClient } from 'graphql-ws'
import {
ApolloClient,
ApolloLink,
HttpLink,
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.
Expand All @@ -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
}

/**
Expand All @@ -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) {
Expand All @@ -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(
Expand Down
63 changes: 28 additions & 35 deletions src/services/mock/websockets.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,55 +15,48 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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}`)
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/workflow.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
66 changes: 21 additions & 45 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
Loading