Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(develop): add query on demand behind feature flag #28127

Merged
merged 9 commits into from Nov 17, 2020
40 changes: 40 additions & 0 deletions packages/gatsby/cache-dir/dev-loader.js
Expand Up @@ -36,6 +36,8 @@ class DevLoader extends BaseLoader {
this.handleStaticQueryResultHotUpdate(msg)
} else if (msg.type === `pageQueryResult`) {
this.handlePageQueryResultHotUpdate(msg)
} else if (msg.type === `dirtyQueries`) {
this.handleDirtyPageQueryMessage(msg)
}
})
} else {
Expand Down Expand Up @@ -75,6 +77,9 @@ class DevLoader extends BaseLoader {
}

doPrefetch(pagePath) {
if (process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND) {
return Promise.resolve()
}
vladar marked this conversation as resolved.
Show resolved Hide resolved
return super.doPrefetch(pagePath).then(result => result.payload)
}

Expand Down Expand Up @@ -138,6 +143,41 @@ class DevLoader extends BaseLoader {
___emitter.emit(`pageQueryResult`, newPageData)
}
}

handleDirtyPageQueryMessage(msg) {
msg.payload.dirtyQueries.forEach(dirtyQueryId => {
if (dirtyQueryId === `/dev-404-page/` || dirtyQueryId === `/404.html`) {
// those pages are not on demand so skipping
return
}

const normalizedId = normalizePagePath(dirtyQueryId)

// We can't just delete items in caches, because then
// using history.back() would show dev-404 page
// due to our special handling of it in root.js (loader.isPageNotFound check)
// so instead we mark it as stale and instruct loader's async methods
// to refetch resources if they are marked as stale

vladar marked this conversation as resolved.
Show resolved Hide resolved
const cachedPageData = this.pageDataDb.get(normalizedId)
if (cachedPageData) {
// if we have page data in cache, mark it as stale
this.pageDataDb.set(normalizedId, {
...cachedPageData,
stale: true,
})
}

const cachedPage = this.pageDb.get(normalizedId)
if (cachedPage) {
// if we have page data in cache, mark it as stale
this.pageDb.set(normalizedId, {
...cachedPage,
payload: { ...cachedPage.payload, stale: true },
vladar marked this conversation as resolved.
Show resolved Hide resolved
})
}
})
}
}

export default DevLoader
8 changes: 8 additions & 0 deletions packages/gatsby/cache-dir/ensure-resources.js
Expand Up @@ -47,6 +47,14 @@ class EnsureResources extends React.Component {
return false
}

if (
process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND &&
nextState.pageResources.stale
) {
this.loadResources(nextProps.location.pathname)
return false
}

