All files / src/cache CacheManager.ts

88.29% Statements 83/94
77.77% Branches 14/18
81.81% Functions 27/33
95.65% Lines 44/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 10538x 2x 2x                         32x 32x 32x 32x 32x   31x 145x 7x 5x 18x 14x 24x 165x 133x 104x                       15x   15x     15x 15x 15x 15x     11x 3x 4x 4x 4x 3x 2x 1x     3x 3x       4x       15x 15x 15x 15x 15x 15x 15x                 16x                             32x    
// src/cache/CacheManager.ts
import * as vscode from 'vscode';
import * as path from 'path';
import type { WorkspaceIndex, ExtensionConfig } from '../types';
 
// ─── LRU Cache ────────────────────────────────────────────────────────────
 
interface LruNode<V> { key: string; value: V; prev: LruNode<V>|null; next: LruNode<V>|null; }
 
export class LruCache<V> {
  private readonly capacity: number;
  private readonly map: Map<string, LruNode<V>>;
  private head: LruNode<V>; private tail: LruNode<V>;
 
  constructor(capacity: number) {
    this.capacity = Math.max(1, capacity);
    this.map = new Map();
    this.head = { key: '__H__', value: undefined as unknown as V, prev: null, next: null };
    this.tail = { key: '__T__', value: undefined as unknown as V, prev: null, next: null };
    this.head.next = this.tail; this.tail.prev = this.head;
  }
  get(key: string): V|undefined { const n = this.map.get(key); if (!n) { return undefined; } this.moveToFront(n); return n.value; }
  set(key: string, value: V): void { const e = this.map.get(key); if (e) { e.value = value; this.moveToFront(e); return; } const n: LruNode<V> = { key, value, prev: null, next: null }; this.map.set(key, n); this.insertAtFront(n); if (this.map.size > this.capacity) { this.evictLru(); } }
  delete(key: string): boolean { const n = this.map.get(key); if (!n) { return false; } this.removeNode(n); this.map.delete(key); return true; }
  has(key: string): boolean { return this.map.has(key); }
  clear(): void { this.map.clear(); this.head.next = this.tail; this.tail.prev = this.head; }
  get size(): number { return this.map.size; }
  private moveToFront(n: LruNode<V>): void { this.removeNode(n); this.insertAtFront(n); }
  private insertAtFront(n: LruNode<V>): void { n.prev = this.head; n.next = this.head.next; this.head.next!.prev = n; this.head.next = n; }
  private removeNode(n: LruNode<V>): void { n.prev!.next = n.next; n.next!.prev = n.prev; }
  private evictLru(): void { const l = this.tail.prev; Eif (l && l !== this.head) { this.removeNode(l); this.map.delete(l.key); } }
}
 
// ─── AST Cache Entry ──────────────────────────────────────────────────────
 
export interface AstCacheEntry { filePath: string; mtime: number; classes: import('../types').PhpClass[]; parseErrors: number; }
 
// ─── Cache Manager ────────────────────────────────────────────────────────
 
export class CacheManager implements vscode.Disposable {
  private readonly astCache: LruCache<AstCacheEntry>;
  private readonly workspaceRoot: string;
  private readonly disposables: vscode.Disposable[] = [];
  private index: WorkspaceIndex;
  private readonly invalidationListeners: Array<(filePath: string) => void> = [];
 
  constructor(workspaceRoot: string, config: ExtensionConfig) {
    this.workspaceRoot = workspaceRoot;
    this.astCache = new LruCache<AstCacheEntry>(config.lruCacheSize);
    this.index = this.createEmptyIndex();
    this.initFileWatcher(config);
  }
 
  setAst(filePath: string, entry: AstCacheEntry): void { this.astCache.set(filePath, entry); }
  getAst(filePath: string): AstCacheEntry|undefined { return this.astCache.get(filePath); }
  invalidateAst(filePath: string): void { this.astCache.delete(filePath); this.notifyInvalidation(filePath); }
  isAstStale(filePath: string, currentMtime: number): boolean { const c = this.astCache.get(filePath); return !c || c.mtime < currentMtime; }
  get astCacheSize(): number { return this.astCache.size; }
  getIndex(): Readonly<WorkspaceIndex> { return this.index; }
  updateIndex(partial: Partial<WorkspaceIndex>): void { this.index = { ...this.index, ...partial, lastIndexed: new Date() }; }
  clearIndex(): void { this.index = this.createEmptyIndex(); }
 
  onInvalidation(listener: (filePath: string) => void): vscode.Disposable {
    this.invalidationListeners.push(listener);
    return new vscode.Disposable(() => { const i = this.invalidationListeners.indexOf(listener); Eif (i !== -1) { this.invalidationListeners.splice(i, 1); } });
  }
 
  private notifyInvalidation(filePath: string): void {
    for (const l of this.invalidationListeners) { try { l(filePath); } catch (e) { console.error('[Maestro] invalidation error:', e); } }
  }
 
  private initFileWatcher(config: ExtensionConfig): void {
    const phpWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(this.workspaceRoot, '**/*.php'));
    const envWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(this.workspaceRoot, '.env*'));
    phpWatcher.onDidChange((u) => { if (!this.isExcluded(u.fsPath, config.excludePatterns)) { this.invalidateAst(u.fsPath); } });
    phpWatcher.onDidCreate((u) => { this.notifyInvalidation(u.fsPath); });
    phpWatcher.onDidDelete((u) => { this.invalidateAst(u.fsPath); this.index.classes.delete(u.fsPath); this.index.models.delete(u.fsPath); });
    envWatcher.onDidChange((u) => { this.notifyInvalidation(u.fsPath); });
    this.disposables.push(phpWatcher, envWatcher);
  }
 
  private isExcluded(filePath: string, patterns: string[]): boolean {
    const rel = path.relative(this.workspaceRoot, filePath);
    return patterns.some((p) => { const prefix = p.replace('/**', '').replace('/*', ''); return rel.startsWith(prefix); });
  }
 
  private createEmptyIndex(): WorkspaceIndex {
    return {
      routes: new Map(), models: new Map(), classes: new Map(),
      bladeComponents: new Map(), configs: new Map(), translations: new Map(),
      dbSchema: new Map(), views: new Map(), formRequests: new Map(),
      livewireComponents: new Map(), inertiaPages: new Map(),
      filamentResources: new Map(), envVariables: new Map(),
      artisanCommands: new Map(), observers: new Map(),
      nwidartModules: new Set<string>(),
      pennantFeatures: [],
      novaResources: new Map(),
      enums: new Map(),
      lastIndexed: new Date(0),
    };
  }
 
  dispose(): void { for (const d of this.disposables) { d.dispose(); } this.astCache.clear(); }
}