22using System . Collections . Generic ;
33using System . Linq ;
44using System . Net . Http ;
5+ using System . Threading ;
56using System . Threading . Tasks ;
67using ClockifyClient ;
78using ClockifyClient . Models ;
@@ -13,6 +14,8 @@ public class ClockifyService(Logger logger)
1314{
1415 private const int MaxPageSize = 5000 ;
1516
17+ private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim ( 1 , 1 ) ;
18+
1619 private PluginSettings _settings = new ( ) ;
1720
1821 private ClockifyApiClient _clockifyClient ;
@@ -21,7 +24,6 @@ public class ClockifyService(Logger logger)
2124 private ProjectDtoV1 _project = new ( ) ;
2225 private List < string > _tags = [ ] ;
2326 private TaskDtoV1 _task = new ( ) ;
24- private ClientWithCurrencyDtoV1 _client = new ( ) ;
2527
2628 public bool IsValid => _clockifyClient is not null
2729 && ! string . IsNullOrWhiteSpace ( _settings . WorkspaceName )
@@ -30,116 +32,140 @@ public class ClockifyService(Logger logger)
3032 public async Task < bool > ToggleTimerAsync ( )
3133 {
3234 logger . LogInfo ( "Toggling timer..." ) ;
33-
34- if ( ! IsValid )
35+
36+ await _cacheLock . WaitAsync ( ) ;
37+ try
3538 {
36- logger . LogError ( $ "Toggling trimer failed, invalid settings: { _settings } ") ;
37- return false ;
38- }
39+ if ( ! IsValid )
40+ {
41+ logger . LogError ( $ "Toggling trimer failed, invalid settings: { _settings } ") ;
42+ return false ;
43+ }
3944
40- var runningTimer = await StopRunningTimerAsync ( ) ;
45+ var runningTimer = await StopRunningTimerAsync ( ) ;
4146
42- if ( runningTimer is not null )
43- {
44- logger . LogInfo ( "Toggling trimer successful, timer has been stopped" ) ;
45- return true ;
46- }
47+ if ( runningTimer is not null )
48+ {
49+ logger . LogInfo ( "Toggling trimer successful, timer has been stopped" ) ;
50+ return true ;
51+ }
4752
48- try
49- {
50- var timeEntryRequest = await CreateTimeEntryRequestAsync ( ) ;
51- await _clockifyClient . V1 . Workspaces [ _workspace . Id ] . TimeEntries . PostAsync ( timeEntryRequest ) ;
52-
53- logger . LogInfo ( "Toggling trimer successful, timer has been started" ) ;
54- return true ;
53+ try
54+ {
55+ var timeEntryRequest = await CreateTimeEntryRequestAsync ( ) ;
56+ await _clockifyClient . V1 . Workspaces [ _workspace . Id ] . TimeEntries . PostAsync ( timeEntryRequest ) ;
57+
58+ logger . LogInfo ( "Toggling trimer successful, timer has been started" ) ;
59+ return true ;
60+ }
61+ catch ( Exception exception ) when ( exception is ApiException or HttpRequestException )
62+ {
63+ logger . LogError ( $ "Toggling trimer failed, TimeEntry creation failed: { exception . Message } ") ;
64+ return false ;
65+ }
5566 }
56- catch ( Exception exception ) when ( exception is ApiException or HttpRequestException )
67+ finally
5768 {
58- logger . LogError ( $ "Toggling trimer failed, TimeEntry creation failed: { exception . Message } ") ;
59- return false ;
69+ _cacheLock . Release ( ) ;
6070 }
6171 }
6272
6373 public async Task < TimeEntryWithRatesDtoV1 > GetRunningTimerAsync ( )
6474 {
6575 logger . LogInfo ( "Fetching running timer..." ) ;
6676
67- if ( ! IsValid )
68- {
69- logger . LogError ( $ "Fetching running timer failed, invalid settings: { _settings } ") ;
70- return null ;
71- }
72-
77+ await _cacheLock . WaitAsync ( ) ;
7378 try
7479 {
75- var timeEntries = await _clockifyClient . V1 . Workspaces [ _workspace . Id ] . User [ _currentUser . Id ] . TimeEntries
76- . GetAsync ( p => p . QueryParameters . InProgress = true ) ;
77-
78- if ( string . IsNullOrEmpty ( _settings . ProjectName ) )
80+ if ( ! IsValid )
81+ {
82+ logger . LogError ( $ "Fetching running timer failed, invalid settings: { _settings } ") ;
83+ return null ;
84+ }
85+
86+ try
7987 {
80- return timeEntries ? . FirstOrDefault ( t => string . IsNullOrEmpty ( _settings . TimerName ) || t . Description == _settings . TimerName ) ;
88+ var timeEntries = await _clockifyClient . V1 . Workspaces [ _workspace . Id ] . User [ _currentUser . Id ] . TimeEntries
89+ . GetAsync ( p => p . QueryParameters . InProgress = true ) ;
90+
91+ if ( string . IsNullOrEmpty ( _settings . ProjectName ) )
92+ {
93+ return timeEntries ? . FirstOrDefault ( t => string . IsNullOrEmpty ( _settings . TimerName ) || t . Description == _settings . TimerName ) ;
94+ }
95+
96+ if ( _project is null )
97+ {
98+ logger . LogError ( $ "Fetching running timer failed, no project in workspace matching { _settings . ProjectName } ") ;
99+ return null ;
100+ }
101+
102+ return timeEntries ? . FirstOrDefault ( t => t . ProjectId == _project . Id
103+ && ( string . IsNullOrEmpty ( _settings . TimerName ) || t . Description == _settings . TimerName )
104+ && ( string . IsNullOrEmpty ( _settings . TaskName ) || string . IsNullOrEmpty ( _task ? . Id ) || t . TaskId == _task . Id )
105+ && ( ( t . TagIds is null && _tags is null ) || ( t . TagIds is not null && _tags is not null && t . TagIds . OrderBy ( s => s , StringComparer . InvariantCulture )
106+ . SequenceEqual ( _tags . OrderBy ( s => s , StringComparer . InvariantCulture ) ) ) )
107+ && t . Billable == _settings . Billable ) ;
81108 }
82-
83- if ( _project is null )
109+ catch ( Exception exception ) when ( exception is ApiException or HttpRequestException )
84110 {
85- logger . LogError ( $ "Fetching running timer failed, no project in workspace matching { _settings . ProjectName } ") ;
111+ logger . LogError ( $ "Fetching running timer failed, TimeEntry request failed: { exception . Message } ") ;
86112 return null ;
87113 }
88-
89- return timeEntries ? . FirstOrDefault ( t => t . ProjectId == _project . Id
90- && ( string . IsNullOrEmpty ( _settings . TimerName ) || t . Description == _settings . TimerName )
91- && ( string . IsNullOrEmpty ( _settings . TaskName ) || string . IsNullOrEmpty ( _task ? . Id ) || t . TaskId == _task . Id )
92- && ( ( t . TagIds is null && _tags is null ) || t . TagIds is not null && _tags is not null && t . TagIds . OrderBy ( s => s , StringComparer . InvariantCulture )
93- . SequenceEqual ( _tags . OrderBy ( s => s , StringComparer . InvariantCulture ) ) )
94- && t . Billable == _settings . Billable ) ;
95114 }
96- catch ( Exception exception ) when ( exception is ApiException or HttpRequestException )
115+ finally
97116 {
98- logger . LogError ( $ "Fetching running timer failed, TimeEntry request failed: { exception . Message } ") ;
99- return null ;
117+ _cacheLock . Release ( ) ;
100118 }
101119 }
102120
103121 public async Task UpdateSettingsAsync ( PluginSettings settings )
104122 {
105123 logger . LogInfo ( "Updating settings..." ) ;
106124
107- SettingsValidator . MigrateServerUrl ( settings ) ;
108-
109- var cacheInvalidationRequired = SettingsValidator . HasChanged ( _settings , settings ) ;
110-
111- // Do we need to recreate the client?
112- if ( ! IsValid || _settings . ApiKey != settings . ApiKey || _settings . ServerUrl != settings . ServerUrl )
125+ await _cacheLock . WaitAsync ( ) ;
126+ try
113127 {
114- logger . LogInfo ( "Updating settings, recreate Clockify client" ) ;
115-
116- var validation = SettingsValidator . Validate ( settings ) ;
128+ SettingsValidator . MigrateServerUrl ( settings ) ;
129+ var cacheInvalidationRequired = SettingsValidator . HasChanged ( _settings , settings ) ;
117130
118- if ( ! validation . IsValid )
131+
132+ // Do we need to recreate the client?
133+ if ( ! IsValid || _settings . ApiKey != settings . ApiKey || _settings . ServerUrl != settings . ServerUrl )
119134 {
120- logger . LogError ( $ "Updating settings failed, settings validation failed: { validation . Error } ") ;
121- return ;
135+ logger . LogInfo ( "Updating settings, recreate Clockify client" ) ;
136+
137+ var validation = SettingsValidator . Validate ( settings ) ;
138+
139+ if ( ! validation . IsValid )
140+ {
141+ logger . LogError ( $ "Updating settings failed, settings validation failed: { validation . Error } ") ;
142+ return ;
143+ }
144+
145+ _clockifyClient = ClockifyApiClientFactory . Create ( settings . ApiKey , settings . ServerUrl ) ;
146+
147+ if ( ! await TestConnectionAsync ( ) )
148+ {
149+ logger . LogError ( "Updating settings failed, invalid server URL or API key" ) ;
150+ _clockifyClient = null ;
151+ _currentUser = new UserDtoV1 ( ) ;
152+ return ;
153+ }
154+
155+ logger . LogInfo ( "Updating settings successful, connection to Clockify established" ) ;
156+ cacheInvalidationRequired = true ;
122157 }
123158
124- _clockifyClient = ClockifyApiClientFactory . Create ( settings . ApiKey , settings . ServerUrl ) ;
159+ _settings = new PluginSettings ( settings ) ;
125160
126- if ( ! await TestConnectionAsync ( ) )
161+ if ( cacheInvalidationRequired )
127162 {
128- logger . LogError ( "Updating settings failed, invalid server URL or API key" ) ;
129- _clockifyClient = null ;
130- _currentUser = new UserDtoV1 ( ) ;
131- return ;
163+ await ReloadCacheAsync ( ) ;
132164 }
133-
134- logger . LogInfo ( "Updating settings successful, connection to Clockify established" ) ;
135- cacheInvalidationRequired = true ;
136165 }
137-
138- _settings = settings ;
139-
140- if ( cacheInvalidationRequired )
166+ finally
141167 {
142- await ReloadCacheAsync ( ) ;
168+ _cacheLock . Release ( ) ;
143169 }
144170 }
145171
@@ -180,22 +206,21 @@ private async Task ReloadCacheAsync()
180206 _project = null ;
181207 _tags = [ ] ;
182208 _task = null ;
183- _client = null ;
184209
185210 try
186211 {
187212 var workspaces = await _clockifyClient . V1 . Workspaces . GetAsync ( ) ;
188213 _workspace = workspaces ? . SingleOrDefault ( w => w . Name == _settings . WorkspaceName ) ;
189214
190- if ( _workspace != null )
215+ if ( _workspace is not null )
191216 {
192- _project = await FindMatchingProjectAsync ( _workspace . Id , _settings . ProjectName ) ;
217+ var client = await FindMatchingClientAsync ( _workspace . Id , _settings . ClientName ) ;
218+ _project = await FindMatchingProjectAsync ( _workspace . Id , _settings . ProjectName , client ? . Id ) ;
193219 _tags = await FindMatchingTagsAsync ( _workspace . Id , _settings . Tags ) ;
194220
195- if ( _project != null )
221+ if ( _project is not null )
196222 {
197223 _task = await FindMatchingTaskAsync ( _workspace . Id , _project . Id , _settings . TaskName ) ;
198- _client = await FindMatchingClientAsync ( _workspace . Id , _settings . ClientName ) ;
199224 }
200225 }
201226
@@ -216,7 +241,7 @@ private async Task<TimeEntryWithRatesDtoV1> StopRunningTimerAsync()
216241 }
217242
218243 var runningTimer = await GetRunningTimerAsync ( ) ;
219- if ( runningTimer == null )
244+ if ( runningTimer is null )
220245 {
221246 // No running timer
222247 return null ;
@@ -246,13 +271,15 @@ private async Task<TimeEntryWithRatesDtoV1> StopRunningTimerAsync()
246271 return runningTimer ;
247272 }
248273
249- private async Task < ProjectDtoV1 > FindMatchingProjectAsync ( string workspaceId , string projectName )
274+ private async Task < ProjectDtoV1 > FindMatchingProjectAsync ( string workspaceId , string projectName , string clientId = null )
250275 {
251276 if ( string . IsNullOrEmpty ( projectName ) )
252277 {
253278 return null ;
254279 }
255280
281+ logger . LogInfo ( "Finding matching project..." ) ;
282+
256283 try
257284 {
258285 var projects = await _clockifyClient . V1 . Workspaces [ workspaceId ] . Projects
@@ -262,9 +289,9 @@ private async Task<ProjectDtoV1> FindMatchingProjectAsync(string workspaceId, st
262289 q . QueryParameters . StrictNameSearch = true ;
263290 q . QueryParameters . PageSize = MaxPageSize ;
264291
265- if ( _client is not null )
292+ if ( clientId is not null )
266293 {
267- q . QueryParameters . Clients = [ _client . Id ] ;
294+ q . QueryParameters . Clients = [ clientId ] ;
268295 }
269296 } ) ;
270297
@@ -395,7 +422,7 @@ private async Task<List<string>> FindMatchingTagsAsync(string workspaceId, strin
395422 var tagsOnWorkspace = await _clockifyClient . V1 . Workspaces [ workspaceId ] . Tags
396423 . GetAsync ( q => q . QueryParameters . PageSize = MaxPageSize ) ;
397424
398- return tagsOnWorkspace == null ? [ ] : tagsOnWorkspace . Where ( t => tagList . Contains ( t . Name ) ) . Select ( t => t . Id ) . ToList ( ) ;
425+ return tagsOnWorkspace is null ? [ ] : tagsOnWorkspace . Where ( t => tagList . Contains ( t . Name ) ) . Select ( t => t . Id ) . ToList ( ) ;
399426 }
400427 catch ( Exception exception ) when ( exception is ApiException or HttpRequestException )
401428 {
0 commit comments