Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/cache/cache.ts
3583 views
1
/*
2
* cache.ts
3
*
4
* A persistent cache for projects
5
*
6
* Copyright (C) 2025 Posit Software, PBC
7
*/
8
9
import { assert } from "testing/asserts";
10
import { ensureDirSync } from "../../deno_ral/fs.ts";
11
import { join } from "../../deno_ral/path.ts";
12
import { md5HashBytes } from "../hash.ts";
13
import { satisfies } from "semver/mod.ts";
14
import { quartoConfig } from "../quarto.ts";
15
import { CacheIndexEntry, ProjectCache } from "./cache-types.ts";
16
import { ProjectContext } from "../../project/types.ts";
17
import {
18
type DiskCacheEntry,
19
type ImmediateBufferCacheEntry,
20
type ImmediateStringCacheEntry,
21
} from "./cache-types.ts";
22
import { Cloneable } from "../safe-clone-deep.ts";
23
export { type ProjectCache } from "./cache-types.ts";
24
25
const currentCacheVersion = "1";
26
const requiredQuartoVersions: Record<string, string> = {
27
"1": ">1.7.0",
28
};
29
30
class ProjectCacheImpl implements Cloneable<ProjectCacheImpl> {
31
projectScratchDir: string;
32
index: Deno.Kv | null;
33
34
constructor(_projectScratchDir: string) {
35
this.projectScratchDir = _projectScratchDir;
36
this.index = null;
37
}
38
39
clone() {
40
return this;
41
}
42
43
close() {
44
if (this.index) {
45
this.index.close();
46
this.index = null;
47
}
48
}
49
50
async clear(): Promise<void> {
51
assert(this.index);
52
const entries = this.index.list({ prefix: [] });
53
for await (const entry of entries) {
54
const diskValue = entry.value;
55
if (entry.key[0] !== "version") {
56
Deno.removeSync(
57
join(this.projectScratchDir, "project-cache", diskValue as string),
58
);
59
}
60
await this.index.delete(entry.key);
61
}
62
}
63
64
async createOnDisk(): Promise<void> {
65
assert(this.index);
66
this.index.set(["version"], currentCacheVersion);
67
}
68
69
async init(): Promise<void> {
70
const indexPath = join(this.projectScratchDir, "project-cache");
71
ensureDirSync(indexPath);
72
this.index = await Deno.openKv(join(indexPath, "deno-kv-file"));
73
let version = await this.index.get(["version"]);
74
if (version.value === null) {
75
await this.createOnDisk();
76
version = await this.index.get(["version"]);
77
}
78
assert(typeof version.value === "string");
79
const requiredVersion = requiredQuartoVersions[version.value];
80
if (
81
!requiredVersion || !satisfies(quartoConfig.version(), requiredVersion)
82
) {
83
console.warn("Unknown project cache version.");
84
console.warn(
85
"Project cache was likely created by a newer version of Quarto.",
86
);
87
console.warn("Quarto will clear the project cache.");
88
await this.clear();
89
await this.createOnDisk();
90
}
91
}
92
93
async addBuffer(key: string[], value: Uint8Array): Promise<void> {
94
assert(this.index);
95
const hash = await md5HashBytes(value);
96
Deno.writeFileSync(
97
join(this.projectScratchDir, "project-cache", hash),
98
value,
99
);
100
const result = await this.index.set(key, {
101
hash,
102
type: "buffer",
103
});
104
assert(result.ok);
105
}
106
107
async addString(key: string[], value: string): Promise<void> {
108
assert(this.index);
109
const buffer = new TextEncoder().encode(value);
110
const hash = await md5HashBytes(buffer);
111
Deno.writeTextFileSync(
112
join(this.projectScratchDir, "project-cache", hash),
113
value,
114
);
115
const result = await this.index.set(key, {
116
hash,
117
type: "string",
118
});
119
assert(result.ok);
120
}
121
122
async addSmallBuffer(key: string[], value: Uint8Array): Promise<void> {
123
assert(this.index);
124
const result = await this.index.set(key, {
125
value,
126
type: "buffer-immediate",
127
});
128
assert(result.ok);
129
}
130
131
async addSmallString(key: string[], value: string): Promise<void> {
132
assert(this.index);
133
const result = await this.index.set(key, {
134
value,
135
type: "string-immediate",
136
});
137
assert(result.ok);
138
}
139
140
async get(key: string[]): Promise<CacheIndexEntry | null> {
141
assert(this.index);
142
const kvResult = await this.index.get(key);
143
if (kvResult.value === null) {
144
return null;
145
}
146
return kvResult.value as CacheIndexEntry;
147
}
148
149
async _getAs<T>(
150
key: string[],
151
getter: (hash: CacheIndexEntry) => T,
152
): Promise<T | null> {
153
assert(this.index);
154
const result = await this.get(key);
155
if (result === null) {
156
return null;
157
}
158
try {
159
return getter(result);
160
} catch (e) {
161
if (!(e instanceof Deno.errors.NotFound)) {
162
throw e;
163
}
164
console.warn(
165
`Entry ${result} not found -- clearing entry in index`,
166
);
167
await this.index.delete(key);
168
return null;
169
}
170
}
171
172
async getSmallString(key: string[]): Promise<string | null> {
173
return this._getAs(key, (result) => {
174
assert(result.type === "string-immediate", "Expected string");
175
return (result as ImmediateStringCacheEntry).value;
176
});
177
}
178
179
async getSmallBuffer(key: string[]): Promise<Uint8Array | null> {
180
return this._getAs(key, (result) => {
181
assert(result.type === "buffer-immediate", "Expected buffer");
182
return (result as ImmediateBufferCacheEntry).value;
183
});
184
}
185
186
async getBuffer(key: string[]): Promise<Uint8Array | null> {
187
return this._getAs(key, (result) => {
188
assert(result.type === "buffer", "Expected buffer");
189
const { hash } = result as DiskCacheEntry;
190
return Deno.readFileSync(
191
join(this.projectScratchDir, "project-cache", hash),
192
);
193
});
194
}
195
196
async getString(key: string[]): Promise<string | null> {
197
return this._getAs(key, (result) => {
198
assert(result.type === "string", "Expected string");
199
const { hash } = result as DiskCacheEntry;
200
return Deno.readTextFileSync(
201
join(this.projectScratchDir, "project-cache", hash),
202
);
203
});
204
}
205
206
memoizeStringFunction(
207
key: string[],
208
fn: (param: string) => Promise<string>,
209
): (param: string) => Promise<string> {
210
return async (param: string) => {
211
const paramHash = await md5HashBytes(new TextEncoder().encode(param));
212
const memoizationKey = [...key, paramHash];
213
const cached = await this.getString(memoizationKey);
214
if (cached !== null) {
215
return cached;
216
}
217
const result = await fn(param);
218
await this.addString(memoizationKey, result);
219
return result;
220
};
221
}
222
}
223
224
export const createProjectCache = async (
225
projectScratchDir: string,
226
): Promise<ProjectCache> => {
227
const result = new ProjectCacheImpl(projectScratchDir);
228
await result.init();
229
return result as ProjectCache;
230
};
231
232
export const memoizeStringFunction = (
233
key: string[],
234
fn: (param: string) => Promise<string>,
235
): (project: ProjectContext, param: string) => Promise<string> => {
236
return async (project: ProjectContext, param: string) => {
237
return (project.diskCache as ProjectCacheImpl).memoizeStringFunction(
238
key,
239
fn,
240
)(param);
241
};
242
};
243
244