Skip to content

Commit

Permalink
Polling (#6264)
Browse files Browse the repository at this point in the history
* Polling

* Go

* chore(dependencies): updated changesets for modified dependencies

* Readonly

* Go

* chore(dependencies): updated changesets for modified dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
ardatan and github-actions[bot] authored Dec 7, 2023
1 parent 1368259 commit 4fac014
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 103 deletions.
10 changes: 10 additions & 0 deletions .changeset/warm-cougars-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@graphql-mesh/merger-bare': patch
'@graphql-mesh/config': patch
'@graphql-mesh/store': patch
'@graphql-mesh/types': patch
'@graphql-mesh/http': patch
'@graphql-mesh/cli': patch
---

Implement polling
2 changes: 2 additions & 0 deletions examples/openapi-stripe/.meshrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ sources:

documents:
- get-customers.query.graphql

pollingInterval: 10000
12 changes: 7 additions & 5 deletions examples/openapi-stripe/get-customers.query.graphql
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
query GetCustomers {
getCustomers {
data {
id
name
email
GetCustomers {
... on CustomerResourceCustomerList {
data {
id
name
email
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ describe('OpenAPI Subscriptions', () => {
return this;
},
};
const mesh$ = Promise.resolve().then(() =>
getMesh({
...config,
fetchFn: appWrapper.app.fetch as any,
}),
);
meshHandler = createMeshHTTPHandler({
baseDir: join(__dirname, '..'),
getBuiltMesh: () =>
getMesh({
...config,
fetchFn: appWrapper.app.fetch as any,
}),
getBuiltMesh: () => mesh$,
rawServeConfig: config.config.serve,
});
appWrapper = createApp(meshHandler.fetch as any);
Expand Down
21 changes: 1 addition & 20 deletions packages/cli/src/commands/serve/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { MeshInstance, ServeMeshOptions } from '@graphql-mesh/runtime';
import type { Logger } from '@graphql-mesh/types';
import { handleFatalError } from '../../handleFatalError.js';
import { GraphQLMeshCLIParams } from '../../index.js';
import { registerTerminateHandler } from '../../terminateHandler.js';

function portSelectorFn(sources: [number, number, number], logger: Logger) {
const port = sources.find(source => Boolean(source)) || 4000;
Expand Down Expand Up @@ -44,26 +45,6 @@ export async function serveMesh(
}: ServeMeshOptions,
cliParams: GraphQLMeshCLIParams,
) {
const terminateEvents = ['SIGINT', 'SIGTERM'] as const;
type TerminateEvents = (typeof terminateEvents)[number];
type TerminateHandler = (eventName: TerminateEvents) => void;
const terminateHandlers = new Set<TerminateHandler>();
for (const eventName of terminateEvents) {
process.once(eventName, () => {
for (const handler of terminateHandlers) {
handler(eventName);
terminateHandlers.delete(handler);
}
});
}

function registerTerminateHandler(callback: TerminateHandler) {
terminateHandlers.add(callback);
return () => {
terminateHandlers.delete(callback);
};
}

const {
fork: configFork = process.env.NODE_ENV?.toLowerCase() === 'production',
port: configPort,
Expand Down
20 changes: 19 additions & 1 deletion packages/cli/src/commands/ts-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export async function generateTsArtifacts(
sdkConfig,
fileType,
codegenConfig = {},
pollingInterval,
}: {
unifiedSchema: GraphQLSchema;
rawSources: readonly RawSourceOutput[];
Expand All @@ -168,6 +169,7 @@ export async function generateTsArtifacts(
sdkConfig: YamlConfig.SDKConfig;
fileType: 'ts' | 'json' | 'js';
codegenConfig: any;
pollingInterval?: number;
},
cliParams: GraphQLMeshCLIParams,
) {
Expand Down Expand Up @@ -334,16 +336,32 @@ const rootStore = new MeshStore('${cliParams.artifactsDir}', new FsStoreStorageA
importFn,
fileType: ${JSON.stringify(fileType)},
}), {
readonly: true,
readonly: ${!pollingInterval},
validate: false
});
${[...meshConfigCodes].join('\n')}
let meshInstance$: Promise<MeshInstance> | undefined;
export const pollingInterval = ${pollingInterval || null};
export function ${cliParams.builtMeshFactoryName}(): Promise<MeshInstance> {
if (meshInstance$ == null) {
if (pollingInterval) {
setInterval(() => {
getMeshOptions()
.then(meshOptions => getMesh(meshOptions))
.then(newMesh =>
meshInstance$.then(oldMesh => {
oldMesh.destroy()
meshInstance$ = Promise.resolve(newMesh)
})
).catch(err => {
console.error("Mesh polling failed so the existing version will be used:", err);
});
}, pollingInterval)
}
meshInstance$ = getMeshOptions().then(meshOptions => getMesh(meshOptions)).then(mesh => {
const id = mesh.pubsub.subscribe('destroy', () => {
meshInstance$ = undefined;
Expand Down
143 changes: 97 additions & 46 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { register as tsConfigPathsRegister } from 'tsconfig-paths';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { fs, path as pathModule, process } from '@graphql-mesh/cross-helpers';
import { getMesh, GetMeshOptions, ServeMeshOptions } from '@graphql-mesh/runtime';
import { getMesh, GetMeshOptions, MeshInstance, ServeMeshOptions } from '@graphql-mesh/runtime';
import { FsStoreStorageAdapter, MeshStore } from '@graphql-mesh/store';
import { Logger, YamlConfig } from '@graphql-mesh/types';
import { defaultImportFn, DefaultLogger, pathExists, rmdirs, writeFile } from '@graphql-mesh/utils';
Expand All @@ -14,6 +14,7 @@ import { serveMesh } from './commands/serve/serve.js';
import { generateTsArtifacts } from './commands/ts-artifacts.js';
import { findAndParseConfig } from './config.js';
import { handleFatalError } from './handleFatalError.js';
import { registerTerminateHandler } from './terminateHandler.js';

export { generateTsArtifacts, serveMesh, findAndParseConfig, handleFatalError };

Expand Down Expand Up @@ -148,33 +149,34 @@ export async function graphqlMesh(
initialLoggerPrefix: cliParams.initialLoggerPrefix,
});
logger = meshConfig.logger;
const meshInstance$ = getMesh(meshConfig);
// We already handle Mesh instance errors inside `serveMesh`
// eslint-disable-next-line @typescript-eslint/no-floating-promises
meshInstance$.then(({ schema }) =>
writeFile(
pathModule.join(outputDir, 'schema.graphql'),
printSchemaWithDirectives(schema),
).catch(e => logger.error(`An error occured while writing the schema file: `, e)),
);

// eslint-disable-next-line @typescript-eslint/no-floating-promises
meshInstance$.then(({ schema, rawSources }) =>
generateTsArtifacts(
{
unifiedSchema: schema,
rawSources,
mergerType: meshConfig.merger.name,
documents: meshConfig.documents,
flattenTypes: false,
importedModulesSet: new Set(),
baseDir,
meshConfigImportCodes: new Set([
`import { findAndParseConfig } from '@graphql-mesh/cli';`,
`import { createMeshHTTPHandler, MeshHTTPHandler } from '@graphql-mesh/http';`,
]),
meshConfigCodes: new Set([
`
// eslint-disable-next-line no-inner-declarations
function buildMeshInstance() {
return getMesh(meshConfig).then(meshInstance => {
// We already handle Mesh instance errors inside `serveMesh`
// eslint-disable-next-line @typescript-eslint/no-floating-promises
writeFile(
pathModule.join(outputDir, 'schema.graphql'),
printSchemaWithDirectives(meshInstance.schema),
).catch(e => logger.error(`An error occured while writing the schema file: `, e));

// eslint-disable-next-line @typescript-eslint/no-floating-promises
generateTsArtifacts(
{
unifiedSchema: meshInstance.schema,
rawSources: meshInstance.rawSources,
mergerType: meshConfig.merger.name,
documents: meshConfig.documents,
flattenTypes: false,
importedModulesSet: new Set(),
baseDir,
pollingInterval: meshConfig.config.pollingInterval,
meshConfigImportCodes: new Set([
`import { findAndParseConfig } from '@graphql-mesh/cli';`,
`import { createMeshHTTPHandler, MeshHTTPHandler } from '@graphql-mesh/http';`,
]),
meshConfigCodes: new Set([
`
export function getMeshOptions() {
console.warn('WARNING: These artifacts are built for development mode. Please run "${
cliParams.commandName
Expand All @@ -196,19 +198,47 @@ export function createBuiltMeshHTTPHandler<TServerContext = {}>(): MeshHTTPHandl
})
}
`.trim(),
]),
logger,
sdkConfig: meshConfig.config.sdk,
fileType: 'ts',
codegenConfig: meshConfig.config.codegen,
},
cliParams,
).catch(e => {
logger.error(
`An error occurred while building the artifacts: ${e.stack || e.message}`,
);
}),
);
]),
logger,
sdkConfig: meshConfig.config.sdk,
fileType: 'ts',
codegenConfig: meshConfig.config.codegen,
},
cliParams,
).catch(e => {
logger.error(
`An error occurred while building the artifacts: ${e.stack || e.message}`,
);
});
return meshInstance;
});
}

let meshInstance$: Promise<MeshInstance>;

meshInstance$ = buildMeshInstance();

if (meshConfig.config.pollingInterval) {
logger.info(`Polling enabled with interval of ${meshConfig.config.pollingInterval}ms`);
const interval = setInterval(() => {
logger.info(`Polling for changes...`);
buildMeshInstance()
.then(newMeshInstance =>
meshInstance$.then(oldMeshInstance => {
oldMeshInstance.destroy();
meshInstance$ = Promise.resolve(newMeshInstance);
}),
)
.catch(e => {
logger.error(`Mesh polling failed so the previous version will be served: `, e);
});
}, meshConfig.config.pollingInterval);
registerTerminateHandler(() => {
logger.info(`Terminating polling...`);
clearInterval(interval);
});
}

const serveMeshOptions: ServeMeshOptions = {
baseDir,
argsPort: args.port,
Expand Down Expand Up @@ -241,14 +271,14 @@ export function createBuiltMeshHTTPHandler<TServerContext = {}>(): MeshHTTPHandl
process.env.NODE_ENV = 'production';
const mainModule = pathModule.join(builtMeshArtifactsPath, 'index');
const builtMeshArtifacts = await defaultImportFn(mainModule);
const getMeshOptions: GetMeshOptions = await builtMeshArtifacts.getMeshOptions();
logger = getMeshOptions.logger;
const rawServeConfig: YamlConfig.Config['serve'] = builtMeshArtifacts.rawServeConfig;
const meshOptions = await builtMeshArtifacts.getMeshOptions();
logger = meshOptions.logger;
const serveMeshOptions: ServeMeshOptions = {
baseDir,
argsPort: args.port,
getBuiltMesh: () => getMesh(getMeshOptions),
logger: getMeshOptions.logger.child('Server'),
getBuiltMesh: builtMeshArtifacts[cliParams.builtMeshFactoryName],
logger,
rawServeConfig,
};
await serveMesh(serveMeshOptions, cliParams);
Expand Down Expand Up @@ -415,6 +445,7 @@ export function createBuiltMeshHTTPHandler<TServerContext = {}>(): MeshHTTPHandl
sdkConfig: meshConfig.config.sdk,
fileType: args.fileType,
codegenConfig: meshConfig.config.codegen,
pollingInterval: meshConfig.config.pollingInterval,
},
cliParams,
);
Expand Down Expand Up @@ -452,13 +483,33 @@ export function createBuiltMeshHTTPHandler<TServerContext = {}>(): MeshHTTPHandl
if (sourceIndex === -1) {
throw new Error(`Source ${args.source} not found`);
}
const meshInstance$ = getMesh({
const getMeshOpts: GetMeshOptions = {
...meshConfig,
additionalTypeDefs: undefined,
additionalResolvers: [],
transforms: [],
sources: [meshConfig.sources[sourceIndex]],
});
};
let meshInstance$: Promise<MeshInstance>;
if (meshConfig.config.pollingInterval) {
const interval = setInterval(() => {
getMesh(getMeshOpts)
.then(newMeshInstance =>
meshInstance$.then(oldMeshInstance => {
oldMeshInstance.destroy();
meshInstance$ = Promise.resolve(newMeshInstance);
}),
)
.catch(e => {
logger.error(`Mesh polling failed so the previous version will be served: `, e);
});
}, meshConfig.config.pollingInterval);
registerTerminateHandler(() => {
clearInterval(interval);
});
} else {
meshInstance$ = getMesh(getMeshOpts);
}
const serveMeshOptions: ServeMeshOptions = {
baseDir,
argsPort: 4000 + sourceIndex + 1,
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/terminateHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const terminateEvents = ['SIGINT', 'SIGTERM'] as const;
type TerminateEvents = (typeof terminateEvents)[number];
type TerminateHandler = (eventName: TerminateEvents) => void;
const terminateHandlers = new Set<TerminateHandler>();
for (const eventName of terminateEvents) {
process.once(eventName, () => {
for (const handler of terminateHandlers) {
handler(eventName);
terminateHandlers.delete(handler);
}
});
}

export function registerTerminateHandler(callback: TerminateHandler) {
terminateHandlers.add(callback);
return () => {
terminateHandlers.delete(callback);
};
}
5 changes: 5 additions & 0 deletions packages/config/yaml-config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ type Query {
"""
additionalEnvelopPlugins: String
plugins: [Plugin]

"""
If you are using a CDN for a source (e.g. Federation Supergraph), this will be the polling interval in milliseconds for the CDN without a downtime
"""
pollingInterval: Int
}

scalar JSON
Expand Down
Loading

0 comments on commit 4fac014

Please sign in to comment.