55 * the toolbar badge with the cache status (HIT/MISS/etc).
66 */
77
8- // Import shared constants (loaded via manifest.json)
9- // Uses: TRACKED_HEADERS, STATUS_COLORS, detectCDN, parseCacheStatus
10-
118// =============================================================================
129// State Management
1310// =============================================================================
1411
1512/** Stores cache header data per tab ID */
1613const tabData = new Map ( ) ;
1714
18- /** Tracks tabs with pending navigations to capture only the first main request */
19- const pendingNavigations = new Set ( ) ;
20-
21- /** Connected popup ports, keyed by tab ID they're watching */
15+ /** Connected popup ports */
2216const popupPorts = new Map ( ) ;
2317
24- /**
25- * Notifies the popup if one is connected and watching this tab.
26- * @param {number } tabId - Tab ID that was updated
27- */
28- function notifyPopup ( tabId ) {
29- const port = popupPorts . get ( tabId ) ;
30- if ( port ) {
31- port . postMessage ( { type : 'update' , data : tabData . get ( tabId ) || null } ) ;
32- }
33- }
18+ /** Track pending navigations to detect missing headers (Safari limitation workaround) */
19+ const pendingNavigations = new Map ( ) ;
3420
3521// =============================================================================
3622// Badge Management
3723// =============================================================================
3824
39- /**
40- * Updates the toolbar badge text and color for a tab.
41- * @param {number } tabId - Browser tab ID
42- * @param {string|null } status - Cache status (HIT, MISS, etc.)
43- * @param {string|null } cdn - CDN identifier
44- */
45- function updateBadge ( tabId , status , cdn ) {
46- const displayStatus = status || 'NONE' ;
47- const colors = STATUS_COLORS [ displayStatus ] || STATUS_COLORS [ 'NONE' ] ;
48-
25+ function updateBadge ( tabId , status ) {
4926 const badgeTextMap = {
50- 'HIT' : 'HIT' , 'MISS' : 'MISS' , 'EXPIRED' : 'EXP' , 'STALE' : 'STL' ,
51- 'REVALIDATED' : 'REV' , 'BYPASS' : 'BYP' , 'DYNAMIC' : 'DYN' ,
52- 'REFRESH' : 'REF' , 'ERROR' : 'ERR'
27+ 'HIT' : 'HIT' ,
28+ 'MISS' : 'MISS' ,
29+ 'EXPIRED' : 'EXP' ,
30+ 'STALE' : 'STALE' ,
31+ 'REVALIDATED' : 'REV' ,
32+ 'BYPASS' : 'BYP' ,
33+ 'DYNAMIC' : 'DYN' ,
34+ 'REFRESH' : 'REF' ,
35+ 'ERROR' : 'ERR'
5336 } ;
54- const badgeText = status ? ( badgeTextMap [ status ] || status . substring ( 0 , 3 ) ) : '' ;
37+ const badgeText = status ? ( badgeTextMap [ status . toUpperCase ( ) ] || status ) : '' ;
38+ const color = status === 'HIT' ? '#34C759' : status === 'MISS' ? '#FF3B30' : '#8E8E93' ;
5539
5640 browser . action . setBadgeText ( { text : badgeText } ) ;
57- browser . action . setBadgeBackgroundColor ( { color : colors . badge } ) ;
41+ browser . action . setBadgeBackgroundColor ( { color } ) ;
5842
5943 try {
6044 browser . action . setBadgeText ( { text : badgeText , tabId } ) ;
61- browser . action . setBadgeBackgroundColor ( { color : colors . badge , tabId } ) ;
45+ browser . action . setBadgeBackgroundColor ( { color, tabId } ) ;
6246 } catch ( e ) {
6347 // Per-tab badges not supported
6448 }
6549}
6650
67- /**
68- * Clears the badge for a tab.
69- * @param {number } tabId - Browser tab ID
70- */
7151function clearBadge ( tabId ) {
7252 browser . action . setBadgeText ( { text : '' } ) ;
7353 try {
7454 browser . action . setBadgeText ( { text : '' , tabId } ) ;
75- } catch ( e ) {
76- // Per-tab badges not supported
55+ } catch ( e ) { }
56+ }
57+
58+ // =============================================================================
59+ // Popup Communication
60+ // =============================================================================
61+
62+ function notifyPopup ( tabId ) {
63+ const port = popupPorts . get ( tabId ) ;
64+ if ( port ) {
65+ port . postMessage ( { type : 'update' , data : tabData . get ( tabId ) || null } ) ;
7766 }
7867}
7968
8069// =============================================================================
8170// Event Listeners
8271// =============================================================================
8372
84- // Track navigation start to capture only the first main document request
73+ // --- Web Navigation Events ---
74+
8575browser . webNavigation . onBeforeNavigate . addListener ( ( details ) => {
8676 if ( details . frameId === 0 ) {
87- pendingNavigations . add ( details . tabId ) ;
8877 tabData . delete ( details . tabId ) ;
78+ clearBadge ( details . tabId ) ;
79+ notifyPopup ( details . tabId ) ;
80+
81+ // Track navigation to detect Safari limitation where webRequest events don't fire
82+ pendingNavigations . set ( details . tabId , {
83+ url : details . url ,
84+ timestamp : Date . now ( ) ,
85+ headersReceived : false
86+ } ) ;
8987 }
9088} ) ;
9189
92- // Update URL after navigation completes (handles redirects)
9390browser . webNavigation . onCompleted . addListener ( ( details ) => {
9491 if ( details . frameId === 0 ) {
9592 const data = tabData . get ( details . tabId ) ;
93+ const pending = pendingNavigations . get ( details . tabId ) ;
94+
95+ // Check if we never received headers (Safari limitation with external links/bookmarks)
96+ if ( pending && ! pending . headersReceived ) {
97+ const existingData = tabData . get ( details . tabId ) ;
98+ if ( existingData ) {
99+ existingData . noHeaders = true ;
100+ existingData . url = details . url ;
101+ } else {
102+ tabData . set ( details . tabId , {
103+ url : details . url ,
104+ headers : { } ,
105+ status : null ,
106+ cdn : null ,
107+ timestamp : Date . now ( ) ,
108+ noHeaders : true
109+ } ) ;
110+ }
111+
112+ // Show reload badge
113+ browser . action . setBadgeText ( { text : 'RLD' } ) ;
114+ browser . action . setBadgeBackgroundColor ( { color : '#8E8E93' } ) ;
115+ try {
116+ browser . action . setBadgeText ( { text : 'RLD' , tabId : details . tabId } ) ;
117+ browser . action . setBadgeBackgroundColor ( { color : '#8E8E93' , tabId : details . tabId } ) ;
118+ } catch ( e ) { }
119+
120+ notifyPopup ( details . tabId ) ;
121+ }
122+
123+ // Update URL if we have data (handles redirects)
96124 if ( data && data . url !== details . url ) {
97125 data . url = details . url ;
98126 }
127+
128+ pendingNavigations . delete ( details . tabId ) ;
99129 }
100130} ) ;
101131
102- // Capture response headers for main document requests
103- browser . webRequest . onHeadersReceived . addListener (
104- ( details ) => {
105- if ( details . type !== 'main_frame' || details . frameId !== 0 ) return ;
106- if ( ! pendingNavigations . has ( details . tabId ) ) return ;
132+ // --- Web Request Events ---
107133
108- pendingNavigations . delete ( details . tabId ) ;
134+ /**
135+ * Process response headers from a main frame request.
136+ * Called by both onHeadersReceived and onResponseStarted (Safari fallback).
137+ */
138+ function processMainFrameHeaders ( details ) {
139+ // Mark navigation as having received headers (for Safari limitation detection)
140+ const pendingNav = pendingNavigations . get ( details . tabId ) ;
141+ if ( pendingNav ) {
142+ pendingNav . headersReceived = true ;
143+ }
109144
110- // Extract tracked headers
111- const headers = { } ;
112- for ( const header of details . responseHeaders || [ ] ) {
113- const name = header . name . toLowerCase ( ) ;
114- if ( TRACKED_HEADERS . includes ( name ) ) {
115- headers [ name ] = header . value ;
116- }
145+ // Extract tracked headers
146+ const headers = { } ;
147+ for ( const header of details . responseHeaders || [ ] ) {
148+ const name = header . name . toLowerCase ( ) ;
149+ if ( TRACKED_HEADERS . includes ( name ) ) {
150+ headers [ name ] = header . value ;
117151 }
152+ }
118153
119- // Detect CDN and parse status using shared functions
120- const cdn = detectCDN ( headers ) ;
121- const status = parseCacheStatus ( headers , cdn ) ;
154+ // Detect CDN and parse status
155+ const cdn = detectCDN ( headers ) ;
156+ const status = parseCacheStatus ( headers , cdn ) ;
122157
123- tabData . set ( details . tabId , {
124- url : details . url ,
125- headers,
126- status,
127- cdn,
128- timestamp : Date . now ( )
129- } ) ;
158+ // Store data
159+ tabData . set ( details . tabId , {
160+ url : details . url ,
161+ headers,
162+ status,
163+ cdn,
164+ timestamp : Date . now ( )
165+ } ) ;
130166
131- updateBadge ( details . tabId , status , cdn ) ;
132- notifyPopup ( details . tabId ) ;
167+ updateBadge ( details . tabId , status ) ;
168+ notifyPopup ( details . tabId ) ;
169+ }
170+
171+ browser . webRequest . onHeadersReceived . addListener (
172+ ( details ) => {
173+ if ( details . type === 'main_frame' && details . frameId === 0 ) {
174+ processMainFrameHeaders ( details ) ;
175+ }
133176 } ,
134177 { urls : [ '<all_urls>' ] } ,
135178 [ 'responseHeaders' ]
136179) ;
137180
138- // Clean up data when tab closes
181+ // Safari fallback: onResponseStarted sometimes fires when onHeadersReceived doesn't
182+ browser . webRequest . onResponseStarted . addListener (
183+ ( details ) => {
184+ if ( details . type !== 'main_frame' || details . frameId !== 0 ) {
185+ return ;
186+ }
187+
188+ const pendingNav = pendingNavigations . get ( details . tabId ) ;
189+ if ( pendingNav && ! pendingNav . headersReceived ) {
190+ processMainFrameHeaders ( details ) ;
191+ }
192+ } ,
193+ { urls : [ '<all_urls>' ] } ,
194+ [ 'responseHeaders' ]
195+ ) ;
196+
197+ // --- Tab Events ---
198+
139199browser . tabs . onRemoved . addListener ( ( tabId ) => {
140200 tabData . delete ( tabId ) ;
201+ popupPorts . delete ( tabId ) ;
141202 pendingNavigations . delete ( tabId ) ;
142203} ) ;
143204
144- // Update badge when switching tabs
145205browser . tabs . onActivated . addListener ( ( activeInfo ) => {
146206 const data = tabData . get ( activeInfo . tabId ) ;
147207 if ( data ) {
148- updateBadge ( activeInfo . tabId , data . status , data . cdn ) ;
208+ updateBadge ( activeInfo . tabId , data . status ) ;
149209 } else {
150210 clearBadge ( activeInfo . tabId ) ;
151211 }
152212} ) ;
153213
154- // Update badge when window focus changes
214+ // --- Window Events ---
215+
155216browser . windows . onFocusChanged . addListener ( async ( windowId ) => {
156217 if ( windowId === browser . windows . WINDOW_ID_NONE ) return ;
157218
@@ -160,7 +221,7 @@ browser.windows.onFocusChanged.addListener(async (windowId) => {
160221 if ( tabs ?. [ 0 ] ) {
161222 const data = tabData . get ( tabs [ 0 ] . id ) ;
162223 if ( data ) {
163- updateBadge ( tabs [ 0 ] . id , data . status , data . cdn ) ;
224+ updateBadge ( tabs [ 0 ] . id , data . status ) ;
164225 } else {
165226 clearBadge ( tabs [ 0 ] . id ) ;
166227 }
@@ -170,7 +231,8 @@ browser.windows.onFocusChanged.addListener(async (windowId) => {
170231 }
171232} ) ;
172233
173- // Handle messages from popup and content scripts
234+ // --- Message Handling ---
235+
174236browser . runtime . onMessage . addListener ( ( message , sender , sendResponse ) => {
175237 if ( message . type === 'getTabData' ) {
176238 sendResponse ( tabData . get ( message . tabId ) || null ) ;
@@ -196,20 +258,19 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
196258 return true ;
197259} ) ;
198260
199- // Handle popup connections for reactive updates
261+ // --- Popup Port Connection ---
262+
200263browser . runtime . onConnect . addListener ( ( port ) => {
201264 if ( port . name !== 'popup' ) return ;
202265
203266 port . onMessage . addListener ( ( msg ) => {
204267 if ( msg . type === 'subscribe' && msg . tabId ) {
205268 popupPorts . set ( msg . tabId , port ) ;
206- // Send initial data immediately
207269 port . postMessage ( { type : 'update' , data : tabData . get ( msg . tabId ) || null } ) ;
208270 }
209271 } ) ;
210272
211273 port . onDisconnect . addListener ( ( ) => {
212- // Remove port from map when popup closes
213274 for ( const [ tabId , p ] of popupPorts ) {
214275 if ( p === port ) {
215276 popupPorts . delete ( tabId ) ;
0 commit comments