-
Notifications
You must be signed in to change notification settings - Fork 432
Expand file tree
/
Copy pathcache.ts
More file actions
214 lines (197 loc) · 6.1 KB
/
cache.ts
File metadata and controls
214 lines (197 loc) · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/*
* cache.ts
*
* A persistent cache for sass compilation based partly on Deno.KV.
*
* Copyright (C) 2024 Posit Software, PBC
*/
import { InternalError } from "../lib/error.ts";
import { md5HashAsync } from "../hash.ts";
import { join } from "../../deno_ral/path.ts";
import { ensureDirSync, existsSync } from "../../deno_ral/fs.ts";
import { TempContext } from "../temp.ts";
import { safeRemoveIfExists } from "../path.ts";
import * as log from "../../deno_ral/log.ts";
import { onCleanup } from "../cleanup.ts";
import { Cloneable } from "../safe-clone-deep.ts";
class SassCache implements Cloneable<SassCache> {
kv: Deno.Kv;
path: string;
clone() {
return this;
}
constructor(kv: Deno.Kv, path: string) {
this.kv = kv;
this.path = path;
}
async getFromHash(
hash: string,
inputHash: string,
force?: boolean,
): Promise<string | null> {
log.debug(
`SassCache.getFromHash(hash=${hash}, inputHash=${inputHash}, force=${force})`,
);
// verify that the hash is a valid md5 hash
if (hash.length !== 32 || !/^[0-9a-f]{32}$/.test(hash)) {
throw new InternalError(`Invalid hash length: ${hash.length}`);
}
const result = await this.kv.get(["entry", hash]);
if (result.value === null) {
log.debug(` cache miss`);
return null;
}
if (typeof result.value !== "object") {
throw new InternalError(
`Unsupported SassCache entry type\nExpected SassCacheEntry, got ${typeof result
.value}`,
);
}
const v = result.value as Record<string, unknown>;
if (typeof v.key !== "string" || typeof v.hash !== "string") {
throw new InternalError(
`Unsupported SassCache entry type\nExpected SassCacheEntry, got ${typeof result
.value}`,
);
}
const outputFilePath = join(this.path, `${hash}.css`);
// if the hash doesn't match the key, return null
if ((v.hash !== inputHash && !force) || !existsSync(outputFilePath)) {
if (v.hash !== inputHash) {
log.debug(` hash mismatch: ${v.hash} !== ${inputHash}`);
} else if (force) {
log.debug(` forcing recomputation`);
} else {
log.debug(` output file missing: ${outputFilePath}`);
}
return null;
}
log.debug(` cache hit`);
return outputFilePath;
}
async setFromHash(
identifierHash: string,
inputHash: string,
cacheIdentifier: string,
compilationThunk: (outputFilePath: string) => Promise<void>,
): Promise<string> {
log.debug(`SassCache.setFromHash(${identifierHash}, ${inputHash}), ...`);
const outputFilePath = join(this.path, `${identifierHash}.css`);
try {
await compilationThunk(outputFilePath);
} catch (error) {
// Compilation failed, so clear out the output file (if exists)
// which will be invalid CSS
try {
safeRemoveIfExists(outputFilePath);
} finally {
// doesn't matter
}
throw error;
}
await this.kv.set(["entry", identifierHash], {
key: cacheIdentifier,
hash: inputHash,
});
return outputFilePath;
}
async set(
input: string,
cacheIdentifier: string,
compilationThunk: (outputFilePath: string) => Promise<void>,
): Promise<string> {
const identifierHash = await md5HashAsync(cacheIdentifier);
const inputHash = await md5HashAsync(input);
return this.setFromHash(
identifierHash,
inputHash,
cacheIdentifier,
compilationThunk,
);
}
async getOrSet(
input: string,
cacheIdentifier: string,
compilationThunk: (outputFilePath: string) => Promise<void>,
): Promise<string> {
log.debug(`SassCache.getOrSet(...)`);
const identifierHash = await md5HashAsync(cacheIdentifier);
const inputHash = await md5HashAsync(input);
const existing = await this.getFromHash(identifierHash, inputHash);
if (existing !== null) {
log.debug(` cache hit`);
return existing;
}
log.debug(` cache miss, setting`);
return this.setFromHash(
identifierHash,
inputHash,
cacheIdentifier,
compilationThunk,
);
}
// add a cleanup method to register a cleanup handler
cleanup(temp: TempContext | undefined) {
const registerCleanup = temp ? temp.onCleanup : onCleanup;
registerCleanup(() => {
try {
this.kv.close();
if (temp) safeRemoveIfExists(this.path);
} catch (error) {
log.info(
`Error occurred during sass cache cleanup for ${this.path}: ${error}`,
);
}
});
}
}
const currentSassCacheVersion = 1;
const requiredQuartoVersions: Record<number, string> = {
1: "1.6.0",
};
async function checkVersion(kv: Deno.Kv, path: string) {
const version = await kv.get(["version"]);
if (version.value === null) {
await kv.set(["version"], 1);
} else {
if (typeof version.value !== "number") {
throw new Error(
`Unsupported SassCache version type in ${path}\nExpected number, got ${typeof version
.value}`,
);
}
if (version.value < currentSassCacheVersion) {
// in the future we should clean this automatically, but this is v1 and there should be
// no old data anywhere.
throw new Error(
`Found outdated SassCache version. Please clear ${path}.`,
);
}
if (version.value > currentSassCacheVersion) {
throw new Error(
`Found a SassCache version that's newer than supported. Please clear ${path} or upgrade Quarto to ${
requiredQuartoVersions[currentSassCacheVersion]
} or later.`,
);
}
}
}
const _sassCache: Record<string, SassCache> = {};
export async function sassCache(
path: string,
temp: TempContext | undefined,
): Promise<SassCache> {
if (!_sassCache[path]) {
log.debug(`Creating SassCache at ${path}`);
ensureDirSync(path);
const kvFile = join(path, "sass.kv");
const kv = await Deno.openKv(kvFile);
await checkVersion(kv, kvFile);
_sassCache[path] = new SassCache(kv, path);
// register cleanup for this cache
_sassCache[path].cleanup(temp);
}
log.debug(`Returning SassCache at ${path}`);
const result = _sassCache[path];
return result;
}