Skip to content

Commit 7880240

Browse files
authored
fix(gatsby): garbage collect stateful pages (#28760)
* add stateful page to artifacts tests * fix(gatsby): garbage collect stateful pages * Revert "fix: clear tracked queries when deleting stale page-data files (#29431)" (#30848) This reverts commit 478cf68.
1 parent 135ddd6 commit 7880240

15 files changed

Lines changed: 285 additions & 88 deletions

File tree

integration-tests/artifacts/__tests__/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ describe(`Second run (different pages created, data changed)`, () => {
530530
`/static-query-result-tracking/stable/`,
531531
`/static-query-result-tracking/rerun-query-but-dont-recreate-html/`,
532532
`/page-that-will-have-trailing-slash-removed`,
533+
`/stateful-page-not-recreated-in-third-run/`,
533534
]
534535

535536
const expectedPages = [
@@ -614,6 +615,7 @@ describe(`Third run (js change, all pages are recreated)`, () => {
614615
`/stale-pages/only-in-first/`,
615616
`/page-query-dynamic-1/`,
616617
`/page-query-dynamic-2/`,
618+
`/stateful-page-not-recreated-in-third-run/`,
617619
]
618620

619621
let changedFileOriginalContent
@@ -699,6 +701,7 @@ describe(`Fourth run (gatsby-browser change - cache get invalidated)`, () => {
699701
const expectedPages = [
700702
`/stale-pages/only-not-in-first`,
701703
`/page-query-dynamic-4/`,
704+
`/stateful-page-not-recreated-in-third-run/`,
702705
]
703706

704707
const unexpectedPages = [

integration-tests/artifacts/gatsby-node.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ exports.createPages = async ({ actions, graphql }) => {
197197
}
198198
}
199199

200+
exports.createPagesStatefully = async ({ actions }) => {
201+
if (runNumber !== 3) {
202+
actions.createPage({
203+
path: `/stateful-page-not-recreated-in-third-run/`,
204+
component: require.resolve(`./src/templates/dummy`),
205+
context: {
206+
dummyId: `stateful-page`,
207+
},
208+
})
209+
}
210+
}
211+
200212
exports.onPreBuild = () => {
201213
console.log(`[test] onPreBuild`)
202214
changedBrowserCompilationHash = `not-changed`

packages/gatsby/src/bootstrap/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
sourceNodes,
77
buildSchema,
88
createPages,
9-
createPagesStatefully,
109
extractQueries,
1110
writeOutRedirects,
1211
postBootstrap,
@@ -32,9 +31,12 @@ export async function bootstrap(
3231

3332
const parentSpan = tracer.startSpan(`bootstrap`, spanArgs)
3433

35-
const bootstrapContext: IBuildContext = {
34+
const bootstrapContext: IBuildContext & {
35+
shouldRunCreatePagesStatefully: boolean
36+
} = {
3637
...initialContext,
3738
parentSpan,
39+
shouldRunCreatePagesStatefully: true,
3840
}
3941

4042
const context = {
@@ -54,8 +56,6 @@ export async function bootstrap(
5456

5557
await createPages(context)
5658

57-
await createPagesStatefully(context)
58-
5959
await handleStalePageData()
6060

6161
await rebuildSchemaWithSitePage(context)

packages/gatsby/src/redux/reducers/queries.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,6 @@ export function queriesReducer(
9090
state.deletedQueries.add(action.payload.path)
9191
return state
9292
}
93-
case `DELETED_STALE_PAGE_DATA_FILES`: {
94-
// this action is a hack/hot fix
95-
// it should be removed/reverted when we start persisting pages state
96-
for (const queryId of action.payload.pagePathsToClear) {
97-
for (const component of state.trackedComponents.values()) {
98-
component.pages.delete(queryId)
99-
}
100-
state = clearNodeDependencies(state, queryId)
101-
state = clearConnectionDependencies(state, queryId)
102-
state.trackedQueries.delete(queryId)
103-
}
104-
105-
return state
106-
}
10793
case `API_FINISHED`: {
10894
if (action.payload.apiName !== `createPages`) {
10995
return state

packages/gatsby/src/redux/types.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,6 @@ export type ActionsUnion =
372372
| IDisableTypeInferenceAction
373373
| ISetProgramAction
374374
| ISetProgramExtensions
375-
| IDeletedStalePageDataFiles
376375
| IRemovedHtml
377376
| ITrackedHtmlCleanup
378377
| IGeneratedHtml
@@ -820,13 +819,6 @@ interface ISetProgramExtensions {
820819
payload: Array<string>
821820
}
822821

823-
interface IDeletedStalePageDataFiles {
824-
type: `DELETED_STALE_PAGE_DATA_FILES`
825-
payload: {
826-
pagePathsToClear: Set<string>
827-
}
828-
}
829-
830822
interface IRemovedHtml {
831823
type: `HTML_REMOVED`
832824
payload: string
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { createPages } from "../create-pages"
2+
import { store, emitter } from "../../redux"
3+
import { actions } from "../../redux/actions"
4+
import apiRunnerNode from "../../utils/api-runner-node"
5+
import * as path from "path"
6+
7+
jest.mock(`../../utils/api-runner-node`)
8+
9+
jest.mock(`../../utils/js-chunk-names`, () => {
10+
return { generateComponentChunkName: (): string => `--mocked--` }
11+
})
12+
13+
let mockAPIs = {}
14+
15+
const component = path.join(process.cwd(), `wat`)
16+
17+
const testPlugin = {
18+
name: `gatsby-source-test`,
19+
version: `1.0.0`,
20+
}
21+
22+
describe(`createPages service cleans up not recreated pages`, () => {
23+
let RealDateNow
24+
let DateNowCallCount = 0
25+
26+
let createPagesRun = 1
27+
let createPagesStatefullyRun = 1
28+
29+
let createPagesHook
30+
let createPagesStatefullyHook
31+
32+
let deletePageActions
33+
34+
function onDeletePage(deletePageAction): void {
35+
deletePageActions.push(deletePageAction)
36+
}
37+
38+
beforeAll(() => {
39+
RealDateNow = Date.now
40+
Date.now = jest.fn(() => ++DateNowCallCount)
41+
apiRunnerNode.mockImplementation((apiName, opts = {}) => {
42+
if (mockAPIs[apiName]) {
43+
return mockAPIs[apiName](
44+
{
45+
actions: Object.keys(actions).reduce((acc, actionName) => {
46+
acc[actionName] = (...args): any =>
47+
store.dispatch(actions[actionName](...args, testPlugin, opts))
48+
return acc
49+
}, {}),
50+
},
51+
{}
52+
)
53+
}
54+
return undefined
55+
})
56+
57+
createPagesHook = mockAPIs[`createPages`] = jest.fn(
58+
({ actions }, _pluginOptions) => {
59+
actions.createPage({
60+
path: `/stateless/stable`,
61+
component,
62+
})
63+
actions.createPage({
64+
path: `/stateless/dynamic/${createPagesRun}`,
65+
component,
66+
})
67+
createPagesRun++
68+
}
69+
)
70+
71+
createPagesStatefullyHook = mockAPIs[`createPagesStatefully`] = jest.fn(
72+
({ actions }, _pluginOptions) => {
73+
actions.createPage({
74+
path: `/stateful/stable`,
75+
component,
76+
})
77+
actions.createPage({
78+
path: `/stateful/dynamic/${createPagesStatefullyRun}`,
79+
component,
80+
})
81+
createPagesStatefullyRun++
82+
}
83+
)
84+
85+
emitter.on(`DELETE_PAGE`, onDeletePage)
86+
})
87+
88+
beforeEach(() => {
89+
createPagesRun = 1
90+
createPagesStatefullyRun = 1
91+
createPagesHook.mockClear()
92+
createPagesStatefullyHook.mockClear()
93+
deletePageActions = []
94+
95+
store.dispatch({ type: `DELETE_CACHE` })
96+
})
97+
98+
afterAll(() => {
99+
Date.now = RealDateNow
100+
mockAPIs = {}
101+
emitter.off(`DELETE_PAGE`, onDeletePage)
102+
})
103+
104+
it.each([
105+
[`From cold cache`, { cacheStatus: `COLD` }],
106+
[`From warm cache`, { cacheStatus: `WARM` }],
107+
])(`%s`, async (_, { cacheStatus }) => {
108+
expect(deletePageActions).toEqual([])
109+
expect(store.getState().pages.size).toEqual(0)
110+
111+
if (cacheStatus === `WARM`) {
112+
// add some junk
113+
store.dispatch(
114+
actions.createPage(
115+
{
116+
path: `/stateless/junk`,
117+
component,
118+
context: {},
119+
},
120+
testPlugin
121+
)
122+
)
123+
store.dispatch(
124+
actions.createPage(
125+
{
126+
path: `/stateful/junk`,
127+
component,
128+
context: {},
129+
},
130+
testPlugin,
131+
{
132+
traceId: `initial-createPagesStatefully`,
133+
}
134+
)
135+
)
136+
137+
expect(store.getState().pages.size).toEqual(2)
138+
expect(Array.from(store.getState().pages.keys())).toEqual([
139+
`/stateless/junk`,
140+
`/stateful/junk`,
141+
])
142+
expect(
143+
store.getState().pages.get(`/stateless/junk`)
144+
.isCreatedByStatefulCreatePages
145+
).toEqual(false)
146+
expect(
147+
store.getState().pages.get(`/stateful/junk`)
148+
.isCreatedByStatefulCreatePages
149+
).toEqual(true)
150+
} else {
151+
expect(store.getState().pages.size).toEqual(0)
152+
}
153+
154+
expect(mockAPIs[`createPages`]).toHaveBeenCalledTimes(0)
155+
expect(mockAPIs[`createPagesStatefully`]).toHaveBeenCalledTimes(0)
156+
157+
await createPages({ store, shouldRunCreatePagesStatefully: true })
158+
159+
expect(mockAPIs[`createPages`]).toHaveBeenCalledTimes(1)
160+
expect(mockAPIs[`createPagesStatefully`]).toHaveBeenCalledTimes(1)
161+
expect(store.getState().pages.size).toEqual(4)
162+
expect(Array.from(store.getState().pages.keys())).toEqual(
163+
expect.arrayContaining([
164+
`/stateless/stable`,
165+
`/stateless/dynamic/1`,
166+
`/stateful/stable`,
167+
`/stateful/dynamic/1`,
168+
])
169+
)
170+
171+
if (cacheStatus === `WARM`) {
172+
// "junk" pages were not recreated, so we expect DELETE_PAGE action to be emitted for those
173+
expect(deletePageActions).toEqual(
174+
expect.arrayContaining([
175+
expect.objectContaining({
176+
type: `DELETE_PAGE`,
177+
payload: expect.objectContaining({
178+
path: `/stateless/junk`,
179+
}),
180+
}),
181+
])
182+
)
183+
expect(deletePageActions).toEqual(
184+
expect.arrayContaining([
185+
expect.objectContaining({
186+
type: `DELETE_PAGE`,
187+
payload: expect.objectContaining({
188+
path: `/stateful/junk`,
189+
}),
190+
}),
191+
])
192+
)
193+
}
194+
195+
await createPages({ store, shouldRunCreatePagesStatefully: false })
196+
197+
// createPagesStatefully should not be called and stateful pages should remain as they were before calling `createPages` service
198+
expect(mockAPIs[`createPages`]).toHaveBeenCalledTimes(2)
199+
expect(mockAPIs[`createPagesStatefully`]).toHaveBeenCalledTimes(1)
200+
expect(store.getState().pages.size).toEqual(4)
201+
202+
expect(Array.from(store.getState().pages.keys())).toEqual(
203+
expect.arrayContaining([
204+
`/stateless/stable`,
205+
`/stateless/dynamic/2`,
206+
`/stateful/stable`,
207+
`/stateful/dynamic/1`,
208+
])
209+
)
210+
211+
// 1st dynamic page was not recreated so we expect that we emitted DELETE_PAGE action
212+
expect(deletePageActions).toEqual(
213+
expect.arrayContaining([
214+
expect.objectContaining({
215+
type: `DELETE_PAGE`,
216+
payload: expect.objectContaining({
217+
path: `/stateless/dynamic/1`,
218+
}),
219+
}),
220+
])
221+
)
222+
})
223+
})

packages/gatsby/src/services/create-pages-statefully.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)