@timesheet/plugin-google-calendar 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,7 @@ export declare class GoogleCalendarClient {
21
21
  expiration?: string;
22
22
  }>;
23
23
  stopWatch(channelId: string, resourceId: string): Promise<void>;
24
+ private static readonly REQUEST_TIMEOUT_MS;
24
25
  private request;
25
26
  private buildUrl;
26
27
  }
@@ -64,14 +64,28 @@ class GoogleCalendarClient {
64
64
  }
65
65
  async stopWatch(channelId, resourceId) {
66
66
  const token = await this.getAccessToken();
67
- const response = await fetch('https://www.googleapis.com/calendar/v3/channels/stop', {
68
- method: 'POST',
69
- headers: {
70
- Authorization: `Bearer ${token}`,
71
- 'Content-Type': 'application/json'
72
- },
73
- body: JSON.stringify({ id: channelId, resourceId })
74
- });
67
+ const controller = new AbortController();
68
+ const timeoutId = setTimeout(() => controller.abort(), GoogleCalendarClient.REQUEST_TIMEOUT_MS);
69
+ let response;
70
+ try {
71
+ response = await fetch('https://www.googleapis.com/calendar/v3/channels/stop', {
72
+ method: 'POST',
73
+ headers: {
74
+ Authorization: `Bearer ${token}`,
75
+ 'Content-Type': 'application/json'
76
+ },
77
+ body: JSON.stringify({ id: channelId, resourceId }),
78
+ signal: controller.signal
79
+ });
80
+ }
81
+ catch (error) {
82
+ clearTimeout(timeoutId);
83
+ if (error instanceof DOMException && error.name === 'AbortError') {
84
+ throw new Error(`Google Calendar API channels/stop timed out after ${GoogleCalendarClient.REQUEST_TIMEOUT_MS}ms`);
85
+ }
86
+ throw error;
87
+ }
88
+ clearTimeout(timeoutId);
75
89
  // 404 means channel already expired — not an error
76
90
  if (!response.ok && response.status !== 404) {
77
91
  const errorText = await response.text();
@@ -80,15 +94,29 @@ class GoogleCalendarClient {
80
94
  }
81
95
  async request(method, path, query, body, retried = false) {
82
96
  const token = await this.getAccessToken();
83
- const response = await fetch(this.buildUrl(path, query), {
84
- method,
85
- headers: {
86
- Authorization: `Bearer ${token}`,
87
- Accept: 'application/json',
88
- 'Content-Type': 'application/json'
89
- },
90
- body: body !== undefined ? JSON.stringify(body) : undefined
91
- });
97
+ const controller = new AbortController();
98
+ const timeoutId = setTimeout(() => controller.abort(), GoogleCalendarClient.REQUEST_TIMEOUT_MS);
99
+ let response;
100
+ try {
101
+ response = await fetch(this.buildUrl(path, query), {
102
+ method,
103
+ headers: {
104
+ Authorization: `Bearer ${token}`,
105
+ Accept: 'application/json',
106
+ 'Content-Type': 'application/json'
107
+ },
108
+ body: body !== undefined ? JSON.stringify(body) : undefined,
109
+ signal: controller.signal
110
+ });
111
+ }
112
+ catch (error) {
113
+ clearTimeout(timeoutId);
114
+ if (error instanceof DOMException && error.name === 'AbortError') {
115
+ throw new Error(`Google Calendar API ${method} ${path} timed out after ${GoogleCalendarClient.REQUEST_TIMEOUT_MS}ms`);
116
+ }
117
+ throw error;
118
+ }
119
+ clearTimeout(timeoutId);
92
120
  if (response.status === 401 && !retried) {
93
121
  const refreshed = await this.refreshAccessToken();
94
122
  if (refreshed) {
@@ -118,3 +146,4 @@ class GoogleCalendarClient {
118
146
  }
119
147
  }
120
148
  exports.GoogleCalendarClient = GoogleCalendarClient;
149
+ GoogleCalendarClient.REQUEST_TIMEOUT_MS = 30000;
@@ -194,6 +194,21 @@ async function syncCalendar(context, projectMapping) {
194
194
  if (!syncToken && metadataSyncToken) {
195
195
  syncToken = metadataSyncToken;
196
196
  }
197
+ try {
198
+ return await fetchAndSyncEvents(context, client, projectMapping, calendarId, syncStateKey, syncToken);
199
+ }
200
+ catch (err) {
201
+ // Google returns 400 or 410 when a sync token is invalid/expired — clear it and do a full resync
202
+ const errMsg = String(err);
203
+ if (syncToken && (errMsg.includes('(410)') || errMsg.includes('Invalid sync token'))) {
204
+ context.logger.warn('Sync token expired, performing full resync', { calendarId });
205
+ await context.state.delete(syncStateKey);
206
+ return await fetchAndSyncEvents(context, client, projectMapping, calendarId, syncStateKey, undefined);
207
+ }
208
+ throw err;
209
+ }
210
+ }
211
+ async function fetchAndSyncEvents(context, client, projectMapping, calendarId, syncStateKey, syncToken) {
197
212
  let pageToken;
198
213
  let nextSyncToken;
199
214
  let syncedCount = 0;
@@ -411,11 +426,7 @@ function getHeader(input, name) {
411
426
  return value === undefined || value === null ? undefined : String(value);
412
427
  }
413
428
  async function ensureWatchChannels(context) {
414
- // The webhook URL is passed via the context metadata (set by the backend runtime)
415
- const contextAny = context;
416
- const metadata = contextAny['metadata'];
417
- const webhooks = metadata?.['webhooks'];
418
- const webhookUrl = webhooks?.['integration-webhook'];
429
+ const webhookUrl = context.metadata?.webhooks?.['integration-webhook'];
419
430
  if (!webhookUrl) {
420
431
  context.logger.info('No webhook URL available — skipping watch channel registration');
421
432
  return;
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "google-calendar-sync",
3
3
  "name": "Google Calendar Sync",
4
- "version": "1.1.0",
4
+ "version": "1.2.1",
5
5
  "description": "Synchronize Timesheet tasks with Google Calendar",
6
6
  "longDescription": "Bidirectional synchronization between Timesheet tasks and Google Calendar events.",
7
7
  "icon": "google-calendar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timesheet/plugin-google-calendar",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [
@@ -13,6 +13,6 @@
13
13
  "prepublishOnly": "npm run build"
14
14
  },
15
15
  "dependencies": {
16
- "@timesheet/integration-sdk": "^0.2.0"
16
+ "@timesheet/integration-sdk": "^0.3.0"
17
17
  }
18
18
  }