Skip to content

Commit 21772b1

Browse files
authored
build uses a stale cache (#296)
* build uses a stale cache * don't crash on it.only * options object
1 parent 656bf71 commit 21772b1

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

src/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export async function build(
8888
let sourcePath = join(root, file);
8989
const outputPath = join("_file", file);
9090
if (!existsSync(sourcePath)) {
91-
const loader = Loader.find(root, file);
91+
const loader = Loader.find(root, file, {useStale: true});
9292
if (!loader) {
9393
effects.logger.error("missing referenced file", sourcePath);
9494
continue;

src/dataloader.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface LoaderOptions {
3535
path: string;
3636
sourceRoot: string;
3737
targetPath: string;
38+
useStale: boolean;
3839
}
3940

4041
export abstract class Loader {
@@ -57,10 +58,16 @@ export abstract class Loader {
5758
*/
5859
readonly targetPath: string;
5960

60-
constructor({path, sourceRoot, targetPath}: LoaderOptions) {
61+
/**
62+
* Should the loader use a stale cache. true when building.
63+
*/
64+
readonly useStale?: boolean;
65+
66+
constructor({path, sourceRoot, targetPath, useStale}: LoaderOptions) {
6167
this.path = path;
6268
this.sourceRoot = sourceRoot;
6369
this.targetPath = targetPath;
70+
this.useStale = useStale;
6471
}
6572

6673
/**
@@ -70,8 +77,8 @@ export abstract class Loader {
7077
* abort if we find a matching folder or reach the source root; for example,
7178
* if docs/data exists, we won’t look for a docs/data.zip.
7279
*/
73-
static find(sourceRoot: string, targetPath: string): Loader | undefined {
74-
const exact = this.findExact(sourceRoot, targetPath);
80+
static find(sourceRoot: string, targetPath: string, {useStale = false} = {}): Loader | undefined {
81+
const exact = this.findExact(sourceRoot, targetPath, {useStale});
7582
if (exact) return exact;
7683
let dir = dirname(targetPath);
7784
for (let parent: string; true; dir = parent) {
@@ -88,23 +95,25 @@ export abstract class Loader {
8895
inflatePath: targetPath.slice(archive.length - ext.length + 1),
8996
path: join(sourceRoot, archive),
9097
sourceRoot,
91-
targetPath
98+
targetPath,
99+
useStale
92100
});
93101
}
94-
const archiveLoader = this.findExact(sourceRoot, archive);
102+
const archiveLoader = this.findExact(sourceRoot, archive, {useStale});
95103
if (archiveLoader) {
96104
return new Extractor({
97105
preload: async (options) => archiveLoader.load(options),
98106
inflatePath: targetPath.slice(archive.length - ext.length + 1),
99107
path: archiveLoader.path,
100108
sourceRoot,
101-
targetPath
109+
targetPath,
110+
useStale
102111
});
103112
}
104113
}
105114
}
106115

107-
private static findExact(sourceRoot: string, targetPath: string): Loader | undefined {
116+
private static findExact(sourceRoot: string, targetPath: string, {useStale}): Loader | undefined {
108117
for (const [ext, [command, ...args]] of Object.entries(languages)) {
109118
if (!existsSync(join(sourceRoot, targetPath + ext))) continue;
110119
if (extname(targetPath) === "") {
@@ -117,7 +126,8 @@ export abstract class Loader {
117126
args: command == null ? args : [...args, path],
118127
path,
119128
sourceRoot,
120-
targetPath
129+
targetPath,
130+
useStale
121131
});
122132
}
123133
}
@@ -127,6 +137,7 @@ export abstract class Loader {
127137
* to the source root; this is within the .observablehq/cache folder within
128138
* the source root.
129139
*/
140+
130141
async load(effects = defaultEffects): Promise<string> {
131142
const key = join(this.sourceRoot, this.targetPath);
132143
let command = runningCommands.get(key);
@@ -137,8 +148,10 @@ export abstract class Loader {
137148
const loaderStat = await maybeStat(this.path);
138149
const cacheStat = await maybeStat(cachePath);
139150
if (!cacheStat) effects.output.write(faint("[missing] "));
140-
else if (cacheStat.mtimeMs < loaderStat!.mtimeMs) effects.output.write(faint("[stale] "));
141-
else return effects.output.write(faint("[fresh] ")), outputPath;
151+
else if (cacheStat.mtimeMs < loaderStat!.mtimeMs) {
152+
if (this.useStale) return effects.output.write(faint("[using stale] ")), outputPath;
153+
else effects.output.write(faint("[stale] "));
154+
} else return effects.output.write(faint("[fresh] ")), outputPath;
142155
const tempPath = join(this.sourceRoot, ".observablehq", "cache", `${this.targetPath}.${process.pid}`);
143156
await prepareOutput(tempPath);
144157
const tempFd = await open(tempPath, "w");

test/dataloaders-test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "node:assert";
2-
import {readFile} from "node:fs/promises";
2+
import {readFile, stat, unlink, utimes} from "node:fs/promises";
33
import {type LoadEffects, Loader} from "../src/dataloader.js";
44

55
const noopEffects: LoadEffects = {
@@ -40,3 +40,56 @@ describe("data loaders are called with the appropriate command", () => {
4040
assert.strictEqual(await readFile("test/" + out, "utf-8"), "Rscript\n");
4141
});
4242
});
43+
44+
describe("data loaders optionally use a stale cache", () => {
45+
it("a dataloader can use ", async () => {
46+
const out = [] as string[];
47+
const outputEffects: LoadEffects = {
48+
logger: {log() {}, warn() {}, error() {}},
49+
output: {
50+
write(a) {
51+
out.push(a);
52+
}
53+
}
54+
};
55+
const loader = Loader.find("test", "dataloaders/data1.txt")!;
56+
// save the loader times.
57+
const {atime, mtime} = await stat(loader.path);
58+
// set the loader mtime to Dec. 1st, 2023.
59+
const time = Date.UTC(2023, 11, 1) / 1000;
60+
await utimes(loader.path, atime, time);
61+
// remove the cache set by another test (unless we it.only this test).
62+
try {
63+
await unlink("test/.observablehq/cache/dataloaders/data1.txt");
64+
} catch {
65+
// ignore;
66+
}
67+
// populate the cache (missing)
68+
await loader.load(outputEffects);
69+
// run again (fresh)
70+
await loader.load(outputEffects);
71+
// touch the loader
72+
await utimes(loader.path, atime, Date.now() + 100);
73+
// run it with useStale=true (using stale)
74+
const loader2 = Loader.find("test", "dataloaders/data1.txt", {useStale: true})!;
75+
await loader2.load(outputEffects);
76+
// run it with useStale=false (stale)
77+
await loader.load(outputEffects);
78+
// revert the loader to its original mtime
79+
await utimes(loader.path, atime, mtime);
80+
assert.deepStrictEqual(
81+
// eslint-disable-next-line no-control-regex
82+
out.map((l) => l.replaceAll(/\x1b\[[0-9]+m/g, "")),
83+
[
84+
"load test/dataloaders/data1.txt.js → ",
85+
"[missing] ",
86+
"load test/dataloaders/data1.txt.js → ",
87+
"[fresh] ",
88+
"load test/dataloaders/data1.txt.js → ",
89+
"[using stale] ",
90+
"load test/dataloaders/data1.txt.js → ",
91+
"[stale] "
92+
]
93+
);
94+
});
95+
});

0 commit comments

Comments
 (0)