@@ -22,13 +22,15 @@ type ProjectRuntime = {
2222type ReminderFence = {
2323 version : number
2424 reconciliations : number
25+ deletions : number
2526 tombstone : boolean
2627}
2728
2829type ScheduleOptions = {
2930 persist ?: boolean
3031 skipOverdue ?: boolean
3132 retryAttempt ?: number
33+ minimumDelayMs ?: number
3234}
3335
3436const RECONCILIATION_DELAY_MS = 250
@@ -57,6 +59,7 @@ function getRuntime(ctx: PluginInput): ProjectRuntime {
5759 runtimes . set ( key , runtime )
5860 }
5961 runtime . fences ??= new Map ( )
62+ for ( const fence of runtime . fences . values ( ) ) fence . deletions ??= 0
6063 return runtime
6164}
6265
@@ -139,7 +142,7 @@ function clearStateReminder(id: string, state: State): void {
139142function beginReconciliation ( id : string , runtime : ProjectRuntime ) : { fence : ReminderFence ; version : number } {
140143 let fence = runtime . fences . get ( id )
141144 if ( ! fence ) {
142- fence = { version : 0 , reconciliations : 0 , tombstone : false }
145+ fence = { version : 0 , reconciliations : 0 , deletions : 0 , tombstone : false }
143146 runtime . fences . set ( id , fence )
144147 }
145148 fence . reconciliations ++
@@ -160,21 +163,31 @@ function finishReconciliation(id: string, runtime: ProjectRuntime, fence: Remind
160163 retireFence ( id , runtime , fence )
161164}
162165
163- function invalidateReminder ( id : string , runtime : ProjectRuntime ) : ReminderFence {
166+ function invalidateReminder (
167+ id : string ,
168+ runtime : ProjectRuntime ,
169+ ) : { fence : ReminderFence ; version : number } {
164170 let fence = runtime . fences . get ( id )
165171 if ( ! fence ) {
166- fence = { version : 0 , reconciliations : 0 , tombstone : false }
172+ fence = { version : 0 , reconciliations : 0 , deletions : 0 , tombstone : false }
167173 runtime . fences . set ( id , fence )
168174 }
169175 fence . version ++
176+ fence . deletions ++
170177 fence . tombstone = true
171- return fence
178+ return { fence, version : fence . version }
179+ }
180+
181+ function finishInvalidation ( id : string , runtime : ProjectRuntime , fence : ReminderFence ) : void {
182+ fence . deletions --
183+ retireFence ( id , runtime , fence )
172184}
173185
174186function retireFence ( id : string , runtime : ProjectRuntime , fence : ReminderFence ) : void {
175187 if (
176188 runtime . fences . get ( id ) === fence
177189 && fence . reconciliations === 0
190+ && fence . deletions === 0
178191 && ! runtime . executions . has ( id )
179192 ) {
180193 runtime . fences . delete ( id )
@@ -183,17 +196,17 @@ function retireFence(id: string, runtime: ProjectRuntime, fence: ReminderFence):
183196
184197async function deleteReminderLocked ( id : string , ctx : PluginInput , state : State ) : Promise < void > {
185198 const runtime = getRuntime ( ctx )
186- const fence = invalidateReminder ( id , runtime )
199+ const { fence, version } = invalidateReminder ( id , runtime )
187200 const execution = stopLocalExecution ( id , runtime )
188201 clearStateReminder ( id , state )
189202 if ( execution && execution . state !== state ) clearStateReminder ( id , execution . state )
190203 try {
191204 await deleteReminder ( id , ctx )
192205 } catch ( error ) {
193- fence . tombstone = false
206+ if ( runtime . fences . get ( id ) === fence && fence . version === version ) fence . tombstone = false
194207 throw error
195208 } finally {
196- retireFence ( id , runtime , fence )
209+ finishInvalidation ( id , runtime , fence )
197210 }
198211}
199212
@@ -243,7 +256,7 @@ export async function scheduleTimer(
243256 }
244257
245258 const now = Date . now ( )
246- const delay = Math . max ( 0 , reminder . time . nextExecution - now )
259+ const delay = Math . max ( options . minimumDelayMs ?? 0 , reminder . time . nextExecution - now , 0 )
247260 const skipOverdue = options . skipOverdue === true && reminder . time . nextExecution < now
248261 const retryAttempt = reminder . time . nextExecution < now ? options . retryAttempt ?? 0 : 0
249262 const timer = setTimeout ( async ( ) => {
@@ -292,6 +305,7 @@ function scheduleReconciliation(
292305 await reconcileReminder ( id , ctx , state , config , {
293306 skipOverdue : options . skipOverdue ,
294307 retryAttempt,
308+ minimumDelayMs : options . minimumDelayMs ,
295309 } )
296310 } , delay )
297311 timer . unref ( )
@@ -339,6 +353,7 @@ export async function reconcileReminder(
339353 persist : false ,
340354 skipOverdue : options . skipOverdue ,
341355 retryAttempt : options . retryAttempt ,
356+ minimumDelayMs : options . minimumDelayMs ,
342357 } )
343358 } )
344359 } catch ( error ) {
@@ -565,10 +580,16 @@ export async function executeReminder(
565580 }
566581}
567582
568- export async function cancelReminder ( id : string , ctx : PluginInput , state : State ) : Promise < boolean > {
583+ export async function cancelReminder (
584+ id : string ,
585+ ctx : PluginInput ,
586+ state : State ,
587+ config : PluginConfig ,
588+ ) : Promise < boolean > {
569589 const runtime = getRuntime ( ctx )
570- const fence = invalidateReminder ( id , runtime )
590+ const { fence, version } = invalidateReminder ( id , runtime )
571591 const execution = stopLocalExecution ( id , runtime )
592+ const rollbackState = execution ?. state ?? state
572593 clearStateReminder ( id , state )
573594 if ( execution && execution . state !== state ) clearStateReminder ( id , execution . state )
574595 try {
@@ -578,13 +599,23 @@ export async function cancelReminder(id: string, ctx: PluginInput, state: State)
578599 clearStateReminder ( id , state )
579600 if ( replacement && replacement . state !== state ) clearStateReminder ( id , replacement . state )
580601 } )
581- retireFence ( id , runtime , fence )
602+ finishInvalidation ( id , runtime , fence )
582603 logger . info ( `Reminder ${ id } cancelled` )
583604 return true
584605 } catch ( error ) {
585- fence . tombstone = false
586- retireFence ( id , runtime , fence )
606+ const ownsFence = runtime . fences . get ( id ) === fence && fence . version === version
607+ if ( ownsFence ) fence . tombstone = false
608+ finishInvalidation ( id , runtime , fence )
587609 logger . error ( `Failed to cancel reminder ${ id } :` , error )
610+ if ( ownsFence ) {
611+ try {
612+ await reconcileReminder ( id , ctx , rollbackState , config , {
613+ minimumDelayMs : RECONCILIATION_DELAY_MS ,
614+ } )
615+ } catch ( rollbackError ) {
616+ logger . error ( `Failed to restore reminder ${ id } after cancellation failure:` , rollbackError )
617+ }
618+ }
588619 throw error
589620 }
590621}
0 commit comments