// Check if the component or json have changed.
if (this.state.pageResources !== nextState.pageResources) {
return true
Expand Down
12 changes: 10 additions & 2 deletions packages/gatsby/cache-dir/loader.js
Expand Up @@ -190,7 +190,10 @@ export class BaseLoader {
loadPageDataJson(rawPath) {
const pagePath = findPath(rawPath)
if (this.pageDataDb.has(pagePath)) {
return Promise.resolve(this.pageDataDb.get(pagePath))
const pageData = this.pageDataDb.get(pagePath)
if (!process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND || !pageData.stale) {
return Promise.resolve(pageData)
}
}

return this.fetchPageDataJson({ pagePath }).then(pageData => {
Expand All @@ -209,7 +212,12 @@ export class BaseLoader {
const pagePath = findPath(rawPath)
if (this.pageDb.has(pagePath)) {
const page = this.pageDb.get(pagePath)
return Promise.resolve(page.payload)
if (
!process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND ||
!page.payload.stale
) {
return Promise.resolve(page.payload)
}
}

if (this.inFlightDb.has(pagePath)) {
Expand Down
Expand Up @@ -57,6 +57,7 @@ Object {
"byConnection": Map {},
"byNode": Map {},
"deletedQueries": Set {},
"dirtyQueriesListToEmitViaWebsocket": Array [],
"queryNodes": Map {},
"trackedComponents": Map {
"/Users/username/dev/site/src/templates/my-sweet-new-page.js" => Object {
Expand Down
7 changes: 7 additions & 0 deletions packages/gatsby/src/redux/actions/internal.ts
Expand Up @@ -20,6 +20,7 @@ import {
ISetGraphQLDefinitionsAction,
IQueryStartAction,
IApiFinishedAction,
IQueryClearDirtyQueriesListToEmitViaWebsocket,
} from "../types"

import { gatsbyConfigSchema } from "../../joi-schemas/joi"
Expand Down Expand Up @@ -249,6 +250,12 @@ export const queryStart = (
}
}

export const clearDirtyQueriesListToEmitViaWebsocket = (): IQueryClearDirtyQueriesListToEmitViaWebsocket => {
return {
type: `QUERY_CLEAR_DIRTY_QUERIES_LIST_TO_EMIT_VIA_WEBSOCKET`,
}
}

/**
* Remove jobs which are marked as stale (inputPath doesn't exists)
* @private
Expand Down
Expand Up @@ -13,6 +13,7 @@ Object {
},
},
"deletedQueries": Set {},
"dirtyQueriesListToEmitViaWebsocket": Array [],
"queryNodes": Map {
"/hi/" => Set {
"SuperCoolNode",
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/src/redux/reducers/__tests__/queries.ts
Expand Up @@ -88,6 +88,7 @@ it(`has expected initial state`, () => {
"byConnection": Map {},
"byNode": Map {},
"deletedQueries": Set {},
"dirtyQueriesListToEmitViaWebsocket": Array [],
"queryNodes": Map {},
"trackedComponents": Map {},
"trackedQueries": Map {},
Expand Down
25 changes: 25 additions & 0 deletions packages/gatsby/src/redux/reducers/queries.ts
Expand Up @@ -26,6 +26,7 @@ const initialState = (): IGatsbyState["queries"] => {
trackedQueries: new Map<QueryId, IQueryState>(),
trackedComponents: new Map<ComponentPath, IComponentState>(),
deletedQueries: new Set<QueryId>(),
dirtyQueriesListToEmitViaWebsocket: [],
}
}

Expand Down Expand Up @@ -69,6 +70,7 @@ export function queriesReducer(
if (!query || action.contextModified) {
query = registerQuery(state, path)
query.dirty = setFlag(query.dirty, FLAG_DIRTY_PAGE)
state = trackDirtyQuery(state, path)
}
registerComponent(state, componentPath).pages.add(path)
state.deletedQueries.delete(path)
Expand Down Expand Up @@ -114,6 +116,7 @@ export function queriesReducer(
const query = state.trackedQueries.get(queryId)
if (query) {
query.dirty = setFlag(query.dirty, FLAG_DIRTY_TEXT)
state = trackDirtyQuery(state, queryId)
}
}
component.query = query
Expand All @@ -134,6 +137,9 @@ export function queriesReducer(
// TODO: unify the behavior?
const query = registerQuery(state, action.payload.id)
query.dirty = setFlag(query.dirty, FLAG_DIRTY_TEXT)
// static queries are not on demand, so skipping tracking which
// queries were marked dirty recently
// state = trackDirtyQuery(state, action.payload.id)
state.deletedQueries.delete(action.payload.id)
return state
}
Expand Down Expand Up @@ -176,12 +182,14 @@ export function queriesReducer(
const query = state.trackedQueries.get(queryId)
if (query) {
query.dirty = setFlag(query.dirty, FLAG_DIRTY_DATA)
state = trackDirtyQuery(state, queryId)
}
}
for (const queryId of queriesByConnection) {
const query = state.trackedQueries.get(queryId)
if (query) {
query.dirty = setFlag(query.dirty, FLAG_DIRTY_DATA)
state = trackDirtyQuery(state, queryId)
}
}
return state
Expand All @@ -199,9 +207,15 @@ export function queriesReducer(
for (const [, query] of state.trackedQueries) {
query.running = 0
}
// Reset list of dirty queries (this is used only to notify runtime and it could've been persisted)
state.dirtyQueriesListToEmitViaWebsocket = []
}
return state
}
case `QUERY_CLEAR_DIRTY_QUERIES_LIST_TO_EMIT_VIA_WEBSOCKET`: {
state.dirtyQueriesListToEmitViaWebsocket = []
return state
}
default:
return state
}
Expand Down Expand Up @@ -305,3 +319,14 @@ function registerComponent(
}
return component
}

function trackDirtyQuery(
state: IGatsbyState["queries"],
queryId: QueryId
): IGatsbyState["queries"] {
if (process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND) {
state.dirtyQueriesListToEmitViaWebsocket.push(queryId)
vladar marked this conversation as resolved.
Show resolved Hide resolved
}

return state
}
6 changes: 6 additions & 0 deletions packages/gatsby/src/redux/types.ts
Expand Up @@ -215,6 +215,7 @@ export interface IGatsbyState {
trackedQueries: Map<Identifier, IQueryState>
trackedComponents: Map<string, IComponentState>
deletedQueries: Set<Identifier>
dirtyQueriesListToEmitViaWebsocket: Array<string>
vladar marked this conversation as resolved.
Show resolved Hide resolved
}
components: Map<
SystemPath,
Expand Down Expand Up @@ -307,6 +308,7 @@ export type ActionsUnion =
| IDeletePageAction
| IPageQueryRunAction
| IPrintTypeDefinitions
| IQueryClearDirtyQueriesListToEmitViaWebsocket
| IQueryExtractedAction
| IQueryExtractedBabelSuccessAction
| IQueryExtractionBabelErrorAction
Expand Down Expand Up @@ -473,6 +475,10 @@ export interface IReplaceStaticQueryAction {
}
}

export interface IQueryClearDirtyQueriesListToEmitViaWebsocket {
type: `QUERY_CLEAR_DIRTY_QUERIES_LIST_TO_EMIT_VIA_WEBSOCKET`
}

export interface IQueryExtractedAction {
type: `QUERY_EXTRACTED`
plugin: IGatsbyPlugin
Expand Down
39 changes: 38 additions & 1 deletion packages/gatsby/src/services/calculate-dirty-queries.ts
Expand Up @@ -5,11 +5,48 @@ import { assertStore } from "../utils/assert-store"

export async function calculateDirtyQueries({
store,
websocketManager,
currentlyHandledPendingQueryRuns,
}: Partial<IQueryRunningContext>): Promise<{
queryIds: IGroupedQueryIds
}> {
assertStore(store)
const state = store.getState()
const queryIds = calcDirtyQueryIds(state)
return { queryIds: groupQueryIds(queryIds) }

let queriesToRun: Array<string> = queryIds

if (
process.env.gatsby_executing_command === `develop` &&
process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND
) {
// 404 are special cases in our runtime that ideally use
// generic things to work, but for now they have special handling
const pagePathFilter = new Set([`/404.html`, `/dev-404-page/`])

// we want to make sure we run queries for pages that user currently
// view in the browser
if (websocketManager?.activePaths) {
for (const activePath of websocketManager.activePaths) {
pagePathFilter.add(activePath)
}
}

// we also want to make sure we include pages that were requested from
// via `page-data` fetches or websocket requests
if (currentlyHandledPendingQueryRuns) {
for (const pendingQuery of currentlyHandledPendingQueryRuns) {
pagePathFilter.add(pendingQuery)
}
}

// static queries are also not on demand
queriesToRun = queryIds.filter(
queryId => queryId.startsWith(`sq--`) || pagePathFilter.has(queryId)
)
}

return {
queryIds: groupQueryIds(queriesToRun),
}
}
18 changes: 17 additions & 1 deletion packages/gatsby/src/services/initialize.ts
@@ -1,5 +1,5 @@
import _ from "lodash"
import { slash } from "gatsby-core-utils"
import { slash, isCI } from "gatsby-core-utils"
import fs from "fs-extra"
import md5File from "md5-file"
import crypto from "crypto"
Expand Down Expand Up @@ -162,6 +162,22 @@ export async function initialize({

activity.end()

if (process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND) {
if (process.env.gatsby_executing_command !== `develop`) {
// we don't want to ever have this flag enabled for anything than develop
// in case someone have this env var globally set
delete process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND
} else if (isCI()) {
delete process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND
reporter.warn(
`Experimental Query on Demand feature is not available in CI environment. Continuing with regular mode.`
)
} else {
reporter.info(`Using experimental Query on Demand feature`)
telemetry.trackFeatureIsUsed(`QueryOnDemand`)
}
}

// run stale jobs
store.dispatch(removeStaleJobs(store.getState()))

Expand Down
23 changes: 23 additions & 0 deletions packages/gatsby/src/utils/page-data.ts
Expand Up @@ -124,6 +124,7 @@ export async function flush(): Promise<void> {
pages,
program,
staticQueriesByTemplate,
queries,
} = store.getState()

const { pagePaths } = pendingPageDataWrites
Expand All @@ -140,6 +141,28 @@ export async function flush(): Promise<void> {
// them, a page might not exist anymore щ(゚Д゚щ)
// This is why we need this check
if (page) {
if (
program?._?.[0] === `develop` &&
process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND
) {
// check if already did run query for this page
// with query-on-demand we might have pending page-data write due to
// changes in static queries assigned to page template, but we might not
// have query result for it
const query = queries.trackedQueries.get(page.path)
if (!query) {
// this should not happen ever
throw new Error(
`We have a page, but we don't have registered query for it (???)`
)
}

if (query.dirty !== 0) {
// query results are not up to date, it's not safe to write page-data and emit results
continue
}
}

pieh marked this conversation as resolved.
Show resolved Hide resolved
const staticQueryHashes =
staticQueriesByTemplate.get(page.componentPath) || []

Expand Down
3 changes: 3 additions & 0 deletions packages/gatsby/src/utils/webpack.config.js
Expand Up @@ -85,6 +85,9 @@ module.exports = async (
envObject.PUBLIC_DIR = JSON.stringify(`${process.cwd()}/public`)
envObject.BUILD_STAGE = JSON.stringify(stage)
envObject.CYPRESS_SUPPORT = JSON.stringify(process.env.CYPRESS_SUPPORT)
envObject.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND = JSON.stringify(
!!process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND
)

if (stage === `develop`) {
envObject.GATSBY_SOCKET_IO_DEFAULT_TRANSPORT = JSON.stringify(
Expand Down