Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/browser/runtime/local-cloak/cloak-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it, vi } from 'vitest';

// Isolated file: the version cache is module-level and populated by the first
// call in the process, so this must import a fresh copy to observe the read.
describe('resolveCloakBrowserVersion', () => {
it('resolves and reads cloakbrowser/package.json only once per process', async () => {
vi.resetModules();
const fs = (await import('node:fs')).default;
// Import before spying: loading the module graph reads files of its own,
// and only reads made by resolveCloakBrowserVersion should be counted.
const { resolveCloakBrowserVersion } = await import('./session-manager.js');
const readFileSync = vi.spyOn(fs, 'readFileSync');
try {
const first = resolveCloakBrowserVersion();
const second = resolveCloakBrowserVersion();
const third = resolveCloakBrowserVersion();

expect(second).toBe(first);
expect(third).toBe(first);
expect(readFileSync).toHaveBeenCalledTimes(1);
} finally {
readFileSync.mockRestore();
}
});
});
21 changes: 17 additions & 4 deletions src/browser/runtime/local-cloak/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,28 @@ import { normalizeProfileId, resolveCloakProfileDir } from './profiles.js';
import { CloakNetworkCapture } from './network.js';
import { findPackageRoot } from '../../../package-paths.js';

/** Installed `cloakbrowser` npm package version, for doctor/status display. */
const UNRESOLVED = Symbol('unresolved');
let cachedCloakBrowserVersion: string | undefined | typeof UNRESOLVED = UNRESOLVED;

/**
* Installed `cloakbrowser` npm package version, for doctor/status display.
*
* Resolved once per process. The version cannot change while we are running, and
* `profileStatuses()` calls this per profile, so an uncached read meant N+1
* synchronous resolve-read-parse cycles on every status poll. The sentinel keeps
* a genuine `undefined` (the catch path) cached too, so an unresolvable
* `cloakbrowser` is not retried on every call.
*/
export function resolveCloakBrowserVersion(): string | undefined {
if (cachedCloakBrowserVersion !== UNRESOLVED) return cachedCloakBrowserVersion;
try {
const entryPath = fileURLToPath(import.meta.resolve('cloakbrowser'));
const pkg = JSON.parse(fs.readFileSync(path.join(findPackageRoot(entryPath), 'package.json'), 'utf-8'));
return typeof pkg.version === 'string' ? pkg.version : undefined;
const pkg = JSON.parse(fs.readFileSync(path.join(findPackageRoot(entryPath), 'package.json'), 'utf-8')) as { version?: unknown };
cachedCloakBrowserVersion = typeof pkg.version === 'string' ? pkg.version : undefined;
} catch {
return undefined;
cachedCloakBrowserVersion = undefined;
}
return cachedCloakBrowserVersion;
}

export type LaunchPersistentContext = typeof cloakLaunchPersistentContext;
Expand Down