diff --git a/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js b/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js index 73b0ed335adba..fb978b5dda412 100644 --- a/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js +++ b/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js @@ -209,5 +209,25 @@ exports.createResolvers = ({ createResolvers }) => { emitter.on(`DELETE_PAGE`, action => { const nodeId = createPageId(action.payload.path) const node = getNode(nodeId) - store.dispatch(actions.deleteNode(node)) + let deleteNodeActions = actions.deleteNode(node) + if (action.transactionId) { + function swapToStagedDelete(action) { + return { + ...action, + type: `DELETE_NODE_STAGING`, + transactionId: action.transactionId, + } + } + + deleteNodeActions = Array.isArray(deleteNodeActions) + ? deleteNodeActions.map(swapToStagedDelete) + : swapToStagedDelete(deleteNodeActions) + } + + console.log(`deletePage stuff`, { + transactionId: action.transactionId, + deleteNodeActions, + }) + + store.dispatch(deleteNodeActions) }) diff --git a/packages/gatsby/src/redux/actions/internal.ts b/packages/gatsby/src/redux/actions/internal.ts index d8ee3b3b2451c..f9d5f9cc46de3 100644 --- a/packages/gatsby/src/redux/actions/internal.ts +++ b/packages/gatsby/src/redux/actions/internal.ts @@ -27,6 +27,7 @@ import { ICreatePageDependencyActionPayloadType, IDeleteNodeManifests, IClearGatsbyImageSourceUrlAction, + ActionsUnion, } from "../types" import { gatsbyConfigSchema } from "../../joi-schemas/joi" @@ -38,6 +39,9 @@ import { getInProcessJobPromise, } from "../../utils/jobs/manager" import { getEngineContext } from "../../utils/engine-context" +import { store } from "../index" + +import { getNode } from "../../datastore" /** * Create a dependency between a page and data. Probably for @@ -445,3 +449,63 @@ export const clearGatsbyImageSourceUrls = type: `CLEAR_GATSBY_IMAGE_SOURCE_URL`, } } + +let publicActions +export const setPublicActions = (actions): void => { + publicActions = actions +} + +export const commitStagingNodes = ( + transactionId: string +): Array => { + const transaction = store + .getState() + .nodesStaging.transactions.get(transactionId) + if (!transaction) { + return [] + } + + const actions: Array = [ + { + type: `COMMIT_STAGING_NODES`, + payload: { + transactionId, + }, + }, + ] + + const nodesState = new Map() + for (const action of transaction) { + if (action.type === `CREATE_NODE_STAGING`) { + nodesState.set(action.payload.id, action) + } else if (action.type === `DELETE_NODE_STAGING` && action.payload?.id) { + nodesState.set(action.payload.id, undefined) + } + } + + function sanitizeNode(node: any): any { + return { + ...node, + internal: { + ...node.internal, + owner: undefined, + }, + } + } + + for (const [id, actionOrDelete] of nodesState.entries()) { + if (actionOrDelete) { + actions.push( + publicActions.createNode( + sanitizeNode(actionOrDelete.payload), + actionOrDelete.plugin + ) + ) + } else { + // delete case + actions.push(publicActions.deleteNode(getNode(id), actionOrDelete.plugin)) + } + } + + return actions +} diff --git a/packages/gatsby/src/redux/actions/public.js b/packages/gatsby/src/redux/actions/public.js index da30fddc21b56..bad5ac5e538b7 100644 --- a/packages/gatsby/src/redux/actions/public.js +++ b/packages/gatsby/src/redux/actions/public.js @@ -30,7 +30,7 @@ const apiRunnerNode = require(`../../utils/api-runner-node`) const { getNonGatsbyCodeFrame } = require(`../../utils/stack-trace-utils`) const { getPageMode } = require(`../../utils/page-mode`) const normalizePath = require(`../../utils/normalize-path`).default -import { createJobV2FromInternalJob } from "./internal" +import { createJobV2FromInternalJob, setPublicActions } from "./internal" import { maybeSendJobToMainProcess } from "../../utils/jobs/worker-messaging" import { reportOnce } from "../../utils/report-once" import { wrapNode } from "../../utils/detect-node-mutations" @@ -78,6 +78,7 @@ const findChildren = initialChildren => { } import type { Plugin } from "./types" +import { shouldRunOnCreateNode } from "../plugin-runner" type Job = { id: string, @@ -137,10 +138,15 @@ type PageDataRemove = { * @example * deletePage(page) */ -actions.deletePage = (page: IPageInput) => { +actions.deletePage = ( + page: IPageInput, + plugin?: Plugin, + actionOptions?: ActionOptions +) => { return { type: `DELETE_PAGE`, payload: page, + transactionId: actionOptions?.transactionId, } } @@ -475,10 +481,44 @@ ${reservedFields.map(f => ` * "${f}"`).join(`\n`)} contentDigest: createContentDigest(node), } node.id = `SitePage ${internalPage.path}` - const oldNode = getNode(node.id) let deleteActions let updateNodeAction + // const shouldCommitImmediately = + // // !shouldRunOnCreateNode() || + // !page.path.includes("hello-world") + const transactionId = + actionOptions?.transactionId ?? + (shouldRunOnCreateNode() ? node.internal.contentDigest : undefined) + + // Sanitize page object so we don't attempt to serialize user-provided objects that are not serializable later + const sanitizedPayload = sanitizeNode(internalPage) + + const createPageAction = { + ...actionOptions, + type: `CREATE_PAGE`, + contextModified, + componentModified, + slicesModified, + plugin, + payload: sanitizedPayload, + } + const actions = [createPageAction] + + if (transactionId) { + createPageAction.transactionId = transactionId + actions.push({ + ...actionOptions, + type: `CREATE_NODE_STAGING`, + plugin: { name: `internal-data-bridge` }, + payload: node, + transactionId, + }) + return actions + } + + const oldNode = getNode(node.id) + // marking internal-data-bridge as owner of SitePage instead of plugin that calls createPage if (oldNode && !hasNodeChanged(node.id, node.internal.contentDigest)) { updateNodeAction = { @@ -517,21 +557,6 @@ ${reservedFields.map(f => ` * "${f}"`).join(`\n`)} } } - // Sanitize page object so we don't attempt to serialize user-provided objects that are not serializable later - const sanitizedPayload = sanitizeNode(internalPage) - - const actions = [ - { - ...actionOptions, - type: `CREATE_PAGE`, - contextModified, - componentModified, - slicesModified, - plugin, - payload: sanitizedPayload, - }, - ] - if (deleteActions && deleteActions.length) { actions.push(...deleteActions) } @@ -1526,3 +1551,5 @@ actions.setRequestHeaders = ({ domain, headers }, plugin: Plugin) => { } module.exports = { actions } + +setPublicActions(actions) diff --git a/packages/gatsby/src/redux/plugin-runner.ts b/packages/gatsby/src/redux/plugin-runner.ts index 8c5b32fa14a92..105c478da44d5 100644 --- a/packages/gatsby/src/redux/plugin-runner.ts +++ b/packages/gatsby/src/redux/plugin-runner.ts @@ -2,7 +2,8 @@ import { Span } from "opentracing" import { emitter, store } from "./index" import apiRunnerNode from "../utils/api-runner-node" import { ActivityTracker } from "../../" -import { ICreateNodeAction } from "./types" +import { ICreateNodeStagingAction } from "./types" +import { commitStagingNodes } from "./actions/internal" type Plugin = any // TODO @@ -39,8 +40,12 @@ interface ICreatePageAction { pluginCreatorId: string componentPath: string } + transactionId?: string } +let hasOnCreatePage = false +let hasOnCreateNode = false + export const startPluginRunner = (): void => { const plugins = store.getState().flattenedPlugins const pluginsImplementingOnCreatePage = plugins.filter(plugin => @@ -50,11 +55,17 @@ export const startPluginRunner = (): void => { plugin.nodeAPIs.includes(`onCreateNode`) ) if (pluginsImplementingOnCreatePage.length > 0) { + hasOnCreatePage = true emitter.on(`CREATE_PAGE`, (action: ICreatePageAction) => { const page = action.payload apiRunnerNode( `onCreatePage`, - { page, traceId: action.traceId, parentSpan: action.parentSpan }, + { + page, + traceId: action.traceId, + parentSpan: action.parentSpan, + transactionId: action.transactionId, + }, { pluginSource: action.plugin.name, activity: action.activity } ) }) @@ -63,15 +74,25 @@ export const startPluginRunner = (): void => { // We make page nodes outside of the normal action for speed so we manually // call onCreateNode here for SitePage nodes. if (pluginsImplementingOnCreateNode.length > 0) { - emitter.on(`CREATE_NODE`, (action: ICreateNodeAction) => { + hasOnCreateNode = true + emitter.on(`CREATE_NODE_STAGING`, (action: ICreateNodeStagingAction) => { const node = action.payload if (node.internal.type === `SitePage`) { apiRunnerNode(`onCreateNode`, { node, parentSpan: action.parentSpan, traceTags: { nodeId: node.id, nodeType: node.internal.type }, + traceId: action.transactionId, + transactionId: action.transactionId, + waitForCascadingActions: true, + }).then(() => { + store.dispatch(commitStagingNodes(action.transactionId)) }) } }) } } + +export const shouldRunOnCreateNode = (): boolean => hasOnCreateNode + +export const shouldRunOnCreatePage = (): boolean => hasOnCreatePage diff --git a/packages/gatsby/src/redux/reducers/index.ts b/packages/gatsby/src/redux/reducers/index.ts index e2b394a5de986..a64c9ffcccba5 100644 --- a/packages/gatsby/src/redux/reducers/index.ts +++ b/packages/gatsby/src/redux/reducers/index.ts @@ -39,6 +39,7 @@ import { componentsUsingSlicesReducer } from "./components-using-slices" import { slicesByTemplateReducer } from "./slices-by-template" import { adapterReducer } from "./adapter" import { remoteFileAllowedUrlsReducer } from "./remote-file-allowed-urls" +import { nodesStagingReducer } from "./nodes-staging" /** * @property exports.nodesTouched Set @@ -85,4 +86,5 @@ export { telemetryReducer as telemetry, adapterReducer as adapter, remoteFileAllowedUrlsReducer as remoteFileAllowedUrls, + nodesStagingReducer as nodesStaging, } diff --git a/packages/gatsby/src/redux/reducers/nodes-staging.ts b/packages/gatsby/src/redux/reducers/nodes-staging.ts new file mode 100644 index 0000000000000..56a302bcaddef --- /dev/null +++ b/packages/gatsby/src/redux/reducers/nodes-staging.ts @@ -0,0 +1,55 @@ +import { ActionsUnion, IGatsbyState, TransactionActionsUnion } from "../types" + +function getInitialState(): IGatsbyState["nodesStaging"] { + return { nodes: new Map(), transactions: new Map() } +} + +function addActionToTransaction( + state: IGatsbyState["nodesStaging"], + action: TransactionActionsUnion +): void { + if (!action.transactionId) { + return + } + + const transaction = state.transactions.get(action.transactionId) + if (!transaction) { + state.transactions.set(action.transactionId, [action]) + } else { + transaction.push(action) + } +} + +export const nodesStagingReducer = ( + state: IGatsbyState["nodesStaging"] = getInitialState(), + action: ActionsUnion +): IGatsbyState["nodesStaging"] => { + switch (action.type) { + case `DELETE_CACHE`: + return getInitialState() + + case `CREATE_NODE_STAGING`: { + if (action.transactionId) { + // state.nodes.set(action.payload.id, action.payload) + addActionToTransaction(state, action) + } + + return state + } + + case `DELETE_NODE_STAGING`: { + if (action.payload && action.transactionId) { + // state.nodes.delete(action.payload.id) + addActionToTransaction(state, action) + } + return state + } + case `COMMIT_STAGING_NODES`: { + state.transactions.delete(action.payload.transactionId) + return state + } + + default: + return state + } +} diff --git a/packages/gatsby/src/redux/types.ts b/packages/gatsby/src/redux/types.ts index 44e8abaafcfa2..735f34f859b42 100644 --- a/packages/gatsby/src/redux/types.ts +++ b/packages/gatsby/src/redux/types.ts @@ -426,6 +426,10 @@ export interface IGatsbyState { config: IAdapterFinalConfig } remoteFileAllowedUrls: Set + nodesStaging: { + nodes: Map + transactions: Map> + } } export type GatsbyStateKeys = keyof IGatsbyState @@ -457,11 +461,13 @@ export type ActionsUnion = | IApiFinishedAction | ICreateFieldExtension | ICreateNodeAction + | ICreateNodeStagingAction | ICreatePageAction | ICreatePageDependencyAction | ICreateTypes | IDeleteCacheAction | IDeleteNodeAction + | IDeleteNodeStagingAction | IDeletePageAction | IPageQueryRunAction | IPrintTypeDefinitions @@ -541,7 +547,11 @@ export type ActionsUnion = | ISetAdapterAction | IDisablePluginsByNameAction | IAddImageCdnAllowedUrl + | ICommitStagingNodes +export type TransactionActionsUnion = + | ICreateNodeStagingAction + | IDeleteNodeStagingAction export interface IInitAction { type: `INIT` } @@ -823,6 +833,7 @@ export interface ICreatePageAction { contextModified?: boolean componentModified?: boolean slicesModified?: boolean + transactionId?: string } export interface ICreateSliceAction { @@ -1040,6 +1051,11 @@ export interface ICreateNodeAction { plugin: IGatsbyPlugin } +export type ICreateNodeStagingAction = Omit & { + type: `CREATE_NODE_STAGING` + transactionId: string +} + export interface IAddFieldToNodeAction { type: `ADD_FIELD_TO_NODE` payload: IGatsbyNode @@ -1059,6 +1075,11 @@ export interface IDeleteNodeAction { isRecursiveChildrenDelete?: boolean } +export type IDeleteNodeStagingAction = Omit & { + type: `DELETE_NODE_STAGING` + transactionId: string +} + export interface ISetSiteFlattenedPluginsAction { type: `SET_SITE_FLATTENED_PLUGINS` payload: IGatsbyState["flattenedPlugins"] @@ -1265,6 +1286,13 @@ export interface IClearJobV2Context { } } +export interface ICommitStagingNodes { + type: `COMMIT_STAGING_NODES` + payload: { + transactionId: string + } +} + export const HTTP_STATUS_CODE = { /** * The server has received the request headers and the client should proceed to send the request body