diff --git a/app/graphql/schema/resolvers.ts b/app/graphql/schema/resolvers.ts index 4ef1c7f9c..9153e281e 100644 --- a/app/graphql/schema/resolvers.ts +++ b/app/graphql/schema/resolvers.ts @@ -4,17 +4,15 @@ */ import { pluginManager, pubsub, utils, bus, errors, states, modules, apiManager, log } from '@unraid/core'; -import * as Sentry from '@sentry/node'; import dee from '@gridplus/docker-events'; import { setIntervalAsync } from 'set-interval-async/dynamic'; import GraphQLJSON from 'graphql-type-json'; import GraphQLLong from 'graphql-type-long'; import GraphQLUUID from 'graphql-type-uuid'; -import fetch from 'cross-fetch'; import { run, publish } from '../../run'; import { userCache, CachedServer, CachedServers } from '../../cache'; import { hasSubscribedToChannel } from '../../ws'; -import { MOTHERSHIP_GRAPHQL_LINK } from '../../consts'; +import { getServers as getUserServers } from '../../utils' const { ensurePermission } = utils; const { usersState, varState, networkState } = states; @@ -102,30 +100,12 @@ const getServers = async (): Promise => { // For now use the my_servers key // Later we should return the correct one for the current user with the correct scope, etc. - const apiKey = apiManager.getValidKeys().find(key => key.name === 'my_servers')?.key.toString(); + const apiKey = apiManager.getValidKeys().find(key => key.name === 'my_servers')?.key.toString()!; // No cached servers found if (!cachedServers) { // Fetch servers from mothership - const servers = await fetch(MOTHERSHIP_GRAPHQL_LINK, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify({ - query: 'query($apiKey: String!) { servers @auth(apiKey: $apiKey) { owner { username url avatar } guid apikey name status wanip lanip localurl remoteurl } }', - variables: { - apiKey - } - }) - }) - .then(r => r.json()) - .then(({ data }) => data.servers as Promise) - .catch(error => { - Sentry.captureException(error); - return []; - }); + const servers = await getUserServers(apiKey); log.debug('Using upstream for /servers endpoint'); diff --git a/app/mothership/index.ts b/app/mothership/index.ts index df2534ca1..4a3314024 100644 --- a/app/mothership/index.ts +++ b/app/mothership/index.ts @@ -4,7 +4,8 @@ import * as Sentry from '@sentry/node'; import { utils, paths, states, log } from '@unraid/core'; import { DynamixConfig } from '@unraid/core/dist/lib/types'; import { MOTHERSHIP_RELAY_WS_LINK, INTERNAL_WS_LINK, ONE_MINUTE } from '../consts'; -import { subscribeToServersEndpoint } from './subscribe-to-servers-endpoint'; +import { subscribeToServer } from './subscribe-to-server'; +import { getServers as getUserServers } from '../utils'; const { loadState } = utils; const { varState } = states; @@ -81,9 +82,9 @@ export const connectToMothership = async (wsServer: WebSocket.Server, currentRet const lanIp = states.networkState.data.find(network => network.ipaddr[0]).ipaddr[0] || ''; const machineId = `${await utils.getMachineId()}`; let localGraphqlApi: WebSocket; - let mothershipServersEndpoint: { + let mothershipServersEndpoints: { unsubscribe: () => void; - }; + }[]; // Connect to mothership's relay endpoint // Keep reference outside this scope so we can disconnect later @@ -148,8 +149,14 @@ export const connectToMothership = async (wsServer: WebSocket.Server, currentRet } }); - // Connect to mothership's "servers" endpoint - mothershipServersEndpoint = subscribeToServersEndpoint(apiKey); + // Get servers + const servers = await getUserServers(apiKey); + if (servers) { + servers.forEach(server => { + // Subscribe to online for each server + mothershipServersEndpoints.push(subscribeToServer(server.apikey)); + }); + } }); // Relay is closed @@ -168,7 +175,9 @@ export const connectToMothership = async (wsServer: WebSocket.Server, currentRet relay?.removeAllListeners(); // Stop subscriptions with mothership - mothershipServersEndpoint.unsubscribe(); + mothershipServersEndpoints.forEach(endpoint => { + endpoint.unsubscribe(); + }); // We likely closed this // This is usually because the API key is updated diff --git a/app/mothership/subscribe-to-server.ts b/app/mothership/subscribe-to-server.ts new file mode 100644 index 000000000..51cc3c280 --- /dev/null +++ b/app/mothership/subscribe-to-server.ts @@ -0,0 +1,56 @@ +import * as Sentry from '@sentry/node'; +import { pubsub } from '@unraid/core'; +import { SubscriptionClient } from 'graphql-subscriptions-client'; +import { MOTHERSHIP_GRAPHQL_LINK } from '../consts'; +import { userCache, CachedServers } from '../cache'; +import { getServers } from '../utils'; + +const client = new SubscriptionClient(MOTHERSHIP_GRAPHQL_LINK, { + reconnect: true, + lazy: true, // only connect when there is a query + connectionCallback: (error) => { + if (error) { + Sentry.captureException(error); + } + } +}); + +export const subscribeToServer = (apiKey: string) => { + // For each server subscribe to it's endpoint + return client.request({ + query: `subscription status ($apiKey: String!) { + status @auth(apiKey: $apiKey) + }`, + variables: { + apiKey + } + }) + .subscribe({ + next({ data, errors }) { + if (errors) { + // Send all errors to Sentry + errors.forEach((error: Error) => { + Sentry.captureException(error); + }); + } + // Only update if we actually have data + if (data && data.status) { + const servers = userCache.get('mine'); + const server = servers?.servers.find(server => server.apikey === apiKey); + if (server && server.status && server.status !== data.status) { + getServers(apiKey).then(servers => { + if (servers) { + // Update internal cache + userCache.set('mine', { + servers + }); + + // Update subscribers + pubsub.publish('servers', servers); + } + }); + } + } + } + }); +}; diff --git a/app/mothership/subscribe-to-servers-endpoint.ts b/app/mothership/subscribe-to-servers-endpoint.ts deleted file mode 100644 index 3cfa2a3ea..000000000 --- a/app/mothership/subscribe-to-servers-endpoint.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { pubsub } from '@unraid/core'; -import { SubscriptionClient } from 'graphql-subscriptions-client'; -import { MOTHERSHIP_GRAPHQL_LINK } from '../consts'; -import { userCache, CachedServers } from '../cache'; - -const client = new SubscriptionClient(MOTHERSHIP_GRAPHQL_LINK, { - reconnect: true, - lazy: true, // only connect when there is a query - connectionCallback: (error) => { - if (error) { - Sentry.captureException(error); - } - } -}); - -export const subscribeToServersEndpoint = (apiKey: string) => { - return client.request({ - query: `subscription servers ($apiKey: String!) { - servers @auth(apiKey: $apiKey) - }`, - variables: { - apiKey - } - }) - .subscribe({ - next({ data, errors }) { - if (errors) { - // Send all errors to Sentry - errors.forEach((error: Error) => { - Sentry.captureException(error); - }); - } - if (data) { - const obj = { - servers: data.servers - }; - - // Update internal cache - userCache.set('mine', obj); - // Update subscribers - pubsub.publish('servers', obj); - } - } - }); -}; diff --git a/app/utils.ts b/app/utils.ts new file mode 100644 index 000000000..137b876ae --- /dev/null +++ b/app/utils.ts @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node'; +import { MOTHERSHIP_GRAPHQL_LINK } from './consts'; +import { CachedServer } from './cache'; + +export const getServers = (apiKey: string) => fetch(MOTHERSHIP_GRAPHQL_LINK, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + query: 'query($apiKey: String!) { servers @auth(apiKey: $apiKey) { owner { username url avatar } guid apikey name status wanip lanip localurl remoteurl } }', + variables: { + apiKey + } + }) +}) +.then(async response => { + const data = await response.json(); + return data.servers as Promise; +}) +.catch(error => { + Sentry.captureException(error); +}); \ No newline at end of file