1- // Resolve the usage-bar template from config (`messages.usageTemplate`): either
2- // an inline template object, or a path to a JSON file. For a path, the template
3- // is read ONCE into memory and then kept fresh by a filesystem watcher, so the
4- // per-reply render path never touches disk — no synchronous stat/read in the
5- // latency-sensitive reply-delivery path. When no usable template resolves, the
6- // caller falls back to the built-in (boring) usage line.
71import { type FSWatcher , readFileSync , watch } from "node:fs" ;
82import { homedir } from "node:os" ;
93import { isAbsolute , resolve } from "node:path" ;
@@ -12,9 +6,6 @@ import type { UsageBarTemplate } from "./translator.js";
126export type UsageTemplateConfig = string | Record < string , unknown > | undefined ;
137
148type CacheEntry = { template : UsageBarTemplate | undefined ; watcher ?: FSWatcher } ;
15- // Keyed by resolved path. A present entry means the file was read at least once;
16- // the reply path then serves `template` synchronously with zero filesystem
17- // access, and a watcher refreshes it off the hot path on change.
189const fileCache = new Map < string , CacheEntry > ( ) ;
1910
2011function expandPath ( p : string ) : string {
@@ -27,7 +18,6 @@ function expandPath(p: string): string {
2718 return isAbsolute ( p ) ? p : resolve ( p ) ;
2819}
2920
30- // A usable template must carry a layout the engine understands.
3121function isUsableTemplate ( value : unknown ) : value is UsageBarTemplate {
3222 if ( typeof value !== "object" || value === null ) {
3323 return false ;
@@ -37,22 +27,38 @@ function isUsableTemplate(value: unknown): value is UsageBarTemplate {
3727 return hasOutput || Array . isArray ( obj . segments ) ;
3828}
3929
40- // Read + parse a template file into a usable template, or undefined for
41- // unreadable/invalid contents. Only called off the reply path: at first load and
42- // from the watcher callback.
4330function readTemplateFile ( path : string ) : UsageBarTemplate | undefined {
4431 let raw : string ;
4532 try {
4633 raw = readFileSync ( path , "utf8" ) ;
4734 } catch {
48- return undefined ; // removed/unreadable -> boring fallback
35+ return undefined ;
4936 }
5037 try {
5138 const parsed : unknown = JSON . parse ( raw ) ;
5239 return isUsableTemplate ( parsed ) ? parsed : undefined ;
5340 } catch {
54- return undefined ; // invalid JSON -> boring fallback
41+ return undefined ;
42+ }
43+ }
44+
45+ function cacheTemplateFile ( path : string ) : UsageBarTemplate | undefined {
46+ const entry : CacheEntry = { template : readTemplateFile ( path ) } ;
47+ if ( entry . template ) {
48+ try {
49+ const watcher = watch ( path , { persistent : false } , ( ) => {
50+ entry . template = readTemplateFile ( path ) ;
51+ } ) ;
52+ watcher . on ( "error" , ( ) => {
53+ watcher . close ( ) ;
54+ } ) ;
55+ entry . watcher = watcher ;
56+ } catch {
57+ // Cache remains valid without live refresh.
58+ }
5559 }
60+ fileCache . set ( path , entry ) ;
61+ return entry . template ;
5662}
5763
5864export function loadUsageBarTemplate (
@@ -67,44 +73,9 @@ export function loadUsageBarTemplate(
6773 const path = expandPath ( configured ) ;
6874 const cached = fileCache . get ( path ) ;
6975 if ( cached ) {
70- return cached . template ; // hot path: in-memory, no filesystem access
71- }
72- // First resolution for this path. Probe once; if the file is missing/unreadable
73- // we do NOT cache, so a later-created template is still picked up on a
74- // subsequent call (the only path that stats per reply is the misconfigured
75- // "configured but absent" one, never the normal one).
76- let raw : string ;
77- try {
78- raw = readFileSync ( path , "utf8" ) ;
79- } catch {
80- return undefined ;
81- }
82- let template : UsageBarTemplate | undefined ;
83- try {
84- const parsed : unknown = JSON . parse ( raw ) ;
85- template = isUsableTemplate ( parsed ) ? parsed : undefined ;
86- } catch {
87- template = undefined ;
76+ return cached . template ;
8877 }
89- // The file exists and was read once; from here the reply path is filesystem
90- // free. Keep the in-memory copy fresh via a watcher (off the hot path). A watch
91- // failure (unsupported FS, race) just leaves the one-time load with no live
92- // refresh — still strictly better than a stat on every reply.
93- const entry : CacheEntry = { template } ;
94- try {
95- const watcher = watch ( path , { persistent : false } , ( ) => {
96- entry . template = readTemplateFile ( path ) ;
97- } ) ;
98- watcher . on ( "error" , ( ) => {
99- // Best-effort: keep the last-known template rather than throwing on a
100- // watch error (e.g. the file being removed).
101- } ) ;
102- entry . watcher = watcher ;
103- } catch {
104- // Unwatchable path: cache the one-time load anyway (no refresh until restart).
105- }
106- fileCache . set ( path , entry ) ;
107- return template ;
78+ return cacheTemplateFile ( path ) ;
10879}
10980
11081export function clearUsageBarTemplateCacheForTest ( ) : void {
0 commit comments