@@ -164,6 +164,17 @@ export type GmailWatcherStartResult = {
164164 reason ?: string ;
165165} ;
166166
167+ type GmailWatcherCancellation = {
168+ dispose : ( ) => void ;
169+ isCancelled : ( ) => boolean ;
170+ signal ?: AbortSignal ;
171+ } ;
172+
173+ type GmailWatcherStartOptions = {
174+ isCancelled ?: ( ) => boolean ;
175+ signal ?: AbortSignal ;
176+ } ;
177+
167178function cancelledGmailWatcherStart (
168179 expectedConfig : GmailHookRuntimeConfig ,
169180) : GmailWatcherStartResult {
@@ -173,13 +184,63 @@ function cancelledGmailWatcherStart(
173184 return { started : false , reason : "startup cancelled" } ;
174185}
175186
187+ function isGmailWatcherStartCancelled ( options : GmailWatcherStartOptions ) : boolean {
188+ return options . signal ?. aborted === true || options . isCancelled ?.( ) === true ;
189+ }
190+
191+ function createGmailWatcherCancellation (
192+ options : GmailWatcherStartOptions ,
193+ ) : GmailWatcherCancellation {
194+ if ( ! options . signal && ! options . isCancelled ) {
195+ return {
196+ dispose : ( ) => { } ,
197+ isCancelled : ( ) => false ,
198+ } ;
199+ }
200+
201+ const abortController = new AbortController ( ) ;
202+ const abort = ( ) => {
203+ if ( ! abortController . signal . aborted ) {
204+ abortController . abort ( ) ;
205+ }
206+ } ;
207+ const onAbort = ( ) => abort ( ) ;
208+ options . signal ?. addEventListener ( "abort" , onAbort , { once : true } ) ;
209+
210+ let cancelPoll : ReturnType < typeof setInterval > | null = null ;
211+ if ( options . isCancelled ) {
212+ cancelPoll = setInterval ( ( ) => {
213+ if ( options . isCancelled ?.( ) ) {
214+ abort ( ) ;
215+ }
216+ } , 100 ) ;
217+ cancelPoll . unref ?.( ) ;
218+ }
219+
220+ if ( isGmailWatcherStartCancelled ( options ) ) {
221+ abort ( ) ;
222+ }
223+
224+ return {
225+ dispose : ( ) => {
226+ if ( cancelPoll ) {
227+ clearInterval ( cancelPoll ) ;
228+ cancelPoll = null ;
229+ }
230+ options . signal ?. removeEventListener ( "abort" , onAbort ) ;
231+ } ,
232+ isCancelled : ( ) => abortController . signal . aborted || isGmailWatcherStartCancelled ( options ) ,
233+ signal : abortController . signal ,
234+ } ;
235+ }
236+
176237/**
177238 * Start the Gmail watcher service.
178239 * Called automatically by the gateway if hooks.gmail is configured.
179240 */
180241export async function startGmailWatcher (
181242 cfg : OpenClawConfig ,
182- options : { isCancelled ?: ( ) => boolean } = { } ,
243+ options : GmailWatcherStartOptions = { } ,
183244) : Promise < GmailWatcherStartResult > {
184245 // Check if gmail hooks are configured
185246 if ( ! cfg . hooks ?. enabled ) {
@@ -203,54 +264,55 @@ export async function startGmailWatcher(
203264 }
204265
205266 const runtimeConfig = resolved . value ;
206- if ( options . isCancelled ?. ( ) ) {
267+ if ( isGmailWatcherStartCancelled ( options ) ) {
207268 return cancelledGmailWatcherStart ( runtimeConfig ) ;
208269 }
209270 currentConfig = runtimeConfig ;
210271
211272 // Set up Tailscale endpoint if needed
212273 if ( runtimeConfig . tailscale . mode !== "off" ) {
274+ const cancellation = createGmailWatcherCancellation ( options ) ;
213275 try {
214276 await ensureTailscaleEndpoint ( {
215277 mode : runtimeConfig . tailscale . mode ,
216278 path : runtimeConfig . tailscale . path ,
217279 port : runtimeConfig . serve . port ,
280+ signal : cancellation . signal ,
218281 target : runtimeConfig . tailscale . target ,
219282 } ) ;
220283 log . info (
221284 `tailscale ${ runtimeConfig . tailscale . mode } configured for port ${ runtimeConfig . serve . port } ` ,
222285 ) ;
223- if ( options . isCancelled ?. ( ) ) {
286+ if ( cancellation . isCancelled ( ) ) {
224287 return cancelledGmailWatcherStart ( runtimeConfig ) ;
225288 }
226289 } catch ( err ) {
290+ if ( cancellation . isCancelled ( ) ) {
291+ return cancelledGmailWatcherStart ( runtimeConfig ) ;
292+ }
227293 log . error ( `tailscale setup failed: ${ String ( err ) } ` ) ;
228294 return {
229295 started : false ,
230296 reason : `tailscale setup failed: ${ String ( err ) } ` ,
231297 } ;
298+ } finally {
299+ cancellation . dispose ( ) ;
232300 }
233301 }
234302
235303 // Start the Gmail watch (register with Gmail API)
236- const abortController = new AbortController ( ) ;
237- const cancelPoll = setInterval ( ( ) => {
238- if ( options . isCancelled ?.( ) ) {
239- abortController . abort ( ) ;
240- }
241- } , 100 ) ;
242- cancelPoll . unref ?.( ) ;
243- const watchStarted = await startGmailWatch ( runtimeConfig , { signal : abortController . signal } ) ;
244- clearInterval ( cancelPoll ) ;
245- if ( options . isCancelled ?.( ) ) {
304+ const cancellation = createGmailWatcherCancellation ( options ) ;
305+ const watchStarted = await startGmailWatch ( runtimeConfig , { signal : cancellation . signal } ) ;
306+ cancellation . dispose ( ) ;
307+ if ( cancellation . isCancelled ( ) ) {
246308 return cancelledGmailWatcherStart ( runtimeConfig ) ;
247309 }
248310 if ( ! watchStarted ) {
249311 log . warn ( "gmail watch start failed, but continuing with serve" ) ;
250312 }
251313
252314 // Spawn the gog serve process
253- if ( options . isCancelled ?. ( ) ) {
315+ if ( isGmailWatcherStartCancelled ( options ) ) {
254316 return cancelledGmailWatcherStart ( runtimeConfig ) ;
255317 }
256318 shuttingDown = false ;
0 commit comments