Skip to content

Commit f9eb09f

Browse files
authored
Merge pull request #24312 from github/repo-sync
repo sync
2 parents aa4f713 + 7505613 commit f9eb09f

File tree

3 files changed

+144
-19
lines changed

3 files changed

+144
-19
lines changed

lib/create-tree.js

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,68 @@ import fs from 'fs/promises'
33

44
import Page from './page.js'
55

6-
export default async function createTree(originalPath, rootPath) {
6+
export default async function createTree(originalPath, rootPath, previousTree) {
77
const basePath = rootPath || originalPath
88

99
// On recursive runs, this is processing page.children items in `/<link>` format.
1010
// If the path exists as is, assume this is a directory with a child index.md.
1111
// Otherwise, assume it's a child .md file and add `.md` to the path.
1212
let filepath
13+
let mtime
14+
// This kills two birds with one stone. We (attempt to) read it as a file,
15+
// to find out if it's a directory or a file and whence we know that
16+
// we also collect it's modification time.
1317
try {
14-
await fs.access(originalPath)
15-
filepath = `${originalPath}/index.md`
16-
} catch {
1718
filepath = `${originalPath}.md`
19+
mtime = await getMtime(filepath)
20+
} catch (error) {
21+
if (error.code !== 'ENOENT') {
22+
throw error
23+
}
24+
filepath = `${originalPath}/index.md`
25+
// Note, if this throws, that's quite fine. It usually means that
26+
// there's a `index.md` whose `children:` entry lists something that
27+
// doesn't exist on disk. So the writer who tries to preview the
28+
// page will see the error and it's hopefully clear what's actually
29+
// wrong.
30+
try {
31+
mtime = await getMtime(filepath)
32+
} catch (error) {
33+
if (error.code === 'ENOENT' && filepath.split(path.sep).includes('early-access')) {
34+
// Do not throw an error if Early Access is not available.
35+
console.warn(
36+
`${filepath} could not be turned into a Page, but is ignored because it's early-access`
37+
)
38+
return
39+
}
40+
throw error
41+
}
1842
}
1943

2044
const relativePath = filepath.replace(`${basePath}/`, '')
2145

22-
// Initialize the Page! This is where the file reads happen.
23-
const page = await Page.init({
24-
basePath,
25-
relativePath,
26-
languageCode: 'en',
27-
})
46+
// Reading in a file from disk is slow and best avoided if we can be
47+
// certain it isn't necessary. If the previous tree is known and that
48+
// tree's page node's `mtime` hasn't changed, we can use that instead.
49+
let page
50+
if (previousTree && previousTree.page.mtime === mtime) {
51+
// A save! We can use the same exact Page instance from the previous
52+
// tree because the assumption is that since the `.md` file it was
53+
// created from hasn't changed (on disk) the instance object wouldn't
54+
// change.
55+
page = previousTree.page
56+
} else {
57+
// Either the previous tree doesn't exist yet or the modification time
58+
// of the file on disk has changed.
59+
page = await Page.init({
60+
basePath,
61+
relativePath,
62+
languageCode: 'en',
63+
mtime,
64+
})
65+
}
2866

2967
if (!page) {
30-
// Do not throw an error if Early Access is not available.
31-
if (relativePath.startsWith('early-access')) {
32-
console.warn(
33-
`${relativePath} could not be turned into a Page, but is ignore because it's early-access`
34-
)
35-
return
36-
}
37-
3868
throw Error(`Cannot initialize page for ${filepath}`)
3969
}
4070

@@ -49,7 +79,12 @@ export default async function createTree(originalPath, rootPath) {
4979
item.childPages = (
5080
await Promise.all(
5181
item.page.children.map(
52-
async (child) => await createTree(path.posix.join(originalPath, child), basePath)
82+
async (child, i) =>
83+
await createTree(
84+
path.posix.join(originalPath, child),
85+
basePath,
86+
previousTree && previousTree.childPages[i]
87+
)
5388
)
5489
)
5590
).filter(Boolean)
@@ -58,6 +93,22 @@ export default async function createTree(originalPath, rootPath) {
5893
return item
5994
}
6095

96+
async function getMtime(filePath) {
97+
// Use mtimeMs, which is a regular floating point number, instead of the
98+
// mtime which is a Date based on that same number.
99+
// Otherwise, if we use the Date instances, we have to compare
100+
// them using `oneDate.getTime() === anotherDate.getTime()`.
101+
const { mtimeMs } = await fs.stat(filePath)
102+
// The `mtimeMs` is a number like `1669827766942.7954`
103+
// From the docs:
104+
// "The timestamp indicating the last time this file was modified expressed
105+
// in nanoseconds since the POSIX Epoch."
106+
// But the number isn't actually all that important. We just need it to
107+
// later be able to know if it changed. We round it to the nearest
108+
// millisecond.
109+
return Math.round(mtimeMs)
110+
}
111+
61112
function assertUniqueChildren(page) {
62113
if (page.children.length !== new Set(page.children).size) {
63114
const count = {}

middleware/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import handleErrors from './handle-errors.js'
2020
import handleInvalidPaths from './handle-invalid-paths.js'
2121
import handleNextDataPath from './handle-next-data-path.js'
2222
import detectLanguage from './detect-language.js'
23+
import reloadTree from './reload-tree.js'
2324
import context from './context.js'
2425
import shortVersions from './contextualizers/short-versions.js'
2526
import languageCodeRedirects from './redirects/language-code-redirects.js'
@@ -212,6 +213,7 @@ export default function (app) {
212213
// *** Config and context for redirects ***
213214
app.use(reqUtils) // Must come before events
214215
app.use(instrument(detectLanguage, './detect-language')) // Must come before context, breadcrumbs, find-page, handle-errors, homepages
216+
app.use(asyncMiddleware(instrument(reloadTree, './reload-tree'))) // Must come before context
215217
app.use(asyncMiddleware(instrument(context, './context'))) // Must come before early-access-*, handle-redirects
216218
app.use(instrument(shortVersions, './contextualizers/short-versions')) // Support version shorthands
217219

middleware/reload-tree.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* This exists for local previewing. Only.
3+
* We load in the entire tree on startup, then that's used for things like
4+
* sidebars and breadcrumbs and landing pages and ToC pages (and possibly
5+
* more).
6+
* When an individual page is requested, we always reload it from disk
7+
* in case it has changed. But that's not feasible with all 1k+ pages.
8+
*
9+
* The core of this middleware calls `createTree()` but by passing the
10+
* optional previous tree so that within `createTree` it can opt to
11+
* re-use those that haven't changed on disk.
12+
*
13+
* The intention here is so that things like sidebars can refresh
14+
* without having to restart the entire server.
15+
*/
16+
17+
import path from 'path'
18+
19+
import languages, { languageKeys } from '../lib/languages.js'
20+
import createTree from '../lib/create-tree.js'
21+
import warmServer from '../lib/warm-server.js'
22+
import { loadSiteTree, loadPages, loadPageMap } from '../lib/page-data.js'
23+
import loadRedirects from '../lib/redirects/precompile.js'
24+
25+
const languagePrefixRegex = new RegExp(`^/(${languageKeys.join('|')})(/|$)`)
26+
const englishPrefixRegex = /^\/en(\/|$)/
27+
28+
const isDev = process.env.NODE_ENV === 'development'
29+
30+
export default async function reloadTree(req, res, next) {
31+
if (!isDev) return next()
32+
// Filter out things like `/will/redirect` or `/_next/data/...`
33+
if (!languagePrefixRegex.test(req.pagePath)) return next()
34+
// We only bother if the loaded URL is something `/en/...`
35+
if (!englishPrefixRegex.test(req.pagePath)) return next()
36+
37+
const warmed = await warmServer()
38+
// For all the real English content, this usually takes about 30-60ms on
39+
// an Intel MacbookPro.
40+
const before = getMtimes(warmed.unversionedTree.en)
41+
warmed.unversionedTree.en = await createTree(
42+
path.join(languages.en.dir, 'content'),
43+
undefined,
44+
warmed.unversionedTree.en
45+
)
46+
const after = getMtimes(warmed.unversionedTree.en)
47+
// The next couple of operations are much slower (in total) than
48+
// refrehing the tree. So we want to know if the tree changed before
49+
// bothering.
50+
// If refreshing of the `.en` part of the `unversionedTree` takes 40ms
51+
// then the following operations takes about 140ms.
52+
if (before !== after) {
53+
warmed.siteTree = await loadSiteTree(warmed.unversionedTree)
54+
warmed.pageList = await loadPages(warmed.unversionedTree)
55+
warmed.pageMap = await loadPageMap(warmed.pageList)
56+
warmed.redirects = await loadRedirects(warmed.pageList)
57+
}
58+
59+
return next()
60+
}
61+
62+
// Given a tree, return a number that represents the mtimes for all pages
63+
// in the tree.
64+
// You can use this to compute it before and after the tree is (maybe)
65+
// mutated and if the numbers *change* you can know the tree changed.
66+
function getMtimes(tree) {
67+
let mtimes = tree.page.mtime
68+
for (const child of tree.childPages || []) {
69+
mtimes += getMtimes(child)
70+
}
71+
return mtimes
72+
}

0 commit comments

Comments
 (0)