diff --git a/packages/gatsby-source-filesystem/package.json b/packages/gatsby-source-filesystem/package.json index 2a59d289cb7f2..b9d12402f9910 100644 --- a/packages/gatsby-source-filesystem/package.json +++ b/packages/gatsby-source-filesystem/package.json @@ -17,7 +17,7 @@ "pretty-bytes": "^5.4.1", "progress": "^2.0.3", "valid-url": "^1.0.9", - "xstate": "^4.26.1" + "xstate": "4.32.1" }, "devDependencies": { "@babel/cli": "^7.15.4", @@ -48,4 +48,4 @@ "engines": { "node": ">=14.15.0" } -} +} \ No newline at end of file diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 3bb9aae67f630..6d410a4ee71a6 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -169,7 +169,7 @@ "webpack-merge": "^5.8.0", "webpack-stats-plugin": "^1.0.3", "webpack-virtual-modules": "^0.3.2", - "xstate": "^4.26.0", + "xstate": "4.32.1", "yaml-loader": "^0.6.0" }, "devDependencies": { diff --git a/packages/gatsby/src/state-machines/__tests__/query-running.ts b/packages/gatsby/src/state-machines/__tests__/query-running.ts new file mode 100644 index 0000000000000..99fbf272b8702 --- /dev/null +++ b/packages/gatsby/src/state-machines/__tests__/query-running.ts @@ -0,0 +1,200 @@ +import { queryRunningMachine } from "../query-running" +import { queryActions } from "../query-running/actions" +import { interpret, Interpreter } from "xstate" +import { IProgram } from "../../commands/types" +import { store } from "../../redux" +import reporter from "gatsby-cli/lib/reporter" +import pDefer from "p-defer" +import { IGroupedQueryIds } from "../../services/types" + +const services = { + extractQueries: jest.fn(async () => {}), + writeOutRequires: jest.fn(async () => {}), + calculateDirtyQueries: jest.fn( + async (): Promise<{ + queryIds: IGroupedQueryIds + }> => { + return { + queryIds: { + pageQueryIds: [], + staticQueryIds: [], + }, + } + } + ), +} + +const machine = queryRunningMachine.withConfig( + { + actions: queryActions, + services, + }, + { + program: {} as IProgram, + store, + reporter, + pendingQueryRuns: new Set([`/`]), + } +) + +const resetMocks = (mocks: Record): void => + Object.values(mocks).forEach(mock => mock.mockClear()) + +const resetAllMocks = (): void => { + resetMocks(services) +} + +const finished = async ( + service: Interpreter +): Promise => + new Promise(resolve => { + service.onDone(() => resolve()) + }) + +function debug(service: Interpreter): void { + let last: any + + service.onTransition(state => { + if (!last) { + last = state + } else if (!state.changed) { + return + } + + reporter.info( + `---onTransition---\n${require(`util`).inspect( + { + stateValue: state.value, + event: state.event, + pendingQueryRuns: state.context.pendingQueryRuns, + changedStateValue: state.value !== last.value, + }, + { depth: Infinity } + )}` + ) + last = state + }) +} + +expect.extend({ + toHaveInSet(received, item) { + if (received.has(item)) { + return { + pass: true, + message: (): string => + `Expected ${Array.from(received)} not to contain ${item}`, + } + } else { + return { + pass: false, + message: (): string => + `Expected ${Array.from(received)} not to contain ${item}`, + } + } + }, +}) + +/* eslint-disable @typescript-eslint/no-namespace */ +declare global { + namespace jest { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Expect { + toHaveInSet(item: any): any + } + } +} + +describe(`query-running state machine`, () => { + beforeEach(() => { + resetAllMocks() + }) + + it(`initialises`, async () => { + const service = interpret(machine) + // debug(service) + + service.start() + expect(service.state.value).toBe(`extractingQueries`) + }) + + it(`doesn't drop pendingQueryRuns that were added during calculation of dirty queries`, async () => { + const deferred = pDefer<{ + queryIds: IGroupedQueryIds + }>() + const waitForExecutionOfCalcDirtyQueries = pDefer() + + services.calculateDirtyQueries.mockImplementation( + async (): Promise<{ + queryIds: IGroupedQueryIds + }> => { + waitForExecutionOfCalcDirtyQueries.resolve() + + // allow test to execute some code before resuming service + + await deferred.promise + + return { + queryIds: { + pageQueryIds: [], + staticQueryIds: [], + }, + } + } + ) + + const service = interpret(machine) + // debug(service) + + service.send({ + type: `QUERY_RUN_REQUESTED`, + payload: { + pagePath: `/bar/`, + }, + }) + + service.start() + + await waitForExecutionOfCalcDirtyQueries.promise + + // we are in middle of execution of calcDirtyQueries service + // let's dispatch QUERY_RUN_REQUESTED for page /foo/ + service.send({ + type: `QUERY_RUN_REQUESTED`, + payload: { + pagePath: `/foo/`, + }, + }) + + deferred.resolve() + + // let state machine reach final state + await finished(service) + + // let's make sure that we called calculateDirtyQueries service + // with every page that was requested, even if page was requested + // while we were executing calcDirtyQueries service + expect(services.calculateDirtyQueries).toHaveBeenCalledWith( + expect.objectContaining({ + currentlyHandledPendingQueryRuns: expect.toHaveInSet(`/`), + }), + expect.anything(), + expect.anything() + ) + + expect(services.calculateDirtyQueries).toHaveBeenCalledWith( + expect.objectContaining({ + currentlyHandledPendingQueryRuns: expect.toHaveInSet(`/bar/`), + }), + expect.anything(), + expect.anything() + ) + + expect(services.calculateDirtyQueries).toHaveBeenCalledWith( + expect.objectContaining({ + currentlyHandledPendingQueryRuns: expect.toHaveInSet(`/foo/`), + }), + expect.anything(), + expect.anything() + ) + }) +}) diff --git a/packages/gatsby/src/state-machines/data-layer/services.ts b/packages/gatsby/src/state-machines/data-layer/services.ts index 3dd8e34ebf8b5..9195d8fc9647f 100644 --- a/packages/gatsby/src/state-machines/data-layer/services.ts +++ b/packages/gatsby/src/state-machines/data-layer/services.ts @@ -1,4 +1,4 @@ -import { ServiceConfig } from "xstate" +import { MachineOptions } from "xstate" import { customizeSchema, createPages, @@ -8,10 +8,10 @@ import { } from "../../services" import { IDataLayerContext } from "./types" -export const dataLayerServices: Record< - string, - ServiceConfig -> = { +export const dataLayerServices: MachineOptions< + IDataLayerContext, + any +>["services"] = { customizeSchema, sourceNodes, createPages, diff --git a/packages/gatsby/src/state-machines/develop/services.ts b/packages/gatsby/src/state-machines/develop/services.ts index eb2f5d7a5a280..04f76043ac9af 100644 --- a/packages/gatsby/src/state-machines/develop/services.ts +++ b/packages/gatsby/src/state-machines/develop/services.ts @@ -13,9 +13,9 @@ import { } from "../data-layer" import { queryRunningMachine } from "../query-running" import { waitingMachine } from "../waiting" -import { ServiceConfig } from "xstate" +import { MachineOptions } from "xstate" -export const developServices: Record> = { +export const developServices: MachineOptions["services"] = { initializeData: initializeDataMachine, reloadData: reloadDataMachine, recreatePages: recreatePagesMachine, diff --git a/packages/gatsby/src/state-machines/query-running/services.ts b/packages/gatsby/src/state-machines/query-running/services.ts index 499e6898aa143..08bb68f0f8455 100644 --- a/packages/gatsby/src/state-machines/query-running/services.ts +++ b/packages/gatsby/src/state-machines/query-running/services.ts @@ -1,4 +1,4 @@ -import { ServiceConfig } from "xstate" +import { MachineOptions } from "xstate" import { extractQueries, writeOutRequires, @@ -10,10 +10,10 @@ import { } from "../../services" import { IQueryRunningContext } from "./types" -export const queryRunningServices: Record< - string, - ServiceConfig -> = { +export const queryRunningServices: MachineOptions< + IQueryRunningContext, + any +>["services"] = { extractQueries, writeOutRequires, calculateDirtyQueries, diff --git a/packages/gatsby/src/utils/state-machine-logging.ts b/packages/gatsby/src/utils/state-machine-logging.ts index fb448d4cdd6ab..ab604e6a4c1e1 100644 --- a/packages/gatsby/src/utils/state-machine-logging.ts +++ b/packages/gatsby/src/utils/state-machine-logging.ts @@ -7,12 +7,14 @@ import { } from "xstate" import reporter from "gatsby-cli/lib/reporter" +type AnyInterpreterWithContext = Interpreter + const isInterpreter = ( actor: Actor | Interpreter ): actor is Interpreter => `machine` in actor export function logTransitions( - service: Interpreter + service: AnyInterpreterWithContext ): void { const listeners = new WeakSet() let last: State diff --git a/yarn.lock b/yarn.lock index 5447867de3a92..d4eec5708776f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26954,10 +26954,10 @@ xss@^1.0.6: commander "^2.20.3" cssfilter "0.0.10" -xstate@^4.26.0, xstate@^4.26.1: - version "4.26.1" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.26.1.tgz#4fc1afd153f88cf302a9ee2b758f6629e6a829b6" - integrity sha512-JLofAEnN26l/1vbODgsDa+Phqa61PwDlxWu8+2pK+YbXf+y9pQSDLRvcYH2H1kkeUBA5fGp+xFL/zfE8jNMw4g== +xstate@4.32.1: + version "4.32.1" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.32.1.tgz#1a09c808a66072938861a3b4acc5b38460244b70" + integrity sha512-QYUd+3GkXZ8i6qdixnOn28bL3EvA++LONYL/EMWwKlFSh/hiLndJ8YTnz77FDs+JUXcwU7NZJg7qoezoRHc4GQ== xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2"