mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 11:42:52 +00:00
Tiny patches previously parsed both full source versions synchronously. Yield between parser work units and admit source by UTF-8 bytes, keeping large patches renderable without tokens. Cancel obsolete highlighting when a newer diff supersedes it.
99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
import { LRUCache } from "lru-cache";
|
|
import type { CheckoutDiffCompare, CheckoutDiffResult } from "../utils/checkout-git.js";
|
|
|
|
interface ActiveRead {
|
|
promise: Promise<CheckoutDiffResult>;
|
|
invalidated: boolean;
|
|
controller: AbortController;
|
|
}
|
|
|
|
interface CachedDiff {
|
|
value: CheckoutDiffResult;
|
|
loadedAt: number;
|
|
lastReadStarted: number;
|
|
}
|
|
|
|
interface CheckoutDiffReadInput {
|
|
cwd: string;
|
|
compare: CheckoutDiffCompare;
|
|
force?: boolean;
|
|
reason?: string;
|
|
load: (signal: AbortSignal) => Promise<CheckoutDiffResult>;
|
|
}
|
|
|
|
/** Completed payloads are bounded; active work survives invalidation and eviction. */
|
|
export class CheckoutDiffCache {
|
|
private readonly values = new LRUCache<string, CachedDiff>({
|
|
max: 64,
|
|
});
|
|
private readonly active = new Map<string, ActiveRead>();
|
|
|
|
constructor(private readonly now: () => number) {}
|
|
|
|
read(input: CheckoutDiffReadInput): Promise<CheckoutDiffResult> {
|
|
const { cwd, compare, force, reason, load } = input;
|
|
if (force && !reason) throw new Error("WorkspaceGitService forced read requires a reason");
|
|
const key = JSON.stringify([
|
|
cwd,
|
|
compare.mode,
|
|
compare.mode !== "uncommitted" ? (compare.baseRef ?? null) : null,
|
|
compare.ignoreWhitespace === true,
|
|
compare.includeStructured === true,
|
|
]);
|
|
const cached = this.values.get(key);
|
|
const now = this.now();
|
|
if (
|
|
!force &&
|
|
cached &&
|
|
(now - cached.loadedAt <= 15_000 || now - cached.lastReadStarted < 2_000)
|
|
)
|
|
return Promise.resolve(cached.value);
|
|
const pending = this.active.get(key);
|
|
if (pending) {
|
|
if (force) {
|
|
pending.invalidated = true;
|
|
pending.controller.abort();
|
|
this.values.delete(key);
|
|
}
|
|
if (!pending.invalidated) return pending.promise;
|
|
// A caller arriving after a change needs a fresh read, once the old one
|
|
// finishes. The original caller can still receive its snapshot promptly.
|
|
// The older result cannot enter the cache. Drop force when retrying so
|
|
// callers waiting on that same result share the next fresh build.
|
|
const reload = () => this.read({ ...input, force: false });
|
|
return pending.promise.then(reload, reload);
|
|
}
|
|
if (cached) cached.lastReadStarted = now;
|
|
|
|
// Defer load until ownership is installed, including synchronous invalidations.
|
|
const read: ActiveRead = {
|
|
invalidated: false,
|
|
controller: new AbortController(),
|
|
promise: Promise.resolve()
|
|
.then(() => load(read.controller.signal))
|
|
.then((value) => {
|
|
// Never repopulate the cache with a snapshot invalidated during its build.
|
|
if (!read.invalidated)
|
|
this.values.set(key, { value, loadedAt: this.now(), lastReadStarted: now });
|
|
return value;
|
|
})
|
|
.finally(() => this.active.delete(key)),
|
|
};
|
|
this.active.set(key, read);
|
|
return read.promise;
|
|
}
|
|
|
|
invalidate(cwd: string, mode: CheckoutDiffCompare["mode"]): void {
|
|
for (const key of new Set([...this.values.keys(), ...this.active.keys()])) {
|
|
const [cachedCwd, cachedMode] = JSON.parse(key) as string[];
|
|
if (cachedCwd !== cwd || (cachedMode !== mode && cachedMode !== "all")) continue;
|
|
this.values.delete(key);
|
|
const pending = this.active.get(key);
|
|
if (pending) {
|
|
pending.invalidated = true;
|
|
pending.controller.abort();
|
|
}
|
|
}
|
|
}
|
|
}
|