diff --git a/docs/config.md b/docs/config.md index 1d036f253..c42a81ab0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -27,6 +27,16 @@ The path to the source root; defaults to `src`. (Prior to { + async load({useStale = false, cacheExpiration}: LoadOptions = {}, effects = defaultEffects): Promise { const loaderPath = join(this.root, this.path); const key = join(this.root, this.targetPath); let command = runningCommands.get(key); @@ -409,7 +412,9 @@ abstract class AbstractLoader implements Loader { const loaderStat = await maybeStat(loaderPath); const cacheStat = await maybeStat(cachePath); if (!cacheStat) effects.output.write(faint("[missing] ")); - else if (cacheStat.mtimeMs < loaderStat!.mtimeMs) { + else if (cacheExpiration !== undefined && (Date.now() - cacheStat.mtimeMs > cacheExpiration * 1000)) { + effects.output.write(faint("[expired] ")); + } else if (cacheStat.mtimeMs < loaderStat!.mtimeMs) { if (useStale) return effects.output.write(faint("[using stale] ")), outputPath; else effects.output.write(faint("[stale] ")); } else return effects.output.write(faint("[fresh] ")), outputPath; diff --git a/src/preview.ts b/src/preview.ts index 130340dcb..a4536844b 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -119,7 +119,7 @@ export class PreviewServer { _handleRequest: RequestListener = async (req, res) => { const config = await this._readConfig(); - const {root, loaders, duckdb} = config; + const {root, loaders, duckdb, cacheExpiration} = config; if (this._verbose) console.log(faint(req.method!), req.url); const url = new URL(req.url!, "http://localhost"); const {origin} = req.headers; @@ -180,7 +180,7 @@ export class PreviewServer { const path = pathname.slice("/_file".length); const loader = loaders.find(path); if (!loader) throw enoent(path); - send(req, await loader.load(), {root}).pipe(res); + send(req, await loader.load({cacheExpiration}), {root}).pipe(res); } else { if ((pathname = normalize(pathname)).startsWith("..")) throw new Error("Invalid path: " + pathname); diff --git a/test/loader-test.ts b/test/loader-test.ts index f2d4a7172..deb634ca6 100644 --- a/test/loader-test.ts +++ b/test/loader-test.ts @@ -114,6 +114,43 @@ describe("LoaderResolver.find(path)", () => { ] ); }); + it("data loaders reload when cacheExpiration is set and expired", async () => { + const out = [] as string[]; + const outputEffects: LoadEffects = { + logger: {log() {}, warn() {}, error() {}}, + output: { + write(a) { + out.push(a); + } + } + }; + const loader = loaders.find("/dataloaders/data1.txt")!; + // remove the cache set by another test (unless we it.only this test). + try { + await unlink("test/.observablehq/cache/dataloaders/data1.txt"); + } catch { + // ignore; + } + // populate the cache (missing) + await loader.load(undefined, outputEffects); + // run again (fresh) + await loader.load(undefined, outputEffects); + // run again (stale due to cacheExpiration) + await loader.load({cacheExpiration: 0}, outputEffects); + + assert.deepStrictEqual( + // eslint-disable-next-line no-control-regex + out.map((l) => l.replaceAll(/\x1b\[[0-9]+m/g, "")), + [ + "load /dataloaders/data1.txt → ", + "[missing] ", + "load /dataloaders/data1.txt → ", + "[fresh] ", + "load /dataloaders/data1.txt → ", + "[expired] " + ] + ); + }); }); describe("LoaderResolver.getSourceFileHash(path)", () => {