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
1 change: 1 addition & 0 deletions .claude/rules/typescript/deno-ral.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The `deno_ral/` directory provides a runtime abstraction layer over Deno's stand
| `fs.ts` | File system | `existsSync`, `ensureDirSync`, `copySync`, `safeMoveSync`, `safeRemoveSync` |
| `path.ts` | Path utilities | `join`, `dirname`, `basename`, `extname` |
| `log.ts` | Logging | `debug`, `info`, `warning`, `error` |
| `kv.ts` | Deno KV cache stores | `openKv`, `getKv`, `kvOp`, `KvDamagedError` |
| `platform.ts` | Platform detection | `isWindows` |
| `process.ts` | Process execution | Process utilities |

Expand Down
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ All changes included in 1.11:
- ([#14878](https://github.com/quarto-dev/quarto-cli/pull/14878)): Add `az` (Azerbaijani) language translation. (author: @abdanar)
- ([#14983](https://github.com/quarto-dev/quarto-cli/issues/14983)): Fix `quarto render` treating a parent folder as a Hugo project when its `config/` folder can't be entered, which silently wrote `hugo-md` output or failed with a permission error. (author: @pinin4fjords)
- ([#15097](https://github.com/quarto-dev/quarto-cli/pull/15097)): Fix the `.content-hidden unless-profile` attribute completion inserting invalid syntax (its `=` was missing), and typos in three attribute completion descriptions. (author: @rkskmt)
- ([#15119](https://github.com/quarto-dev/quarto-cli/issues/15119)): Fix renders failing with `RangeError: could not deserialize value` when a Quarto cache file is damaged. A damaged entry is now recomputed, and a damaged cache file is reported with the folder to delete.
- ([#15126](https://github.com/quarto-dev/quarto-cli/issues/15126)): Translate the `revealjs-menu-button-label` language key in French. (author: @D3ller)
- ([#15140](https://github.com/quarto-dev/quarto-cli/pull/15140)): Add support for definition lists written as a `definition-list` div holding a nested bullet list, the definition list syntax of Quarto 2.
- ([#15180](https://github.com/quarto-dev/quarto-cli/pull/15180)): Fix older Quarto versions failing to render with `RangeError: could not deserialize value` once a Quarto built on a newer Deno has used the same cache. Also fix a project cache that already has entries failing to clear with `Path must be a string`.
130 changes: 76 additions & 54 deletions src/core/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type ImmediateStringCacheEntry,
} from "./cache-types.ts";
import { Cloneable } from "../safe-clone-deep.ts";
import { getKv, kvOp, openKv } from "../../deno_ral/kv.ts";
export { type ProjectCache } from "./cache-types.ts";

const currentCacheVersion = "1";
Expand All @@ -31,6 +32,7 @@ const requiredQuartoVersions: Record<string, string> = {
class ProjectCacheImpl implements Cloneable<ProjectCacheImpl> {
projectScratchDir: string;
storeDir: string;
indexFile: string;
index: Deno.Kv | null;

constructor(_projectScratchDir: string) {
Expand All @@ -40,6 +42,7 @@ class ProjectCacheImpl implements Cloneable<ProjectCacheImpl> {
"project-cache",
`v8-${v8Branch}`,
);
this.indexFile = join(this.storeDir, "deno-kv-file");
this.index = null;
}

Expand All @@ -56,58 +59,74 @@ class ProjectCacheImpl implements Cloneable<ProjectCacheImpl> {

async clear(): Promise<void> {
assert(this.index);
const entries = this.index.list({ prefix: [] });
for await (const entry of entries) {
const value = entry.value as CacheIndexEntry;
// immediate entries live in the index only; disk entries with
// identical content share one hash file
if (
entry.key[0] !== "version" &&
(value.type === "buffer" || value.type === "string")
) {
const { hash } = value as DiskCacheEntry;
safeRemoveSync(join(this.storeDir, hash));
const index = this.index;
await kvOp(this.indexFile, async () => {
const entries = index.list({ prefix: [] });
for await (const entry of entries) {
const value = entry.value as CacheIndexEntry;
// immediate entries live in the index only; disk entries with
// identical content share one hash file
if (
entry.key[0] !== "version" &&
(value.type === "buffer" || value.type === "string")
) {
const { hash } = value as DiskCacheEntry;
safeRemoveSync(join(this.storeDir, hash));
}
await index.delete(entry.key);
}
await this.index.delete(entry.key);
}
});
}

async createOnDisk(): Promise<void> {
assert(this.index);
this.index.set(["version"], currentCacheVersion);
const index = this.index;
await kvOp(
this.indexFile,
() => index.set(["version"], currentCacheVersion),
);
}

async init(): Promise<void> {
ensureDirSync(this.storeDir);
this.index = await Deno.openKv(join(this.storeDir, "deno-kv-file"));
let version = await this.index.get(["version"]);
if (version.value === null) {
await this.createOnDisk();
version = await this.index.get(["version"]);
}
assert(typeof version.value === "string");
const requiredVersion = requiredQuartoVersions[version.value];
if (
!requiredVersion || !satisfies(quartoConfig.version(), requiredVersion)
) {
console.warn("Unknown project cache version.");
console.warn(
"Project cache was likely created by a newer version of Quarto.",
);
console.warn("Quarto will clear the project cache.");
await this.clear();
await this.createOnDisk();
this.index = await openKv(this.indexFile);
try {
let version = await getKv<string>(this.index, this.indexFile, [
"version",
]);
if (version === null) {
await this.createOnDisk();
version = await getKv<string>(this.index, this.indexFile, ["version"]);
}
assert(typeof version === "string");
const requiredVersion = requiredQuartoVersions[version];
if (
!requiredVersion || !satisfies(quartoConfig.version(), requiredVersion)
) {
console.warn("Unknown project cache version.");
console.warn(
"Project cache was likely created by a newer version of Quarto.",
);
console.warn("Quarto will clear the project cache.");
await this.clear();
await this.createOnDisk();
}
} catch (e) {
this.close();
throw e;
}
}

async addBuffer(key: string[], value: Uint8Array): Promise<void> {
assert(this.index);
const hash = await md5HashBytes(value);
Deno.writeFileSync(join(this.storeDir, hash), value);
const result = await this.index.set(key, {
hash,
type: "buffer",
});
const index = this.index;
const result = await kvOp(this.indexFile, () =>
index.set(key, {
hash,
type: "buffer",
}));
assert(result.ok);
}

Expand All @@ -116,38 +135,40 @@ class ProjectCacheImpl implements Cloneable<ProjectCacheImpl> {
const buffer = new TextEncoder().encode(value);
const hash = await md5HashBytes(buffer);
Deno.writeTextFileSync(join(this.storeDir, hash), value);
const result = await this.index.set(key, {
hash,
type: "string",
});
const index = this.index;
const result = await kvOp(this.indexFile, () =>
index.set(key, {
hash,
type: "string",
}));
assert(result.ok);
}

async addSmallBuffer(key: string[], value: Uint8Array): Promise<void> {
assert(this.index);
const result = await this.index.set(key, {
value,
type: "buffer-immediate",
});
const index = this.index;
const result = await kvOp(this.indexFile, () =>
index.set(key, {
value,
type: "buffer-immediate",
}));
assert(result.ok);
}

async addSmallString(key: string[], value: string): Promise<void> {
assert(this.index);
const result = await this.index.set(key, {
value,
type: "string-immediate",
});
const index = this.index;
const result = await kvOp(this.indexFile, () =>
index.set(key, {
value,
type: "string-immediate",
}));
assert(result.ok);
}

async get(key: string[]): Promise<CacheIndexEntry | null> {
assert(this.index);
const kvResult = await this.index.get(key);
if (kvResult.value === null) {
return null;
}
return kvResult.value as CacheIndexEntry;
return await getKv<CacheIndexEntry>(this.index, this.indexFile, key);
}

async _getAs<T>(
Expand All @@ -168,7 +189,8 @@ class ProjectCacheImpl implements Cloneable<ProjectCacheImpl> {
console.warn(
`Entry ${result} not found -- clearing entry in index`,
);
await this.index.delete(key);
const index = this.index;
await kvOp(this.indexFile, () => index.delete(key));
return null;
}
}
Expand Down
54 changes: 30 additions & 24 deletions src/core/sass/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ 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";
import { getKv, kvOp, openKv } from "../../deno_ral/kv.ts";

class SassCache implements Cloneable<SassCache> {
kv: Deno.Kv;
path: string;
kvFile: string;

clone() {
return this;
}

constructor(kv: Deno.Kv, path: string) {
constructor(kv: Deno.Kv, path: string, kvFile: string) {
this.kv = kv;
this.path = path;
this.kvFile = kvFile;
}

async getFromHash(
Expand All @@ -43,22 +46,20 @@ class SassCache implements Cloneable<SassCache> {
throw new InternalError(`Invalid hash length: ${hash.length}`);
}

const result = await this.kv.get(["entry", hash]);
if (result.value === null) {
const value = await getKv<unknown>(this.kv, this.kvFile, ["entry", hash]);
if (value === null) {
log.debug(` cache miss`);
return null;
}
if (typeof result.value !== "object") {
if (typeof value !== "object") {
throw new InternalError(
`Unsupported SassCache entry type\nExpected SassCacheEntry, got ${typeof result
.value}`,
`Unsupported SassCache entry type\nExpected SassCacheEntry, got ${typeof value}`,
);
}
const v = result.value as Record<string, unknown>;
const v = 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}`,
`Unsupported SassCache entry type\nExpected SassCacheEntry, got ${typeof value}`,
);
}
const outputFilePath = join(this.path, `${hash}.css`);
Expand Down Expand Up @@ -99,10 +100,11 @@ class SassCache implements Cloneable<SassCache> {
}
throw error;
}
await this.kv.set(["entry", identifierHash], {
key: cacheIdentifier,
hash: inputHash,
});
await kvOp(this.kvFile, () =>
this.kv.set(["entry", identifierHash], {
key: cacheIdentifier,
hash: inputHash,
}));
return outputFilePath;
}

Expand Down Expand Up @@ -174,24 +176,23 @@ const requiredQuartoVersions: Record<number, string> = {
};

async function checkVersion(kv: Deno.Kv, path: string) {
const version = await kv.get(["version"]);
if (version.value === null) {
await kv.set(["version"], 1);
const version = await getKv<unknown>(kv, path, ["version"]);
if (version === null) {
await kvOp(path, () => kv.set(["version"], 1));
} else {
if (typeof version.value !== "number") {
if (typeof version !== "number") {
throw new Error(
`Unsupported SassCache version type in ${path}\nExpected number, got ${typeof version
.value}`,
`Unsupported SassCache version type in ${path}\nExpected number, got ${typeof version}`,
);
}
if (version.value < currentSassCacheVersion) {
if (version < 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) {
if (version > currentSassCacheVersion) {
throw new Error(
`Found a SassCache version that's newer than supported. Please clear ${path} or upgrade Quarto to ${
requiredQuartoVersions[currentSassCacheVersion]
Expand All @@ -212,9 +213,14 @@ export async function sassCache(
log.debug(`Creating SassCache at ${storeDir}`);
ensureDirSync(storeDir);
const kvFile = join(storeDir, "sass.kv");
const kv = await Deno.openKv(kvFile);
await checkVersion(kv, kvFile);
_sassCache[path] = new SassCache(kv, storeDir);
const kv = await openKv(kvFile);
try {
await checkVersion(kv, kvFile);
} catch (e) {
kv.close();
throw e;
}
_sassCache[path] = new SassCache(kv, storeDir, kvFile);
// register cleanup for this cache
_sassCache[path].cleanup(temp, path);
}
Expand Down
Loading
Loading