browser-debugger-cli
Version:
DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.
233 lines • 7.68 kB
JavaScript
/**
* Query cache manager for DOM element index-based access.
*
* Provides centralized management of DOM query result caching with:
* - Singleton pattern for consistent cache access
* - Navigation-aware staleness detection
* - TTL-based navigation ID caching to reduce IPC calls
*
* The cache enables index-based element access patterns like
* "bdg dom get 0" after running "bdg dom query .selector".
*
* @example
* ```typescript
* const manager = QueryCacheManager.getInstance();
*
* // Store query results
* await manager.set(queryResult);
*
* // Validate and retrieve (throws if stale)
* const validation = await manager.validate();
* if (validation.valid) {
* const cache = validation.cache;
* }
* ```
*/
import { existsSync } from 'fs';
import { readFile, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import { getSessionDir } from '../session/paths.js';
import { createLogger } from '../ui/logging/index.js';
import { getErrorMessage } from '../utils/errors.js';
const log = createLogger('session');
/** TTL for cached navigation ID (500ms). */
const NAVIGATION_ID_CACHE_TTL_MS = 500;
/**
* Singleton manager for DOM query result caching.
*
* Centralizes all cache operations with navigation-aware staleness detection.
* Uses file-based persistence for cross-process access.
*/
export class QueryCacheManager {
static instance = null;
/** Cached navigation ID with timestamp for TTL-based invalidation. */
cachedNavigationId = null;
/**
* Get the singleton instance.
*
* @returns QueryCacheManager instance
*/
static getInstance() {
QueryCacheManager.instance ??= new QueryCacheManager();
return QueryCacheManager.instance;
}
/**
* Reset the singleton instance (for testing).
*/
static resetInstance() {
QueryCacheManager.instance = null;
}
/**
* Get path to query cache file.
*
* @returns Absolute path to query-cache.json
*/
getCachePath() {
return join(getSessionDir(), 'query-cache.json');
}
/**
* Store query results for index-based access.
*
* Writes results to ~/.bdg/query-cache.json for cross-process access.
*
* @param result - DOM query result to cache
*/
async set(result) {
try {
const cachePath = this.getCachePath();
await writeFile(cachePath, JSON.stringify(result), 'utf-8');
log.debug(`Cached ${result.nodes.length} query results to ${cachePath}`);
}
catch (error) {
log.debug(`Failed to write query cache: ${getErrorMessage(error)}`);
}
}
/**
* Get validated cache results.
*
* Returns null if cache is stale or doesn't exist.
* Use getRaw() for unchecked access.
*
* @returns Cached query result or null if invalid/missing
*/
async get() {
const validation = await this.validate();
return validation.valid ? validation.cache : null;
}
/**
* Get raw cache without validation.
*
* Reads from ~/.bdg/query-cache.json if it exists.
* Does not check navigation staleness.
*
* @returns Cached query result or null if no cache exists
*/
async getRaw() {
try {
const cachePath = this.getCachePath();
if (!existsSync(cachePath)) {
return null;
}
const content = await readFile(cachePath, 'utf-8');
const result = JSON.parse(content);
log.debug(`Retrieved ${result.nodes.length} cached query results`);
return result;
}
catch (error) {
log.debug(`Failed to read query cache: ${getErrorMessage(error)}`);
return null;
}
}
/**
* Validate cache against current navigation state.
*
* Checks if the cached query results are still valid by comparing
* the stored navigationId with the current one from the daemon.
*
* @returns Validation result with cache and error info
*
* @example
* ```typescript
* const validation = await manager.validate();
* if (!validation.valid) {
* throw new CommandError(validation.error, { suggestion: validation.suggestion });
* }
* const cache = validation.cache;
* ```
*/
async validate() {
const cache = await this.getRaw();
if (!cache) {
return {
valid: false,
cache: null,
error: 'No cached query results found',
suggestion: 'Run "bdg dom query <selector>" first to generate indexed results',
};
}
if (cache.navigationId === undefined) {
log.debug('Query cache missing navigationId (legacy format), allowing access');
return { valid: true, cache };
}
const currentNavId = await this.getCurrentNavigationId();
if (currentNavId === null) {
log.debug('Could not get current navigationId, allowing cache access');
return { valid: true, cache };
}
if (cache.navigationId !== currentNavId) {
return {
valid: false,
cache,
error: `Query cache is stale (page has navigated since query was run)`,
suggestion: `Re-run "bdg dom query ${cache.selector}" to refresh cached results`,
};
}
return { valid: true, cache };
}
/**
* Clear the query cache.
*
* Removes ~/.bdg/query-cache.json.
* Called when starting a new query or when the session ends.
*/
async clear() {
try {
const cachePath = this.getCachePath();
if (existsSync(cachePath)) {
await rm(cachePath, { force: true });
log.debug('Cleared query cache');
}
}
catch (error) {
log.debug(`Failed to clear query cache: ${getErrorMessage(error)}`);
}
}
/**
* Check if cache file exists.
*
* @returns True if cache file exists
*/
exists() {
return existsSync(this.getCachePath());
}
/**
* Get current navigation ID from the daemon.
*
* Caches the result for 500ms to avoid redundant IPC calls within a single
* command execution while ensuring freshness for subsequent commands.
*
* @returns Current navigation ID or null if unavailable
*/
async getCurrentNavigationId() {
if (this.cachedNavigationId &&
Date.now() - this.cachedNavigationId.timestamp < NAVIGATION_ID_CACHE_TTL_MS) {
return this.cachedNavigationId.value;
}
try {
const { getStatus } = await import('../ipc/client.js');
const response = await getStatus();
if (response.status === 'ok' && response.data?.navigationId !== undefined) {
this.cachedNavigationId = {
value: response.data.navigationId,
timestamp: Date.now(),
};
return response.data.navigationId;
}
return null;
}
catch (error) {
log.debug(`Failed to get current navigation ID: ${getErrorMessage(error)}`);
return null;
}
}
/**
* Invalidate cached navigation ID.
*
* Forces fresh fetch on next getCurrentNavigationId() call.
* Useful after navigation events.
*/
invalidateNavigationCache() {
this.cachedNavigationId = null;
}
}
//# sourceMappingURL=QueryCacheManager.js.map