@graphql-mesh/fusion-runtime
Version:
Runtime for GraphQL Mesh Fusion Supergraph
154 lines (153 loc) • 6.98 kB
JavaScript
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();
}
}