fix: properly lock across source and content refresh by sharing a mutex

This commit is contained in:
Jacky Zhao 2023-08-22 22:27:41 -07:00
parent 8b63ff882a
commit 99dbe525d9
2 changed files with 19 additions and 12 deletions

View File

@ -394,12 +394,12 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
const buildMutex = new Mutex() const buildMutex = new Mutex()
const timeoutIds = new Set() const timeoutIds = new Set()
let firstBuild = true let cleanupBuild = null
const build = async (clientRefresh) => { const build = async (clientRefresh) => {
const release = await buildMutex.acquire() const release = await buildMutex.acquire()
if (firstBuild) {
firstBuild = false if (cleanupBuild) {
} else { await cleanupBuild()
console.log(chalk.yellow("Detected a source code change, doing a hard rebuild...")) console.log(chalk.yellow("Detected a source code change, doing a hard rebuild..."))
} }
@ -408,6 +408,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
console.log(`Reason: ${chalk.grey(err)}`) console.log(`Reason: ${chalk.grey(err)}`)
process.exit(1) process.exit(1)
}) })
release()
if (argv.bundleInfo) { if (argv.bundleInfo) {
const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs" const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
@ -423,9 +424,8 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
// bypass module cache // bypass module cache
// https://github.com/nodejs/modules/issues/307 // https://github.com/nodejs/modules/issues/307
const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`) const { default: buildQuartz } = await import(cacheFile + `?update=${randomUUID()}`)
await buildQuartz(argv, clientRefresh) cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh)
clientRefresh() clientRefresh()
release()
} }
const rebuild = (clientRefresh) => { const rebuild = (clientRefresh) => {

View File

@ -18,7 +18,7 @@ import { trace } from "./util/trace"
import { options } from "./util/sourcemap" import { options } from "./util/sourcemap"
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
async function buildQuartz(argv: Argv, clientRefresh: () => void) { async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
argv, argv,
cfg, cfg,
@ -38,6 +38,7 @@ async function buildQuartz(argv: Argv, clientRefresh: () => void) {
console.log(` Emitters: ${pluginNames("emitters").join(", ")}`) console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
} }
const release = await mut.acquire()
perf.addEvent("clean") perf.addEvent("clean")
await rimraf(output) await rimraf(output)
console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`) console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
@ -56,15 +57,17 @@ async function buildQuartz(argv: Argv, clientRefresh: () => void) {
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
await emitContent(ctx, filteredContent) await emitContent(ctx, filteredContent)
console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`)) console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
release()
if (argv.serve) { if (argv.serve) {
return startServing(ctx, parsedFiles, clientRefresh) return startServing(ctx, mut, parsedFiles, clientRefresh)
} }
} }
// setup watcher for rebuilds // setup watcher for rebuilds
async function startServing( async function startServing(
ctx: BuildCtx, ctx: BuildCtx,
mut: Mutex,
initialContent: ProcessedContent[], initialContent: ProcessedContent[],
clientRefresh: () => void, clientRefresh: () => void,
) { ) {
@ -78,7 +81,6 @@ async function startServing(
} }
const initialSlugs = ctx.allSlugs const initialSlugs = ctx.allSlugs
const buildMutex = new Mutex()
const timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set() const timeoutIds: Set<ReturnType<typeof setTimeout>> = new Set()
const toRebuild: Set<FilePath> = new Set() const toRebuild: Set<FilePath> = new Set()
const toRemove: Set<FilePath> = new Set() const toRemove: Set<FilePath> = new Set()
@ -111,7 +113,7 @@ async function startServing(
// debounce rebuilds every 250ms // debounce rebuilds every 250ms
timeoutIds.add( timeoutIds.add(
setTimeout(async () => { setTimeout(async () => {
const release = await buildMutex.acquire() const release = await mut.acquire()
timeoutIds.forEach((id) => clearTimeout(id)) timeoutIds.forEach((id) => clearTimeout(id))
timeoutIds.clear() timeoutIds.clear()
@ -164,11 +166,16 @@ async function startServing(
.on("add", (fp) => rebuild(fp, "add")) .on("add", (fp) => rebuild(fp, "add"))
.on("change", (fp) => rebuild(fp, "change")) .on("change", (fp) => rebuild(fp, "change"))
.on("unlink", (fp) => rebuild(fp, "delete")) .on("unlink", (fp) => rebuild(fp, "delete"))
return async () => {
timeoutIds.forEach((id) => clearTimeout(id))
await watcher.close()
}
} }
export default async (argv: Argv, clientRefresh: () => void) => { export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
try { try {
return await buildQuartz(argv, clientRefresh) return await buildQuartz(argv, mut, clientRefresh)
} catch (err) { } catch (err) {
trace("\nExiting Quartz due to a fatal error", err as Error) trace("\nExiting Quartz due to a fatal error", err as Error)
} }