@@ -99,6 +99,18 @@ function isAlreadyDeletedError(error: unknown): boolean {
9999 * Handles creating, updating, and deleting calendar events when tasks change.
100100 */
101101export class TaskCalendarSyncService {
102+ /** In-flight create operations keyed by calendar and task path to avoid duplicate Google events */
103+ private static pendingEventCreates : Map < string , Promise < string > > = new Map ( ) ;
104+
105+ /** In-flight detached exception creates keyed by calendar and task path */
106+ private static pendingExceptionEventCreates : Map < string , Promise < string > > = new Map ( ) ;
107+
108+ /** Event IDs written during this session, used while Obsidian metadata catches up */
109+ private static taskEventIdCache : Map < string , string > = new Map ( ) ;
110+
111+ /** Detached exception event IDs written during this session */
112+ private static taskExceptionEventIdCache : Map < string , string > = new Map ( ) ;
113+
102114 private plugin : TaskNotesPlugin ;
103115 private googleCalendarService : GoogleCalendarService ;
104116 private rateLimitChain : Promise < unknown > = Promise . resolve ( ) ;
@@ -119,15 +131,6 @@ export class TaskCalendarSyncService {
119131 /** Store the latest explicitly passed task object during debounce to avoid cache race conditions */
120132 private pendingTasks : Map < string , TaskInfo > = new Map ( ) ;
121133
122- /** In-flight create operations keyed by task path to avoid duplicate Google events */
123- private pendingEventCreates : Map < string , Promise < string > > = new Map ( ) ;
124-
125- /** Event IDs written during this session, used while Obsidian metadata catches up */
126- private taskEventIdCache : Map < string , string > = new Map ( ) ;
127-
128- /** Detached recurring exception event IDs written during this session */
129- private taskExceptionEventIdCache : Map < string , string > = new Map ( ) ;
130-
131134 /** Last calendar-relevant task fingerprint persisted after successful syncs */
132135 private calendarFingerprints : Map < string , string > | null = null ;
133136
@@ -136,6 +139,64 @@ export class TaskCalendarSyncService {
136139 this . googleCalendarService = googleCalendarService ;
137140 }
138141
142+ private static getTaskCalendarCacheKey ( taskPath : string , calendarId ?: string ) : string {
143+ return calendarId ? `${ calendarId } ::${ taskPath } ` : taskPath ;
144+ }
145+
146+ private static deleteTaskPathEntries ( cache : Map < string , unknown > , taskPath : string ) : void {
147+ cache . delete ( TaskCalendarSyncService . getTaskCalendarCacheKey ( taskPath ) ) ;
148+ for ( const key of Array . from ( cache . keys ( ) ) ) {
149+ if ( key . endsWith ( `::${ taskPath } ` ) ) {
150+ cache . delete ( key ) ;
151+ }
152+ }
153+ }
154+
155+ private static clearTaskEventIdCache ( taskPath : string , calendarId ?: string ) : void {
156+ if ( calendarId ) {
157+ TaskCalendarSyncService . taskEventIdCache . delete (
158+ TaskCalendarSyncService . getTaskCalendarCacheKey ( taskPath , calendarId )
159+ ) ;
160+ return ;
161+ }
162+
163+ TaskCalendarSyncService . deleteTaskPathEntries (
164+ TaskCalendarSyncService . taskEventIdCache ,
165+ taskPath
166+ ) ;
167+ }
168+
169+ private static clearTaskExceptionEventIdCache (
170+ taskPath : string ,
171+ calendarId ?: string
172+ ) : void {
173+ if ( calendarId ) {
174+ TaskCalendarSyncService . taskExceptionEventIdCache . delete (
175+ TaskCalendarSyncService . getTaskCalendarCacheKey ( taskPath , calendarId )
176+ ) ;
177+ return ;
178+ }
179+
180+ TaskCalendarSyncService . deleteTaskPathEntries (
181+ TaskCalendarSyncService . taskExceptionEventIdCache ,
182+ taskPath
183+ ) ;
184+ }
185+
186+ static clearSharedGoogleCalendarSyncStateForTests ( ) : void {
187+ TaskCalendarSyncService . pendingEventCreates . clear ( ) ;
188+ TaskCalendarSyncService . pendingExceptionEventCreates . clear ( ) ;
189+ TaskCalendarSyncService . taskEventIdCache . clear ( ) ;
190+ TaskCalendarSyncService . taskExceptionEventIdCache . clear ( ) ;
191+ }
192+
193+ private getTaskEventIdCacheKey ( taskPath : string , calendarId ?: string ) : string {
194+ return TaskCalendarSyncService . getTaskCalendarCacheKey (
195+ taskPath ,
196+ calendarId || this . plugin . settings . googleCalendarExport . targetCalendarId
197+ ) ;
198+ }
199+
139200 private profileAsync < T > (
140201 name : string ,
141202 fn : ( ) => Promise < T > ,
@@ -178,9 +239,6 @@ export class TaskCalendarSyncService {
178239 this . pendingSyncs . clear ( ) ;
179240 this . previousTaskState . clear ( ) ;
180241 this . pendingTasks . clear ( ) ;
181- this . pendingEventCreates . clear ( ) ;
182- this . taskEventIdCache . clear ( ) ;
183- this . taskExceptionEventIdCache . clear ( ) ;
184242 this . calendarFingerprints = null ;
185243 }
186244
@@ -1235,7 +1293,12 @@ export class TaskCalendarSyncService {
12351293 * Get the Google Calendar event ID from the task's frontmatter
12361294 */
12371295 getTaskEventId ( task : TaskInfo ) : string | undefined {
1238- return task . googleCalendarEventId || this . taskEventIdCache . get ( task . path ) ;
1296+ return (
1297+ task . googleCalendarEventId ||
1298+ TaskCalendarSyncService . taskEventIdCache . get (
1299+ this . getTaskEventIdCacheKey ( task . path )
1300+ )
1301+ ) ;
12391302 }
12401303
12411304 /**
@@ -1244,7 +1307,9 @@ export class TaskCalendarSyncService {
12441307 getTaskExceptionEventId ( task : TaskInfo ) : string | undefined {
12451308 return (
12461309 task . googleCalendarExceptionEventId ||
1247- this . taskExceptionEventIdCache . get ( task . path )
1310+ TaskCalendarSyncService . taskExceptionEventIdCache . get (
1311+ this . getTaskEventIdCacheKey ( task . path )
1312+ )
12481313 ) ;
12491314 }
12501315
@@ -1296,7 +1361,11 @@ export class TaskCalendarSyncService {
12961361 /**
12971362 * Save the Google Calendar event ID to the task's frontmatter
12981363 */
1299- private async saveTaskEventId ( taskPath : string , eventId : string ) : Promise < void > {
1364+ private async saveTaskEventId (
1365+ taskPath : string ,
1366+ eventId : string ,
1367+ calendarId ?: string
1368+ ) : Promise < void > {
13001369 const file = this . plugin . app . vault . getAbstractFileByPath ( taskPath ) ;
13011370 if ( ! ( file instanceof TFile ) ) {
13021371 tasknotesLogger . warn ( `Cannot save event ID: file not found at ${ taskPath } ` , {
@@ -1310,9 +1379,13 @@ export class TaskCalendarSyncService {
13101379 await processVaultFrontMatter ( this . plugin . app , file , ( frontmatter ) => {
13111380 frontmatter [ fieldName ] = eventId ;
13121381 } ) ;
1313- this . taskEventIdCache . set ( taskPath , eventId ) ;
1382+ TaskCalendarSyncService . taskEventIdCache . set (
1383+ this . getTaskEventIdCacheKey ( taskPath , calendarId ) ,
1384+ eventId
1385+ ) ;
13141386
1315- const targetCalendarId = this . plugin . settings . googleCalendarExport . targetCalendarId ;
1387+ const targetCalendarId =
1388+ calendarId || this . plugin . settings . googleCalendarExport . targetCalendarId ;
13161389 if ( targetCalendarId ) {
13171390 await this . upsertEventIndex ( taskPath , targetCalendarId , eventId ) ;
13181391 }
@@ -1328,7 +1401,7 @@ export class TaskCalendarSyncService {
13281401 category : "provider" ,
13291402 operation : "remove-event-id-file-not-found" ,
13301403 } ) ;
1331- this . taskEventIdCache . delete ( taskPath ) ;
1404+ TaskCalendarSyncService . clearTaskEventIdCache ( taskPath ) ;
13321405 await this . removeEventIndexForTask ( taskPath ) ;
13331406 return ;
13341407 }
@@ -1337,7 +1410,7 @@ export class TaskCalendarSyncService {
13371410 await processVaultFrontMatter ( this . plugin . app , file , ( frontmatter ) => {
13381411 delete frontmatter [ fieldName ] ;
13391412 } ) ;
1340- this . taskEventIdCache . delete ( taskPath ) ;
1413+ TaskCalendarSyncService . clearTaskEventIdCache ( taskPath ) ;
13411414 await this . removeEventIndexForTask ( taskPath ) ;
13421415 }
13431416
@@ -1350,7 +1423,8 @@ export class TaskCalendarSyncService {
13501423 | "googleCalendarExceptionOriginalScheduled"
13511424 | "googleCalendarMovedOriginalDates"
13521425 >
1353- >
1426+ > ,
1427+ calendarId ?: string
13541428 ) : Promise < void > {
13551429 const file = this . plugin . app . vault . getAbstractFileByPath ( taskPath ) ;
13561430 if ( ! ( file instanceof TFile ) ) {
@@ -1394,12 +1468,12 @@ export class TaskCalendarSyncService {
13941468
13951469 if ( "googleCalendarExceptionEventId" in updates ) {
13961470 if ( updates . googleCalendarExceptionEventId ) {
1397- this . taskExceptionEventIdCache . set (
1398- taskPath ,
1471+ TaskCalendarSyncService . taskExceptionEventIdCache . set (
1472+ this . getTaskEventIdCacheKey ( taskPath , calendarId ) ,
13991473 updates . googleCalendarExceptionEventId
14001474 ) ;
14011475 } else {
1402- this . taskExceptionEventIdCache . delete ( taskPath ) ;
1476+ TaskCalendarSyncService . clearTaskExceptionEventIdCache ( taskPath , calendarId ) ;
14031477 }
14041478 }
14051479 }
@@ -2017,7 +2091,7 @@ export class TaskCalendarSyncService {
20172091 ? createdEvent . id . slice ( prefix . length )
20182092 : createdEvent . id ;
20192093
2020- await this . saveTaskEventId ( task . path , eventId ) ;
2094+ await this . saveTaskEventId ( task . path , eventId , calendarId ) ;
20212095 return eventId ;
20222096 }
20232097
@@ -2111,10 +2185,14 @@ export class TaskCalendarSyncService {
21112185 }
21122186 }
21132187
2114- await this . saveTaskExceptionMetadata ( task . path , {
2115- googleCalendarExceptionEventId : undefined ,
2116- googleCalendarExceptionOriginalScheduled : undefined ,
2117- } ) ;
2188+ await this . saveTaskExceptionMetadata (
2189+ task . path ,
2190+ {
2191+ googleCalendarExceptionEventId : undefined ,
2192+ googleCalendarExceptionOriginalScheduled : undefined ,
2193+ } ,
2194+ targetCalendarId
2195+ ) ;
21182196 return ;
21192197 }
21202198
@@ -2137,29 +2215,64 @@ export class TaskCalendarSyncService {
21372215 if ( getErrorStatus ( error ) !== 404 ) {
21382216 throw error ;
21392217 }
2140- await this . saveTaskExceptionMetadata ( task . path , {
2141- googleCalendarExceptionEventId : undefined ,
2142- } ) ;
2218+ await this . saveTaskExceptionMetadata (
2219+ task . path ,
2220+ {
2221+ googleCalendarExceptionEventId : undefined ,
2222+ } ,
2223+ targetCalendarId
2224+ ) ;
21432225 }
21442226 }
21452227
2146- const createdEvent = await this . withGoogleRateLimit ( ( ) =>
2228+ const createCacheKey = this . getTaskEventIdCacheKey ( task . path , targetCalendarId ) ;
2229+ const pendingCreate =
2230+ TaskCalendarSyncService . pendingExceptionEventCreates . get ( createCacheKey ) ;
2231+ if ( pendingCreate ) {
2232+ const eventId = await pendingCreate ;
2233+ await this . withGoogleRateLimit ( ( ) =>
2234+ this . googleCalendarService . updateEvent ( targetCalendarId , eventId , eventData )
2235+ ) ;
2236+ return ;
2237+ }
2238+
2239+ const createPromise = this . withGoogleRateLimit ( ( ) =>
21472240 this . googleCalendarService . createEvent ( targetCalendarId , {
21482241 ...eventData ,
21492242 isAllDay : ! ! eventData . start . date ,
21502243 } )
2151- ) ;
2152- const prefix = `google-${ targetCalendarId } -` ;
2153- const eventId = createdEvent . id . startsWith ( prefix )
2154- ? createdEvent . id . slice ( prefix . length )
2155- : createdEvent . id ;
2244+ ) . then ( async ( createdEvent ) => {
2245+ const prefix = `google-${ targetCalendarId } -` ;
2246+ const eventId = createdEvent . id . startsWith ( prefix )
2247+ ? createdEvent . id . slice ( prefix . length )
2248+ : createdEvent . id ;
21562249
2157- await this . saveTaskExceptionMetadata ( task . path , {
2158- googleCalendarExceptionEventId : eventId ,
2159- googleCalendarExceptionOriginalScheduled : getDatePart (
2160- task . googleCalendarExceptionOriginalScheduled || ""
2161- ) ,
2250+ await this . saveTaskExceptionMetadata (
2251+ task . path ,
2252+ {
2253+ googleCalendarExceptionEventId : eventId ,
2254+ googleCalendarExceptionOriginalScheduled : getDatePart (
2255+ task . googleCalendarExceptionOriginalScheduled || ""
2256+ ) ,
2257+ } ,
2258+ targetCalendarId
2259+ ) ;
2260+ return eventId ;
21622261 } ) ;
2262+ TaskCalendarSyncService . pendingExceptionEventCreates . set (
2263+ createCacheKey ,
2264+ createPromise
2265+ ) ;
2266+ try {
2267+ await createPromise ;
2268+ } finally {
2269+ if (
2270+ TaskCalendarSyncService . pendingExceptionEventCreates . get ( createCacheKey ) ===
2271+ createPromise
2272+ ) {
2273+ TaskCalendarSyncService . pendingExceptionEventCreates . delete ( createCacheKey ) ;
2274+ }
2275+ }
21632276 }
21642277
21652278 /**
@@ -2232,7 +2345,12 @@ export class TaskCalendarSyncService {
22322345 )
22332346 ) ;
22342347 } else {
2235- const pendingCreate = this . pendingEventCreates . get ( task . path ) ;
2348+ const createCacheKey = this . getTaskEventIdCacheKey (
2349+ task . path ,
2350+ targetCalendarId
2351+ ) ;
2352+ const pendingCreate =
2353+ TaskCalendarSyncService . pendingEventCreates . get ( createCacheKey ) ;
22362354 if ( pendingCreate ) {
22372355 const eventId = await pendingCreate ;
22382356 await this . withGoogleRateLimit ( ( ) =>
@@ -2244,12 +2362,18 @@ export class TaskCalendarSyncService {
22442362 eventData ,
22452363 targetCalendarId
22462364 ) ;
2247- this . pendingEventCreates . set ( task . path , createPromise ) ;
2365+ TaskCalendarSyncService . pendingEventCreates . set (
2366+ createCacheKey ,
2367+ createPromise
2368+ ) ;
22482369 try {
22492370 await createPromise ;
22502371 } finally {
2251- if ( this . pendingEventCreates . get ( task . path ) === createPromise ) {
2252- this . pendingEventCreates . delete ( task . path ) ;
2372+ if (
2373+ TaskCalendarSyncService . pendingEventCreates . get ( createCacheKey ) ===
2374+ createPromise
2375+ ) {
2376+ TaskCalendarSyncService . pendingEventCreates . delete ( createCacheKey ) ;
22532377 }
22542378 }
22552379 }
0 commit comments