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

[v2] improve how we load GraphQL query results in development/production (aka Ludicrous Mode) #4555

Merged
merged 38 commits into from
Apr 6, 2018
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e218c1f
Create placeholder JSON store
m-allanson Mar 15, 2018
0894947
Rename
m-allanson Mar 15, 2018
4ca7dda
Websocket placeholder
m-allanson Mar 15, 2018
cf55f6b
Push query results JSON over websockets
m-allanson Mar 15, 2018
293f36a
More descriptive variable name
m-allanson Mar 16, 2018
7e852cf
Fix queries being overwritten
m-allanson Mar 16, 2018
2adaffe
Remove eslint-disable flag
m-allanson Mar 16, 2018
c03cc34
Remove junk
m-allanson Mar 16, 2018
ca50615
test require error fix for windows
pieh Mar 15, 2018
460ca7d
dont require json data in sync-require
pieh Mar 15, 2018
f4c92e5
dont add layout data to json array multiple times
pieh Mar 16, 2018
e6239ee
initial async loading
pieh Mar 16, 2018
4e064d7
revert saving json directly to public for now
pieh Mar 16, 2018
1801fa7
updated production-app to sync with prop name change in ComponentRend…
pieh Mar 16, 2018
53bc172
we load json data via json-loader component in develop and not handli…
pieh Mar 16, 2018
adc3ce1
hashes for json files
pieh Mar 17, 2018
9008479
fix preloading, use xhr instead of fetch - for some reason can't forc…
pieh Mar 20, 2018
3050620
dont use full paths in dataPath - remove static/d/ path and .json ext…
pieh Mar 21, 2018
46e5235
Merge pull request #4635 from pieh/json-loader
m-allanson Mar 21, 2018
a3b6aee
Enable cached query results to be loaded
m-allanson Mar 22, 2018
66e462c
Don't dump all query results out to the client
m-allanson Mar 26, 2018
8196649
Merge pull request #4658 from m-allanson/load-develop-query-results
m-allanson Mar 26, 2018
7602a73
Merge remote-tracking branch 'upstream/v2' into json-loader-w4
pieh Mar 26, 2018
f0b3b0c
fix preload link to json data
pieh Mar 26, 2018
c268376
Merge remote-tracking branch 'upstream/v2' into json-loader
pieh Mar 28, 2018
2d9e286
remove not used function
pieh Mar 28, 2018
95fcd4a
remove more not used code
pieh Mar 28, 2018
20252dc
Update to latest webpack/mini-css-extract-plugin
KyleAMathews Mar 29, 2018
44797c3
don't write new (a)sync-requires.js if components didn't change (#4759)
pieh Mar 29, 2018
467a792
create just one websocket client (#4763)
pieh Mar 29, 2018
dd442c7
Filter out duplicate query jobs and create secondary queue for jobs i…
KyleAMathews Mar 29, 2018
abcb58c
[json-loader] Don't emit new file node until previous is finished pro…
KyleAMathews Apr 3, 2018
e374818
[json-loader] Only log file events if we're past bootstrap (#4826)
KyleAMathews Apr 3, 2018
62754de
[json-loader] dont recompile on data change - part 2 (#4837)
pieh Apr 5, 2018
4a09f74
[json-loader] develop - reading results from file improvments (#4850)
pieh Apr 5, 2018
868ef8b
Add query prioritization based on what page(s) user(s) are on
KyleAMathews Apr 5, 2018
b4fdecb
Add initial forward slash
KyleAMathews Apr 6, 2018
1bb7cc4
Actually this is how we add back the initial forward slash
KyleAMathews Apr 6, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/gatsby-source-filesystem/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"mime": "^2.2.0",
"pretty-bytes": "^4.0.2",
"slash": "^1.0.0",
"valid-url": "^1.0.9"
"valid-url": "^1.0.9",
"xstate": "^3.1.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0-beta.42",
Expand Down
7 changes: 0 additions & 7 deletions packages/gatsby-source-filesystem/src/create-file-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ const prettyBytes = require(`pretty-bytes`)
const md5File = require(`bluebird`).promisify(require(`md5-file`))
const crypto = require(`crypto`)

const createId = path => {
const slashed = slash(path)
return `${slashed} absPath of file`
}

exports.createId = createId

exports.createFileNode = async (
pathToFile,
createNodeId,
Expand Down
159 changes: 143 additions & 16 deletions packages/gatsby-source-filesystem/src/gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,77 @@
const chokidar = require(`chokidar`)
const fs = require(`fs`)
const { Machine } = require(`xstate`)

const { createId, createFileNode } = require(`./create-file-node`)

/**
* Create a state machine to manage Chokidar's not-ready/ready states and for
* emitting file system events into Gatsby.
*
* On the latter, this solves the problem where if you call createNode for the
* same File node in quick succession, this can leave Gatsby's internal state
* in disarray causing queries to fail. The latter state machine tracks when
* Gatsby is "processing" a node update or when it's "idle". If updates come in
* while Gatsby is processing, we queue them until the system returns to an
* "idle" state.
*/
const fsMachine = Machine({
key: "emitFSEvents",
parallel: true,
strict: true,
states: {
CHOKIDAR: {
initial: `CHOKIDAR_NOT_READY`,
states: {
CHOKIDAR_NOT_READY: {
on: {
CHOKIDAR_READY: "CHOKIDAR_WATCHING",
BOOTSTRAP_FINISHED: "CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED",
},
},
CHOKIDAR_WATCHING: {
on: {
BOOTSTRAP_FINISHED: "CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED",
CHOKIDAR_READY: "CHOKIDAR_WATCHING",
},
},
CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED: {
on: {
CHOKIDAR_READY: "CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED",
},
},
},
},
PROCESSING: {
initial: `BOOTSTRAPPING`,
states: {
BOOTSTRAPPING: {
on: {
BOOTSTRAP_FINISHED: "IDLE",
},
},
IDLE: {
on: {
EMIT_FS_EVENT: `PROCESSING`,
},
},
PROCESSING: {
on: {
QUERY_QUEUE_DRAINED: `IDLE`,
TOUCH_NODE: `IDLE`,
},
},
},
},
},
})

let currentState = fsMachine.initialState

const fileQueue = new Map()

exports.sourceNodes = (
{ actions, getNode, createNodeId, hasNodeChanged, reporter },
{ actions, getNode, createNodeId, hasNodeChanged, reporter, emitter },
pluginOptions
) => {
const { createNode, deleteNode } = actions
Expand All @@ -21,8 +88,36 @@ Please pick a path to an existing directory.
See docs here - https://www.gatsbyjs.org/packages/gatsby-source-filesystem/
`)
}
let fileNodeQueue = new Map()

// Once bootstrap is finished, we only let one File node update go through
// the system at a time.
emitter.on(`BOOTSTRAP_FINISHED`, () => {
currentState = fsMachine.transition(
currentState.value,
`BOOTSTRAP_FINISHED`
)
})
emitter.on(`TOUCH_NODE`, () => {
// If we create a node which is the same as the previous version, createNode
// returns TOUCH_NODE and then nothing else happens so we listen to that
// to return the state back to IDLE.
currentState = fsMachine.transition(currentState.value, `TOUCH_NODE`)
})

let ready = false
emitter.on(`QUERY_QUEUE_DRAINED`, () => {
currentState = fsMachine.transition(
currentState.value,
`QUERY_QUEUE_DRAINED`
)
// If we have any updates queued, run one of them now.
if (fileNodeQueue.size > 0) {
const toProcess = fileNodeQueue.get(Array.from(fileNodeQueue.keys())[0])
fileNodeQueue.delete(toProcess.id)
currentState = fsMachine.transition(currentState.value, `EMIT_FS_EVENT`)
createNode(toProcess)
}
})

const watcher = chokidar.watch(pluginOptions.path, {
ignored: [
Expand All @@ -36,8 +131,21 @@ See docs here - https://www.gatsbyjs.org/packages/gatsby-source-filesystem/
],
})

const createAndProcessNode = path =>
createFileNode(path, createNodeId, pluginOptions).then(createNode)
const createAndProcessNode = path => {
const fileNodePromise = createFileNode(
path,
createNodeId,
pluginOptions
).then(fileNode => {
if (currentState.value.PROCESSING === `PROCESSING`) {
fileNodeQueue.set(fileNode.id, fileNode)
} else {
currentState = fsMachine.transition(currentState.value, `EMIT_FS_EVENT`)
createNode(fileNode)
}
})
return fileNodePromise
}

// For every path that is reported before the 'ready' event, we throw them
// into a queue and then flush the queue when 'ready' event arrives.
Expand All @@ -50,49 +158,68 @@ See docs here - https://www.gatsbyjs.org/packages/gatsby-source-filesystem/
}

watcher.on(`add`, path => {
if (ready) {
reporter.info(`added file at ${path}`)
if (currentState.value.CHOKIDAR !== `CHOKIDAR_NOT_READY`) {
if (
currentState.value.CHOKIDAR === `CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED`
) {
reporter.info(`added file at ${path}`)
}
createAndProcessNode(path).catch(err => reporter.error(err))
} else {
pathQueue.push(path)
}
})

watcher.on(`change`, path => {
reporter.info(`changed file at ${path}`)
if (
currentState.value.CHOKIDAR === `CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED`
) {
reporter.info(`changed file at ${path}`)
}
createAndProcessNode(path).catch(err => reporter.error(err))
})

watcher.on(`unlink`, path => {
reporter.info(`file deleted at ${path}`)
const node = getNode(createId(path))
if (
currentState.value.CHOKIDAR === `CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED`
) {
reporter.info(`file deleted at ${path}`)
}
const node = getNode(createNodeId(path))
// It's possible the file node was never created as sometimes tools will
// write and then immediately delete temporary files to the file system.
if (node) {
currentState = fsMachine.transition(currentState.value, `EMIT_FS_EVENT`)
deleteNode(node.id, node)
}
})

watcher.on(`addDir`, path => {
if (ready) {
reporter.info(`added directory at ${path}`)
if (currentState.value.CHOKIDAR !== `CHOKIDAR_NOT_READY`) {
if (
currentState.value.CHOKIDAR === `CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED`
) {
reporter.info(`added directory at ${path}`)
}
createAndProcessNode(path).catch(err => reporter.error(err))
} else {
pathQueue.push(path)
}
})

watcher.on(`unlinkDir`, path => {
reporter.info(`directory deleted at ${path}`)
const node = getNode(createId(path))
if (
currentState.value.CHOKIDAR === `CHOKIDAR_WATCHING_BOOTSTRAP_FINISHED`
) {
reporter.info(`directory deleted at ${path}`)
}
const node = getNode(createNodeId(path))
deleteNode(node.id, node)
})

return new Promise((resolve, reject) => {
watcher.on(`ready`, () => {
if (ready) return

ready = true
currentState = fsMachine.transition(currentState.value, `CHOKIDAR_READY`)
flushPathQueue().then(resolve, reject)
})
})
Expand Down
7 changes: 6 additions & 1 deletion packages/gatsby/cache-dir/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ window.___emitter = emitter
// Let the site/plugins run code very early.
apiRunnerAsync(`onClientEntry`).then(() => {
// Hook up the client to socket.io on server
socketIo()
const socket = socketIo()
if (socket) {
socket.on(`reload`, () => {
window.location.reload()
})
}

/**
* Service Workers are persistent by nature. They stick around,
Expand Down
4 changes: 2 additions & 2 deletions packages/gatsby/cache-dir/component-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class ComponentRenderer extends React.Component {
})
const replacementComponent = pluginResponses[0]
// If page.
if (this.props.page) {
if (this.props.isPage) {
if (this.state.pageResources) {
return (
replacementComponent ||
Expand Down Expand Up @@ -163,7 +163,7 @@ class ComponentRenderer extends React.Component {
}

ComponentRenderer.propTypes = {
page: PropTypes.bool,
isPage: PropTypes.bool,
layout: PropTypes.bool,
location: PropTypes.object,
}
Expand Down
98 changes: 98 additions & 0 deletions packages/gatsby/cache-dir/json-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { createElement } from "react"
import { Route } from "react-router-dom"
import ComponentRenderer from "./component-renderer"
import syncRequires from "./sync-requires"
import socketIo from "./socketIo"
import omit from "lodash/omit"
import get from "lodash/get"

const getPathFromProps = props => {
if (props.isPage) {
return get(props.pageResources, `page.path`)
} else {
return `/dev-404-page/`
}
}

class JSONStore extends React.Component {
constructor(props) {
super(props)
this.state = {
data: {},
path: null,
}

this.setPageData = this.setPageData.bind(this)

this.socket = socketIo()
this.socket.on(`queryResult`, this.setPageData)
}

componentWillMount() {
this.registerPath(getPathFromProps(this.props))
}

componentWillReceiveProps(nextProps) {
const { path } = this.state
const newPath = getPathFromProps(nextProps)

if (path !== newPath) {
this.unregisterPath(path)
this.registerPath(newPath)
}
}

registerPath(path) {
this.setState({ path })
this.socket.emit(`registerPath`, path)
}

unregisterPath(path) {
this.setState({ path: null })
this.socket.emit(`unregisterPath`, path)
}

componentWillUnmount() {
this.unregisterPath(this.state.path)
}

setPageData({ path, result }) {
this.setState({
data: {
...this.state.data,
[path]: result,
},
})
}

render() {
const { isPage, pages, pageResources } = this.props
const data = this.state.data[this.state.path]
const propsWithoutPages = omit(this.props, `pages`)

if (!data) {
return null
} else if (isPage) {
return createElement(ComponentRenderer, {
key: `normal-page`,
...propsWithoutPages,
...data,
})
} else {
const dev404Page = pages.find(p => /^\/dev-404-page/.test(p.path))
return createElement(Route, {
key: `404-page`,
component: props =>
createElement(
syncRequires.components[dev404Page.componentChunkName],
{
...propsWithoutPages,
...data,
}
),
})
}
}
}

export default JSONStore
Loading