@@ -36,30 +36,73 @@ type GuardEntry = {
3636 settings : unknown ;
3737} ;
3838
39- const offscreenApi = ( globalThis as { chrome ?: { offscreen ?: any } } ) . chrome ?. offscreen ;
39+ // MV3-only (Chrome): WXT provides typings for `browser.offscreen`.
40+ // Avoid `globalThis` shims/`any` here so type checking stays strict.
41+ const offscreenApi = isMV3 ? browser . offscreen : undefined ;
42+
43+ function isNoOffscreenDocumentError ( error : unknown ) : boolean {
44+ const message =
45+ error instanceof Error
46+ ? error . message
47+ : typeof error === "string"
48+ ? error
49+ : JSON . stringify ( error ) ;
50+ return message . includes ( "No current offscreen document" ) ;
51+ }
52+
53+ function isReceivingEndDoesNotExistError ( error : unknown ) : boolean {
54+ const message =
55+ error instanceof Error
56+ ? error . message
57+ : typeof error === "string"
58+ ? error
59+ : JSON . stringify ( error ) ;
60+ return (
61+ message . includes ( "Receiving end does not exist" ) ||
62+ message . includes ( "Could not establish connection" ) ||
63+ message . includes ( "No current offscreen document" )
64+ ) ;
65+ }
4066
4167async function ensureOffscreenDocument ( ) : Promise < void > {
4268 if ( ! isMV3 || ! offscreenApi ) return ;
43- const hasDocument = ( await offscreenApi . hasDocument ?.( ) ) as boolean | undefined ;
44- if ( hasDocument ) return ;
45-
46- await offscreenApi . createDocument ?.( {
47- url : browser . runtime . getURL (
48- "/offscreen.html" as unknown as Parameters < typeof browser . runtime . getURL > [ 0 ] ,
49- ) ,
50- reasons : [ "WORKERS" ] ,
51- justification : "Maintain unlock guards that require real-time server state via WebSocket." ,
52- } ) ;
69+ try {
70+ const hasDocument = ( await offscreenApi . hasDocument ( ) ) as boolean | undefined ;
71+ if ( hasDocument ) return ;
72+ } catch ( error ) {
73+ // Treat errors as "no document" and attempt to create one.
74+ console . warn ( "[distracted] offscreen.hasDocument failed:" , error ) ;
75+ }
76+
77+ try {
78+ await offscreenApi . createDocument ( {
79+ url : browser . runtime . getURL (
80+ "/offscreen.html" as unknown as Parameters < typeof browser . runtime . getURL > [ 0 ] ,
81+ ) ,
82+ reasons : [ "WORKERS" ] ,
83+ justification : "Maintain unlock guards that require real-time server state via WebSocket." ,
84+ } ) ;
85+ } catch ( error ) {
86+ // Chrome can be flaky here; if the document was created concurrently, this is safe to ignore.
87+ console . warn ( "[distracted] offscreen.createDocument failed:" , error ) ;
88+ }
5389}
5490
5591async function maybeCloseOffscreenDocument ( ) : Promise < void > {
5692 if ( ! isMV3 || ! offscreenApi ) return ;
5793 const session = await browser . storage . session . get ( ) ;
5894 const hasGuards = Object . keys ( session ) . some ( ( key ) => key . startsWith ( GUARD_PREFIX ) ) ;
5995 if ( ! hasGuards ) {
60- const hasDocument = ( await offscreenApi . hasDocument ?.( ) ) as boolean | undefined ;
61- if ( hasDocument ) {
62- await offscreenApi . closeDocument ?.( ) ;
96+ try {
97+ const hasDocument = ( await offscreenApi . hasDocument ( ) ) as boolean | undefined ;
98+ if ( hasDocument ) {
99+ await offscreenApi . closeDocument ( ) ;
100+ }
101+ } catch ( error ) {
102+ // If the document is already gone, treat close as a no-op.
103+ if ( ! isNoOffscreenDocumentError ( error ) ) {
104+ console . warn ( "[distracted] offscreen.closeDocument failed:" , error ) ;
105+ }
63106 }
64107 }
65108}
@@ -135,6 +178,24 @@ async function startGuardForSite(site: BlockedSite): Promise<void> {
135178 pollIntervalMs : guard . pollIntervalMs ,
136179 } ) ;
137180 } catch ( error ) {
181+ // MV3 service worker/offscreen can race; retry once after ensuring document exists.
182+ if ( isReceivingEndDoesNotExistError ( error ) ) {
183+ try {
184+ await ensureOffscreenDocument ( ) ;
185+ await browser . runtime . sendMessage ( {
186+ type : "GUARD_START" ,
187+ siteId : entry . siteId ,
188+ method : entry . method ,
189+ settings : entry . settings ,
190+ heartbeatMs : GUARD_HEARTBEAT_MS ,
191+ pollIntervalMs : guard . pollIntervalMs ,
192+ } ) ;
193+ return ;
194+ } catch ( retryError ) {
195+ console . warn ( "[distracted] Failed to start guard in offscreen (retry):" , retryError ) ;
196+ return ;
197+ }
198+ }
138199 console . warn ( "[distracted] Failed to start guard in offscreen:" , error ) ;
139200 }
140201 } else {
@@ -152,7 +213,10 @@ async function stopGuardForSite(siteId: string): Promise<void> {
152213 siteId,
153214 } ) ;
154215 } catch ( error ) {
155- console . warn ( "[distracted] Failed to stop guard in offscreen:" , error ) ;
216+ // If offscreen isn't around anymore, stopping is effectively complete.
217+ if ( ! isReceivingEndDoesNotExistError ( error ) ) {
218+ console . warn ( "[distracted] Failed to stop guard in offscreen:" , error ) ;
219+ }
156220 }
157221 await maybeCloseOffscreenDocument ( ) ;
158222 } else {
0 commit comments