UNPKG

@graphql-mesh/fusion-runtime

Version:

Runtime for GraphQL Mesh Fusion Supergraph

154 lines (153 loc) • 6.98 kB
import { buildASTSchema, buildSchema, isSchema } from 'graphql'; import { getInContextSDK } from '@graphql-mesh/runtime'; import { mapMaybePromise } from '@graphql-mesh/utils'; import { isDocumentNode } from '@graphql-tools/utils'; import { AsyncDisposableStack, DisposableSymbols } from '@whatwg-node/disposablestack'; import { handleFederationSupergraph } from './federation/supergraph.js'; import { compareSchemas, compareSubgraphNames, getOnSubgraphExecute, } from './utils.js'; export function ensureSchema(source) { if (isSchema(source)) { return source; } if (typeof source === 'string') { return buildSchema(source, { assumeValid: true, assumeValidSDL: true }); } if (isDocumentNode(source)) { return buildASTSchema(source, { assumeValid: true, assumeValidSDL: true }); } return source; } export class UnifiedGraphManager { constructor(opts) { this.opts = opts; this.disposableStack = new AsyncDisposableStack(); this.batch = opts.batch ?? true; this.handleUnifiedGraph = opts.handleUnifiedGraph || handleFederationSupergraph; this.onSubgraphExecuteHooks = opts?.onSubgraphExecuteHooks || []; this.disposableStack.defer(() => { this.unifiedGraph = undefined; this.lastLoadedUnifiedGraph = undefined; this.inContextSDK = undefined; this.initialUnifiedGraph$ = undefined; this.pausePolling(); return this._transportExecutorStack?.disposeAsync(); }); } pausePolling() { if (this.currentTimeout) { clearTimeout(this.currentTimeout); this.currentTimeout = undefined; } } continuePolling() { if (this.opts.pollingInterval) { this.currentTimeout = setTimeout(() => { this.currentTimeout = undefined; return this.getAndSetUnifiedGraph(); }, this.opts.pollingInterval); } } ensureUnifiedGraph() { if (!this.initialUnifiedGraph$ && !this.unifiedGraph) { this.initialUnifiedGraph$ = this.getAndSetUnifiedGraph(); } return this.initialUnifiedGraph$; } getAndSetUnifiedGraph() { this.pausePolling(); try { return mapMaybePromise(this.opts.getUnifiedGraph(this.opts.transportContext), (loadedUnifiedGraph) => { if (loadedUnifiedGraph != null && this.lastLoadedUnifiedGraph != null && compareSchemas(loadedUnifiedGraph, this.lastLoadedUnifiedGraph)) { this.opts.transportContext?.logger?.debug('Unified Graph has not changed, skipping...'); this.continuePolling(); return; } if (this.lastLoadedUnifiedGraph != null) { this.opts.transportContext?.logger?.debug('Unified Graph changed, updating...'); } let cleanupJob$; if (this._transportExecutorStack) { cleanupJob$ = this._transportExecutorStack.disposeAsync(); } return mapMaybePromise(cleanupJob$, () => { this._transportExecutorStack = new AsyncDisposableStack(); this.lastLoadedUnifiedGraph ||= loadedUnifiedGraph; this.lastLoadedUnifiedGraph = loadedUnifiedGraph; this.unifiedGraph = ensureSchema(loadedUnifiedGraph); const { unifiedGraph: newUnifiedGraph, transportEntryMap, subschemas, additionalResolvers, } = this.handleUnifiedGraph({ unifiedGraph: this.unifiedGraph, additionalTypeDefs: this.opts.additionalTypeDefs, additionalResolvers: this.opts.additionalResolvers, onSubgraphExecute(subgraphName, execReq) { return onSubgraphExecute(subgraphName, execReq); }, transportEntryAdditions: this.opts.transportEntryAdditions, batch: this.batch, }); this.unifiedGraph = newUnifiedGraph; const onSubgraphExecute = getOnSubgraphExecute({ onSubgraphExecuteHooks: this.onSubgraphExecuteHooks, transports: this.opts.transports, transportContext: this.opts.transportContext, transportEntryMap, getSubgraphSchema(subgraphName) { const subgraph = subschemas.find(s => compareSubgraphNames(s.name, subgraphName)); if (!subgraph) { throw new Error(`Subgraph ${subgraphName} not found`); } return subgraph.schema; }, transportExecutorStack: this._transportExecutorStack, }); if (this.opts.additionalResolvers || additionalResolvers.length) { this.inContextSDK = getInContextSDK(this.unifiedGraph, // @ts-expect-error Legacy Mesh RawSource is not compatible with new Mesh subschemas, this.opts.transportContext?.logger, this.opts.onDelegateHooks || []); } this.continuePolling(); this._transportEntryMap = transportEntryMap; this.opts.onSchemaChange?.(this.unifiedGraph); return true; }); }, err => { this.opts.transportContext?.logger?.error('Failed to load Supergraph', err); this.continuePolling(); if (!this.unifiedGraph) { throw err; } return true; }); } catch (e) { this.opts.transportContext?.logger?.error('Failed to load Supergraph', e); this.continuePolling(); if (!this.unifiedGraph) { throw e; } return true; } } getUnifiedGraph() { return mapMaybePromise(this.ensureUnifiedGraph(), () => this.unifiedGraph); } getContext(base = {}) { return mapMaybePromise(this.ensureUnifiedGraph(), () => { if (this.inContextSDK) { Object.assign(base, this.inContextSDK); } Object.assign(base, this.opts.transportContext); return base; }); } getTransportEntryMap() { return mapMaybePromise(this.ensureUnifiedGraph(), () => this._transportEntryMap); } invalidateUnifiedGraph() { return this.getAndSetUnifiedGraph(); } [DisposableSymbols.asyncDispose]() { return this.disposableStack.disposeAsync(); } }