Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export async function build(
let sourcePath = join(root, file);
const outputPath = join("_file", file);
if (!existsSync(sourcePath)) {
const loader = Loader.find(root, file);
const loader = Loader.find(root, file, {useStale: true});
if (!loader) {
effects.logger.error("missing referenced file", sourcePath);
continue;
Expand Down
33 changes: 23 additions & 10 deletions src/dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface LoaderOptions {
path: string;
sourceRoot: string;
targetPath: string;
useStale: boolean;
}

export abstract class Loader {
Expand All @@ -57,10 +58,16 @@ export abstract class Loader {
*/
readonly targetPath: string;

constructor({path, sourceRoot, targetPath}: LoaderOptions) {
/**
* Should the loader use a stale cache. true when building.
*/
readonly useStale?: boolean;

constructor({path, sourceRoot, targetPath, useStale}: LoaderOptions) {
this.path = path;
this.sourceRoot = sourceRoot;
this.targetPath = targetPath;
this.useStale = useStale;
}

/**
Expand All @@ -70,8 +77,8 @@ export abstract class Loader {
* abort if we find a matching folder or reach the source root; for example,
* if docs/data exists, we won’t look for a docs/data.zip.
*/
static find(sourceRoot: string, targetPath: string): Loader | undefined {
const exact = this.findExact(sourceRoot, targetPath);
static find(sourceRoot: string, targetPath: string, {useStale = false} = {}): Loader | undefined {
const exact = this.findExact(sourceRoot, targetPath, {useStale});
if (exact) return exact;
let dir = dirname(targetPath);
for (let parent: string; true; dir = parent) {
Expand All @@ -88,23 +95,25 @@ export abstract class Loader {
inflatePath: targetPath.slice(archive.length - ext.length + 1),
path: join(sourceRoot, archive),
sourceRoot,
targetPath
targetPath,
useStale
});
}
const archiveLoader = this.findExact(sourceRoot, archive);
const archiveLoader = this.findExact(sourceRoot, archive, {useStale});
if (archiveLoader) {
return new Extractor({
preload: async (options) => archiveLoader.load(options),
inflatePath: targetPath.slice(archive.length - ext.length + 1),
path: archiveLoader.path,
sourceRoot,
targetPath
targetPath,
useStale
});
}
}
}

private static findExact(sourceRoot: string, targetPath: string): Loader | undefined {
private static findExact(sourceRoot: string, targetPath: string, {useStale}): Loader | undefined {
for (const [ext, [command, ...args]] of Object.entries(languages)) {
if (!existsSync(join(sourceRoot, targetPath + ext))) continue;
if (extname(targetPath) === "") {
Expand All @@ -117,7 +126,8 @@ export abstract class Loader {
args: command == null ? args : [...args, path],
path,
sourceRoot,
targetPath
targetPath,
useStale
});
}
}
Expand All @@ -127,6 +137,7 @@ export abstract class Loader {
* to the source root; this is within the .observablehq/cache folder within
* the source root.
*/

async load(effects = defaultEffects): Promise<string> {
const key = join(this.sourceRoot, this.targetPath);
let command = runningCommands.get(key);
Expand All @@ -137,8 +148,10 @@ export abstract class Loader {
const loaderStat = await maybeStat(this.path);
const cacheStat = await maybeStat(cachePath);
if (!cacheStat) effects.output.write(faint("[missing] "));
else if (cacheStat.mtimeMs < loaderStat!.mtimeMs) effects.output.write(faint("[stale] "));
else return effects.output.write(faint("[fresh] ")), outputPath;
else if (cacheStat.mtimeMs < loaderStat!.mtimeMs) {
if (this.useStale) return effects.output.write(faint("[using stale] ")), outputPath;
else effects.output.write(faint("[stale] "));
} else return effects.output.write(faint("[fresh] ")), outputPath;
const tempPath = join(this.sourceRoot, ".observablehq", "cache", `${this.targetPath}.${process.pid}`);
await prepareOutput(tempPath);
const tempFd = await open(tempPath, "w");
Expand Down
55 changes: 54 additions & 1 deletion test/dataloaders-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "node:assert";
import {readFile} from "node:fs/promises";
import {readFile, stat, unlink, utimes} from "node:fs/promises";
import {type LoadEffects, Loader} from "../src/dataloader.js";

const noopEffects: LoadEffects = {
Expand Down Expand Up @@ -40,3 +40,56 @@ describe("data loaders are called with the appropriate command", () => {
assert.strictEqual(await readFile("test/" + out, "utf-8"), "Rscript\n");
});
});

describe("data loaders optionally use a stale cache", () => {
it("a dataloader can use ", async () => {
const out = [] as string[];
const outputEffects: LoadEffects = {
logger: {log() {}, warn() {}, error() {}},
output: {
write(a) {
out.push(a);
}
}
};
const loader = Loader.find("test", "dataloaders/data1.txt")!;
// save the loader times.
const {atime, mtime} = await stat(loader.path);
// set the loader mtime to Dec. 1st, 2023.
const time = Date.UTC(2023, 11, 1) / 1000;
await utimes(loader.path, atime, time);
// 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(outputEffects);
// run again (fresh)
await loader.load(outputEffects);
// touch the loader
await utimes(loader.path, atime, Date.now() + 100);
// run it with useStale=true (using stale)
const loader2 = Loader.find("test", "dataloaders/data1.txt", {useStale: true})!;
await loader2.load(outputEffects);
// run it with useStale=false (stale)
await loader.load(outputEffects);
// revert the loader to its original mtime
await utimes(loader.path, atime, mtime);
assert.deepStrictEqual(
// eslint-disable-next-line no-control-regex
out.map((l) => l.replaceAll(/\x1b\[[0-9]+m/g, "")),
[
"load test/dataloaders/data1.txt.js → ",
"[missing] ",
"load test/dataloaders/data1.txt.js → ",
"[fresh] ",
"load test/dataloaders/data1.txt.js → ",
"[using stale] ",
"load test/dataloaders/data1.txt.js → ",
"[stale] "
]
);
});
});