diff --git a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts index fe3824218b97..0a2829122d33 100644 --- a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts +++ b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts @@ -149,7 +149,7 @@ describe('ACI - Latest runs and Average duration', { viewportWidth: 1200, viewpo beforeEach(() => { cy.loginUser() - cy.remoteGraphQLIntercept(async (obj) => { + cy.remoteGraphQLInterceptBatched(async (obj) => { if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { obj.result.data.cloudSpecByPath = { __typename: 'CloudProjectSpecNotFound', diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index c8637fe224c3..aa06ea61e6b0 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -436,7 +436,7 @@ const collapsible = computed(() => { }) const treeSpecList = computed(() => collapsible.value.tree.filter(((item) => !item.hidden.value))) -const { containerProps, list, wrapperProps, scrollTo } = useVirtualList(treeSpecList, { itemHeight: 40, overscan: 10 }) +const { containerProps, list, wrapperProps, scrollTo } = useVirtualList(treeSpecList, { itemHeight: 40, overscan: 2 }) const scrollbarOffset = ref(0) diff --git a/packages/data-context/src/sources/CloudDataSource.ts b/packages/data-context/src/sources/CloudDataSource.ts index 260e87e901bc..3c4656a54b1c 100644 --- a/packages/data-context/src/sources/CloudDataSource.ts +++ b/packages/data-context/src/sources/CloudDataSource.ts @@ -9,7 +9,7 @@ import crypto from 'crypto' import type { DataContext } from '..' import getenv from 'getenv' -import { print, DocumentNode, ExecutionResult, GraphQLResolveInfo, OperationTypeNode } from 'graphql' +import { print, DocumentNode, ExecutionResult, GraphQLResolveInfo, OperationTypeNode, visit, OperationDefinitionNode } from 'graphql' import { createClient, dedupExchange, @@ -50,6 +50,7 @@ export interface CloudExecuteQuery { } export interface CloudExecuteRemote extends CloudExecuteQuery { + shouldBatch?: boolean operationType?: OperationTypeNode requestPolicy?: RequestPolicy onUpdatedResult?: (data: any) => any @@ -81,14 +82,9 @@ export class CloudDataSource { constructor (private params: CloudDataSourceParams) { this.#cloudUrqlClient = this.reset() - this.#batchExecutor = createBatchingExecutor(({ document, variables }) => { - debug(`Executing remote dashboard request %s, %j`, print(document), variables) - - return this.#cloudUrqlClient.query(document, variables ?? {}, { - ...this.#additionalHeaders, - requestPolicy: 'network-only', - }).toPromise() - }) + this.#batchExecutor = createBatchingExecutor((config) => { + return this.#executeQuery(namedExecutionDocument(config.document), config.variables) + }, { maxBatchSize: 20 }) this.#batchExecutorBatcher = this.#makeBatchExecutionBatcher() } @@ -213,7 +209,11 @@ export class CloudDataSource { return loading } - loading = this.#batchExecutorBatcher.load(config).then(this.#formatWithErrors) + const query = config.shouldBatch + ? this.#batchExecutorBatcher.load(config) + : this.#executeQuery(config.operationDoc, config.operationVariables) + + loading = query.then(this.#formatWithErrors) .then((op) => { this.#pendingPromises.delete(stableKey) @@ -235,6 +235,12 @@ export class CloudDataSource { return loading } + #executeQuery (operationDoc: DocumentNode, operationVariables: object = {}) { + debug(`Executing remote dashboard request %s, %j`, print(operationDoc), operationVariables) + + return this.#cloudUrqlClient.query(operationDoc, operationVariables, { requestPolicy: 'network-only' }).toPromise() + } + isResolving (config: CloudExecuteQuery) { const stableKey = this.#hashRemoteRequest(config) @@ -330,18 +336,6 @@ export class CloudDataSource { */ #makeBatchExecutionBatcher () { return new DataLoader(async (toBatch) => { - const first = toBatch[0] - - // If we only have a single entry, we can just hit the query directly, - // without rewriting anything - this makes the queries simpler in most cases in the app - if (toBatch.length === 1 && first) { - return [this.#cloudUrqlClient.query(first.operation, first.operationVariables ?? {}, { - ...this.#additionalHeaders, - requestPolicy: 'network-only', - }).toPromise()] - } - - // Otherwise run this through batchExecutor: return Promise.allSettled(toBatch.map((b) => { return this.#batchExecutor({ operationType: 'query', @@ -358,3 +352,37 @@ export class CloudDataSource { return val instanceof Error ? val : new Error(val) } } + +/** + * Adds "batchExecutionQuery" to the query that we generate from the batch loader, + * useful to key off of in the tests. + */ +function namedExecutionDocument (document: DocumentNode) { + let hasReplaced = false + + return visit(document, { + enter () { + if (hasReplaced) { + return false + } + + return + }, + OperationDefinition (op) { + if (op.name) { + return op + } + + hasReplaced = true + const namedOperationNode: OperationDefinitionNode = { + ...op, + name: { + kind: 'Name', + value: 'batchTestRunnerExecutionQuery', + }, + } + + return namedOperationNode + }, + }) +} diff --git a/packages/data-context/src/sources/RemoteRequestDataSource.ts b/packages/data-context/src/sources/RemoteRequestDataSource.ts index 208bd4fac2cb..5409bb97c40f 100644 --- a/packages/data-context/src/sources/RemoteRequestDataSource.ts +++ b/packages/data-context/src/sources/RemoteRequestDataSource.ts @@ -19,6 +19,7 @@ interface MaybeLoadRemoteFetchable extends CloudExecuteQuery { shouldFetch?: boolean remoteQueryField: string isMutation: boolean + shouldBatch?: boolean } interface OperationDefinition { @@ -65,6 +66,7 @@ export class RemoteRequestDataSource { remoteQueryField, shouldFetch: true, isMutation: true, + shouldBatch: true, }) } @@ -157,6 +159,7 @@ export class RemoteRequestDataSource { operationHash, operationVariables, requestPolicy: 'network-only', + shouldBatch: params.shouldBatch, })) .then((result) => { const toPushDefinition = this.#operationRegistryPushToFrontend.get(operationHash) @@ -292,6 +295,7 @@ export class RemoteRequestDataSource { shouldFetch: shouldEagerFetch, remoteQueryField: fieldConfig.remoteQueryField, isMutation: false, + shouldBatch: true, }) }) } diff --git a/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts b/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts index 4a3d2f8c5067..961a3dfc3fa6 100644 --- a/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts +++ b/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts @@ -105,6 +105,7 @@ async function makeE2ETasks () { let ctx: DataContext let testState: Record = {} let remoteGraphQLIntercept: RemoteGraphQLInterceptor | undefined + let remoteGraphQLInterceptBatchHandler: RemoteGraphQLInterceptor | undefined let scaffoldedProjects = new Set() const cachedCwd = process.cwd() @@ -182,6 +183,7 @@ async function makeE2ETasks () { sinon.reset() sinon.restore() remoteGraphQLIntercept = undefined + remoteGraphQLInterceptBatchHandler = undefined const fetchApi = ctx.util.fetch @@ -213,7 +215,11 @@ async function makeE2ETasks () { operationCount[operationName ?? 'unknown']++ - if (remoteGraphQLIntercept) { + if (operationName === 'batchTestRunnerExecutionQuery' && remoteGraphQLInterceptBatchHandler) { + // const toSettle = _.mapValues(result.data, (key, val) => { + // /^_(?:\d+)_(.*?)%/.test(key) + // }) + } else if (remoteGraphQLIntercept) { try { result = await remoteGraphQLIntercept({ operationName, @@ -278,6 +284,11 @@ async function makeE2ETasks () { return null }, + __internal_remoteGraphQLInterceptBatched (fn: string) { + remoteGraphQLInterceptBatchHandler = new Function('console', 'obj', 'testState', `return (${fn})(obj, testState)`).bind(null, console) as RemoteGraphQLInterceptor + + return null + }, async __internal_addProject (opts: InternalAddProjectOpts) { if (!scaffoldedProjects.has(opts.projectName)) { await __internal_scaffoldProject(opts.projectName) diff --git a/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts b/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts index 486e1ab4574b..a7cea47b03a9 100644 --- a/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts +++ b/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts @@ -133,6 +133,10 @@ declare global { * Gives the ability to intercept the remote GraphQL request & respond accordingly */ remoteGraphQLIntercept: typeof remoteGraphQLIntercept + /** + * Gives the ability to intercept the remote GraphQL request & respond accordingly + */ + remoteGraphQLInterceptBatched: typeof remoteGraphQLInterceptBatched /** * Removes the sinon spy'ing on the remote GraphQL fake requests */ @@ -444,6 +448,12 @@ function remoteGraphQLIntercept (fn: RemoteGraphQLInterceptor) { }) } +function remoteGraphQLInterceptBatched (fn: RemoteGraphQLInterceptor) { + return logInternal('remoteGraphQLInterceptBatched', () => { + return taskInternal('__internal_remoteGraphQLInterceptBatched', fn.toString()) + }) +} + type Resolved = V extends Promise ? U : V /**