Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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, 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: boolean): 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", 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] "
]
);
});
});