@kubun/connector-google-calendar 0.17.4 → 0.18.0
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.
- package/lib/actions.d.ts +1 -1
- package/lib/actions.js +15 -6
- package/lib/api.d.ts +43 -1
- package/lib/api.js +166 -16
- package/lib/connector.js +22 -2
- package/lib/intent.d.ts +4 -0
- package/lib/intent.js +23 -0
- package/lib/model.d.ts +8 -53
- package/lib/model.js +37 -74
- package/lib/provider.d.ts +14 -0
- package/lib/provider.js +399 -135
- package/package.json +12 -11
package/lib/provider.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { ConnectorError, fetchOrAbsent, parseMaxAgeDays, resolveModelID } from '@kubun/connector';
|
|
1
|
+
import { addBoundaryDuration, ConnectorError, fetchOrAbsent, parseMaxAgeDays, resolveModelID } from '@kubun/connector';
|
|
2
2
|
import { deriveAccountKey } from './account-key.js';
|
|
3
|
-
import { buildCalendarListURL, buildEventsListURL, calendarAPIClient, getCalendarListEntry, getEvent, getEventWithETag, mapCalendarToEntity, mapEventToEntity, splitEventSourceID } from './api.js';
|
|
3
|
+
import { buildCalendarListURL, buildEventsListURL, calendarAPIClient, fetchEventResource, getCalendarListEntry, getEvent, getEventWithETag, mapCalendarToEntity, mapEventToEntity, splitEventSourceID } from './api.js';
|
|
4
4
|
import { googleCalendarClusters } from './model.js';
|
|
5
5
|
const CALENDAR_MODEL_ID = resolveModelID(googleCalendarClusters, 'GoogleCalendar');
|
|
6
6
|
const CALENDAR_EVENT_MODEL_ID = resolveModelID(googleCalendarClusters, 'GoogleCalendarEvent');
|
|
7
|
+
// How often the forward window rolls by a tokenless full event pass.
|
|
8
|
+
const WINDOW_ROLL_INTERVAL = 'P30D';
|
|
7
9
|
function computeTimeMin(boundary) {
|
|
8
10
|
const days = parseMaxAgeDays(boundary);
|
|
9
11
|
if (days == null) return undefined;
|
|
@@ -11,6 +13,56 @@ function computeTimeMin(boundary) {
|
|
|
11
13
|
date.setDate(date.getDate() - days);
|
|
12
14
|
return date.toISOString();
|
|
13
15
|
}
|
|
16
|
+
// The window of a tokenless full event pass starting now. The lower bound stays
|
|
17
|
+
// frozen once set; the upper bound is recomputed from `maxAhead` but never moves
|
|
18
|
+
// backwards, so a replay from an older checkpoint lists the same events or a
|
|
19
|
+
// superset.
|
|
20
|
+
function fullPassWindow(boundary, prior) {
|
|
21
|
+
const now = new Date();
|
|
22
|
+
const windowStart = prior?.windowStart ?? computeTimeMin(boundary);
|
|
23
|
+
const fresh = boundary.maxAhead != null ? addBoundaryDuration(now, boundary.maxAhead) : null;
|
|
24
|
+
if (fresh == null) {
|
|
25
|
+
return {
|
|
26
|
+
windowStart
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const carried = prior?.windowEnd != null ? new Date(prior.windowEnd) : null;
|
|
30
|
+
const windowEnd = carried != null && carried > fresh ? carried : fresh;
|
|
31
|
+
return {
|
|
32
|
+
windowStart,
|
|
33
|
+
windowEnd: windowEnd.toISOString(),
|
|
34
|
+
windowRolledAt: now.toISOString()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function isRollDue(boundary, checkpoint) {
|
|
38
|
+
if (boundary.maxAhead == null) return false;
|
|
39
|
+
if (checkpoint.windowEnd == null || checkpoint.windowRolledAt == null) return true;
|
|
40
|
+
const dueAt = addBoundaryDuration(new Date(checkpoint.windowRolledAt), WINDOW_ROLL_INTERVAL);
|
|
41
|
+
return dueAt == null || Date.now() >= dueAt.getTime();
|
|
42
|
+
}
|
|
43
|
+
function windowFields(window) {
|
|
44
|
+
return {
|
|
45
|
+
...window.windowStart != null ? {
|
|
46
|
+
windowStart: window.windowStart
|
|
47
|
+
} : {},
|
|
48
|
+
...window.windowEnd != null ? {
|
|
49
|
+
windowEnd: window.windowEnd
|
|
50
|
+
} : {},
|
|
51
|
+
...window.windowRolledAt != null ? {
|
|
52
|
+
windowRolledAt: window.windowRolledAt
|
|
53
|
+
} : {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Google's `timeMax` is an exclusive bound on the start, applied here to
|
|
57
|
+
// listings that could not send it.
|
|
58
|
+
function startsBefore(windowEnd) {
|
|
59
|
+
if (windowEnd == null) return ()=>true;
|
|
60
|
+
const bound = new Date(windowEnd).getTime();
|
|
61
|
+
return (event)=>{
|
|
62
|
+
const start = event.start.dateTime ?? event.start.date;
|
|
63
|
+
return start == null || new Date(start).getTime() < bound;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
14
66
|
async function* fetchAllCalendars(params) {
|
|
15
67
|
let pageToken;
|
|
16
68
|
let isFirstPage = true;
|
|
@@ -32,21 +84,24 @@ async function* fetchAllCalendars(params) {
|
|
|
32
84
|
}while (pageToken != null)
|
|
33
85
|
}
|
|
34
86
|
async function* fetchEventsForCalendar(params) {
|
|
35
|
-
let pageToken;
|
|
87
|
+
let pageToken = params.pageToken;
|
|
36
88
|
let isFirstPage = true;
|
|
37
89
|
do {
|
|
38
90
|
const url = buildEventsListURL({
|
|
39
91
|
calendarID: params.calendarID,
|
|
40
92
|
syncToken: isFirstPage ? params.syncToken : undefined,
|
|
41
93
|
pageToken,
|
|
42
|
-
timeMin: params.timeMin
|
|
94
|
+
timeMin: params.timeMin,
|
|
95
|
+
timeMax: params.timeMax,
|
|
96
|
+
showDeleted: params.showDeleted
|
|
43
97
|
});
|
|
44
98
|
const data = await params.client.get(url, {
|
|
45
99
|
signal: params.signal
|
|
46
100
|
});
|
|
47
101
|
yield {
|
|
48
102
|
items: data.items ?? [],
|
|
49
|
-
nextSyncToken: data.nextSyncToken
|
|
103
|
+
nextSyncToken: data.nextSyncToken,
|
|
104
|
+
nextPageToken: data.nextPageToken
|
|
50
105
|
};
|
|
51
106
|
pageToken = data.nextPageToken;
|
|
52
107
|
isFirstPage = false;
|
|
@@ -62,12 +117,11 @@ export function createGoogleCalendarProvider(config) {
|
|
|
62
117
|
// providerAccount capture.
|
|
63
118
|
const accountKey = config.credential.providerAccount != null ? deriveAccountKey(config.credential.providerAccount) : undefined;
|
|
64
119
|
async function* fetchAllEntities(params) {
|
|
65
|
-
// Reuse a window frozen by a prior sync
|
|
66
|
-
//
|
|
67
|
-
// `now`; only a genuine first sync computes it fresh.
|
|
120
|
+
// Reuse a window frozen by a prior sync when an initial pass is re-driven;
|
|
121
|
+
// only a genuine first sync computes it fresh.
|
|
68
122
|
const prior = params.checkpoint;
|
|
69
|
-
const
|
|
70
|
-
const
|
|
123
|
+
const window = fullPassWindow(params.boundary, prior);
|
|
124
|
+
const inWindow = startsBefore(window.windowEnd);
|
|
71
125
|
// Fetch all calendars (collect all pages first, then yield single batch)
|
|
72
126
|
const allCalendars = [];
|
|
73
127
|
let calendarListSyncToken;
|
|
@@ -84,7 +138,7 @@ export function createGoogleCalendarProvider(config) {
|
|
|
84
138
|
calendarListSyncToken = page.nextSyncToken;
|
|
85
139
|
}
|
|
86
140
|
}
|
|
87
|
-
//
|
|
141
|
+
// Event batches checkpoint each completed calendar after this list batch.
|
|
88
142
|
const calendarListToken = mintFetchHLC();
|
|
89
143
|
yield {
|
|
90
144
|
modelID: CALENDAR_MODEL_ID,
|
|
@@ -96,23 +150,31 @@ export function createGoogleCalendarProvider(config) {
|
|
|
96
150
|
hasMore: true
|
|
97
151
|
};
|
|
98
152
|
// Fetch events per calendar
|
|
99
|
-
const calendarTokens =
|
|
153
|
+
const calendarTokens = Object.fromEntries(allCalendars.map((calendar)=>[
|
|
154
|
+
calendar.id,
|
|
155
|
+
{}
|
|
156
|
+
]));
|
|
100
157
|
let eventBatchYielded = false;
|
|
101
158
|
for (const [i, calendar] of allCalendars.entries()){
|
|
102
159
|
const isLastCalendar = i === allCalendars.length - 1;
|
|
103
160
|
let eventsSyncToken;
|
|
161
|
+
const relistID = mintFetchHLC();
|
|
162
|
+
const seen = new Set();
|
|
104
163
|
for await (const page of fetchEventsForCalendar({
|
|
105
164
|
calendarID: calendar.id,
|
|
106
165
|
client,
|
|
107
|
-
timeMin,
|
|
166
|
+
timeMin: window.windowStart,
|
|
167
|
+
timeMax: window.windowEnd,
|
|
168
|
+
showDeleted: true,
|
|
108
169
|
signal: params.signal
|
|
109
170
|
})){
|
|
171
|
+
for (const event of page.items)seen.add(`${calendar.id}/${event.id}`);
|
|
110
172
|
if (page.nextSyncToken != null) {
|
|
111
173
|
eventsSyncToken = page.nextSyncToken;
|
|
112
174
|
}
|
|
113
175
|
const isLastPage = page.nextSyncToken != null // last page has nextSyncToken
|
|
114
176
|
;
|
|
115
|
-
const checkpoint =
|
|
177
|
+
const checkpoint = isLastPage ? {
|
|
116
178
|
calendarListSyncToken,
|
|
117
179
|
calendars: {
|
|
118
180
|
...calendarTokens,
|
|
@@ -120,12 +182,10 @@ export function createGoogleCalendarProvider(config) {
|
|
|
120
182
|
eventsSyncToken: page.nextSyncToken
|
|
121
183
|
}
|
|
122
184
|
},
|
|
123
|
-
...
|
|
124
|
-
windowStart
|
|
125
|
-
} : {}
|
|
185
|
+
...windowFields(window)
|
|
126
186
|
} : undefined;
|
|
127
187
|
// Split items: cancelled → deleted, others → entities
|
|
128
|
-
const activeItems = page.items.filter((e)=>e.status !== 'cancelled');
|
|
188
|
+
const activeItems = page.items.filter((e)=>e.status !== 'cancelled' && inWindow(e));
|
|
129
189
|
const cancelledItems = page.items.filter((e)=>e.status === 'cancelled');
|
|
130
190
|
const pageToken = mintFetchHLC();
|
|
131
191
|
yield {
|
|
@@ -143,7 +203,28 @@ export function createGoogleCalendarProvider(config) {
|
|
|
143
203
|
sourceFetchHLC: pageToken
|
|
144
204
|
})),
|
|
145
205
|
hasMore: !(isLastCalendar && isLastPage),
|
|
146
|
-
checkpoint
|
|
206
|
+
checkpoint,
|
|
207
|
+
...isLastPage ? {
|
|
208
|
+
sweep: {
|
|
209
|
+
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
210
|
+
scope: {
|
|
211
|
+
calendarID: calendar.id
|
|
212
|
+
},
|
|
213
|
+
partition: calendar.id,
|
|
214
|
+
provenance: relistID,
|
|
215
|
+
complete: true,
|
|
216
|
+
kind: 'relist',
|
|
217
|
+
keep: [
|
|
218
|
+
...seen
|
|
219
|
+
],
|
|
220
|
+
sourceFetchHLC: relistID,
|
|
221
|
+
predicate: {
|
|
222
|
+
field: 'startAt',
|
|
223
|
+
from: window.windowStart ?? null,
|
|
224
|
+
to: window.windowEnd ?? null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} : {}
|
|
147
228
|
};
|
|
148
229
|
eventBatchYielded = true;
|
|
149
230
|
}
|
|
@@ -161,147 +242,274 @@ export function createGoogleCalendarProvider(config) {
|
|
|
161
242
|
checkpoint: {
|
|
162
243
|
calendarListSyncToken,
|
|
163
244
|
calendars: calendarTokens,
|
|
164
|
-
...
|
|
165
|
-
windowStart
|
|
166
|
-
} : {}
|
|
245
|
+
...windowFields(window)
|
|
167
246
|
}
|
|
168
247
|
};
|
|
169
248
|
}
|
|
170
249
|
}
|
|
171
250
|
async function* fetchIncrementalEntities(params, checkpoint) {
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
251
|
+
const resumingRelist = Object.values(checkpoint.calendars).some((calendar)=>calendar.relist != null);
|
|
252
|
+
const rollDue = checkpoint.roll != null || checkpoint.completed == null && !resumingRelist && isRollDue(params.boundary, checkpoint);
|
|
253
|
+
const window = checkpoint.roll != null ? {
|
|
254
|
+
windowStart: checkpoint.windowStart,
|
|
255
|
+
windowEnd: checkpoint.roll.windowEnd,
|
|
256
|
+
windowRolledAt: checkpoint.roll.startedAt
|
|
257
|
+
} : rollDue ? fullPassWindow(params.boundary, checkpoint) : {
|
|
258
|
+
windowStart: checkpoint.windowStart ?? computeTimeMin(params.boundary),
|
|
259
|
+
...params.boundary.maxAhead != null ? {
|
|
260
|
+
windowEnd: checkpoint.windowEnd,
|
|
261
|
+
windowRolledAt: checkpoint.windowRolledAt
|
|
262
|
+
} : {}
|
|
263
|
+
};
|
|
264
|
+
const roll = checkpoint.roll ?? (rollDue && window.windowEnd != null && window.windowRolledAt != null ? {
|
|
265
|
+
windowEnd: window.windowEnd,
|
|
266
|
+
startedAt: window.windowRolledAt,
|
|
267
|
+
completed: []
|
|
268
|
+
} : undefined);
|
|
176
269
|
const activeCalendars = [];
|
|
177
|
-
const deletedCalendarIDs =
|
|
270
|
+
const deletedCalendarIDs = new Set();
|
|
271
|
+
// Every calendar the list reports live, excluded ones included: exclusion
|
|
272
|
+
// is local configuration, never evidence that a calendar was removed.
|
|
273
|
+
const listedCalendarIDs = new Set();
|
|
178
274
|
let newCalendarListSyncToken;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
275
|
+
const collectCalendars = async (syncToken)=>{
|
|
276
|
+
activeCalendars.length = 0;
|
|
277
|
+
deletedCalendarIDs.clear();
|
|
278
|
+
listedCalendarIDs.clear();
|
|
279
|
+
for await (const page of fetchAllCalendars({
|
|
280
|
+
client,
|
|
281
|
+
syncToken,
|
|
282
|
+
signal: params.signal
|
|
283
|
+
})){
|
|
284
|
+
for (const cal of page.items){
|
|
285
|
+
if (cal.deleted === true) {
|
|
286
|
+
deletedCalendarIDs.add(cal.id);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
listedCalendarIDs.add(cal.id);
|
|
290
|
+
if (!excludeSet.has(cal.id)) activeCalendars.push(cal);
|
|
189
291
|
}
|
|
292
|
+
if (page.nextSyncToken != null) newCalendarListSyncToken = page.nextSyncToken;
|
|
190
293
|
}
|
|
191
|
-
|
|
192
|
-
|
|
294
|
+
};
|
|
295
|
+
try {
|
|
296
|
+
await collectCalendars(checkpoint.calendarListSyncToken);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (!(err instanceof ConnectorError && err.httpStatus === 410)) throw err;
|
|
299
|
+
await collectCalendars();
|
|
300
|
+
for (const id of Object.keys(checkpoint.calendars)){
|
|
301
|
+
if (!listedCalendarIDs.has(id)) deletedCalendarIDs.add(id);
|
|
193
302
|
}
|
|
194
303
|
}
|
|
195
|
-
// Yield calendar batch (active + deletions)
|
|
196
304
|
const calendarListToken = mintFetchHLC();
|
|
197
305
|
yield {
|
|
198
306
|
modelID: CALENDAR_MODEL_ID,
|
|
199
|
-
entities: activeCalendars.map((
|
|
307
|
+
entities: activeCalendars.map((cal)=>({
|
|
200
308
|
present: true,
|
|
201
|
-
record: mapCalendarToEntity(
|
|
309
|
+
record: mapCalendarToEntity(cal, accountKey),
|
|
202
310
|
sourceFetchHLC: calendarListToken
|
|
203
311
|
})),
|
|
204
|
-
deleted:
|
|
312
|
+
deleted: [
|
|
313
|
+
...deletedCalendarIDs
|
|
314
|
+
].map((id)=>({
|
|
205
315
|
present: false,
|
|
206
316
|
sourceID: id,
|
|
207
317
|
sourceFetchHLC: calendarListToken
|
|
208
318
|
})),
|
|
209
319
|
hasMore: true
|
|
210
320
|
};
|
|
211
|
-
|
|
212
|
-
// Start from the previous calendar set, remove deleted, add newly discovered active ones
|
|
213
|
-
const prevCalendarTokens = {
|
|
321
|
+
const calendarTokens = {
|
|
214
322
|
...checkpoint.calendars
|
|
215
323
|
};
|
|
324
|
+
for (const id of deletedCalendarIDs)delete calendarTokens[id];
|
|
325
|
+
for (const cal of activeCalendars){
|
|
326
|
+
if (!(cal.id in calendarTokens)) calendarTokens[cal.id] = {};
|
|
327
|
+
}
|
|
328
|
+
const completed = new Set(checkpoint.completed ?? []);
|
|
329
|
+
const rollCompleted = new Set(roll?.completed ?? []);
|
|
330
|
+
const pendingCalendarIDs = Object.keys(calendarTokens).filter((id)=>!completed.has(id) && !rollCompleted.has(id));
|
|
331
|
+
const checkpointFor = (tokens, complete, completedCalendarID)=>{
|
|
332
|
+
if (completedCalendarID != null) {
|
|
333
|
+
if (roll == null) completed.add(completedCalendarID);
|
|
334
|
+
else rollCompleted.add(completedCalendarID);
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
calendarListSyncToken: newCalendarListSyncToken ?? checkpoint.calendarListSyncToken,
|
|
338
|
+
calendars: tokens,
|
|
339
|
+
...roll == null && !complete ? {
|
|
340
|
+
completed: [
|
|
341
|
+
...completed
|
|
342
|
+
]
|
|
343
|
+
} : {},
|
|
344
|
+
...roll != null && !complete ? {
|
|
345
|
+
roll: {
|
|
346
|
+
...roll,
|
|
347
|
+
completed: [
|
|
348
|
+
...rollCompleted
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
} : {},
|
|
352
|
+
...windowFields(complete ? window : {
|
|
353
|
+
...window,
|
|
354
|
+
windowRolledAt: checkpoint.windowRolledAt
|
|
355
|
+
})
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
const eventBatch = (calendarID, items, token, hasMore, nextCheckpoint)=>({
|
|
359
|
+
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
360
|
+
// A sync-token delta can move an existing event outside the fetch
|
|
361
|
+
// window. Preserve its document with the new date; only a provider
|
|
362
|
+
// cancellation is an authoritative deletion.
|
|
363
|
+
entities: items.filter((event)=>event.status !== 'cancelled').map((event)=>({
|
|
364
|
+
present: true,
|
|
365
|
+
record: mapEventToEntity(event, calendarID),
|
|
366
|
+
sourceFetchHLC: token
|
|
367
|
+
})),
|
|
368
|
+
deleted: items.filter((event)=>event.status === 'cancelled').map((event)=>({
|
|
369
|
+
present: false,
|
|
370
|
+
sourceID: `${calendarID}/${event.id}`,
|
|
371
|
+
sourceFetchHLC: token
|
|
372
|
+
})),
|
|
373
|
+
hasMore,
|
|
374
|
+
checkpoint: nextCheckpoint
|
|
375
|
+
});
|
|
216
376
|
for (const deletedID of deletedCalendarIDs){
|
|
217
|
-
|
|
377
|
+
yield {
|
|
378
|
+
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
379
|
+
entities: [],
|
|
380
|
+
sweep: {
|
|
381
|
+
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
382
|
+
scope: {
|
|
383
|
+
calendarID: deletedID
|
|
384
|
+
},
|
|
385
|
+
keep: [],
|
|
386
|
+
sourceFetchHLC: calendarListToken,
|
|
387
|
+
partition: deletedID,
|
|
388
|
+
provenance: calendarListToken,
|
|
389
|
+
complete: true,
|
|
390
|
+
predicate: null,
|
|
391
|
+
kind: 'partition-removal'
|
|
392
|
+
},
|
|
393
|
+
hasMore: true
|
|
394
|
+
};
|
|
218
395
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
const updatedCalendarTokens = {};
|
|
236
|
-
for (const [i, entry] of allCalendarIDsToSync.entries()){
|
|
237
|
-
const { id: calendarID, isNew } = entry;
|
|
238
|
-
const isLastCalendar = i === allCalendarIDsToSync.length - 1;
|
|
239
|
-
// Use stored syncToken for known calendars; use timeMin for new ones
|
|
240
|
-
const storedToken = isNew ? undefined : prevCalendarTokens[calendarID]?.eventsSyncToken;
|
|
241
|
-
let eventsSyncToken;
|
|
242
|
-
for await (const page of fetchEventsForCalendar({
|
|
243
|
-
calendarID,
|
|
244
|
-
client,
|
|
245
|
-
syncToken: storedToken,
|
|
246
|
-
timeMin: isNew ? timeMin : undefined,
|
|
247
|
-
signal: params.signal
|
|
248
|
-
})){
|
|
249
|
-
if (page.nextSyncToken != null) {
|
|
250
|
-
eventsSyncToken = page.nextSyncToken;
|
|
396
|
+
for (const [index, calendarID] of pendingCalendarIDs.entries()){
|
|
397
|
+
const prior = calendarTokens[calendarID];
|
|
398
|
+
const storedToken = prior?.eventsSyncToken;
|
|
399
|
+
const relist = prior?.relist;
|
|
400
|
+
if (rollDue && storedToken != null && relist == null) {
|
|
401
|
+
try {
|
|
402
|
+
for await (const page of fetchEventsForCalendar({
|
|
403
|
+
calendarID,
|
|
404
|
+
client,
|
|
405
|
+
syncToken: storedToken,
|
|
406
|
+
signal: params.signal
|
|
407
|
+
})){
|
|
408
|
+
yield eventBatch(calendarID, page.items, mintFetchHLC(), true);
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if (!(err instanceof ConnectorError && err.httpStatus === 410)) throw err;
|
|
251
412
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
413
|
+
}
|
|
414
|
+
const list = async function*(syncToken, currentRelist) {
|
|
415
|
+
let tracking = currentRelist;
|
|
416
|
+
if (syncToken == null && tracking == null) {
|
|
417
|
+
const startedFetchHLC = mintFetchHLC();
|
|
418
|
+
tracking = {
|
|
419
|
+
relistID: startedFetchHLC,
|
|
420
|
+
startedFetchHLC,
|
|
421
|
+
seen: []
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
for await (const page of fetchEventsForCalendar({
|
|
425
|
+
calendarID,
|
|
426
|
+
client,
|
|
427
|
+
syncToken,
|
|
428
|
+
pageToken: tracking?.pageToken,
|
|
429
|
+
timeMin: syncToken == null ? window.windowStart : undefined,
|
|
430
|
+
timeMax: syncToken == null ? window.windowEnd : undefined,
|
|
431
|
+
showDeleted: syncToken == null ? true : undefined,
|
|
432
|
+
signal: params.signal
|
|
433
|
+
})){
|
|
434
|
+
const token = mintFetchHLC();
|
|
435
|
+
const seen = tracking == null ? [] : [
|
|
436
|
+
...new Set([
|
|
437
|
+
...tracking.seen,
|
|
438
|
+
...page.items.map((item)=>`${calendarID}/${item.id}`)
|
|
439
|
+
])
|
|
440
|
+
];
|
|
441
|
+
const finalPage = page.nextSyncToken != null;
|
|
442
|
+
const complete = index === pendingCalendarIDs.length - 1 && finalPage;
|
|
443
|
+
const nextTokens = {
|
|
444
|
+
...calendarTokens,
|
|
445
|
+
[calendarID]: finalPage ? {
|
|
258
446
|
eventsSyncToken: page.nextSyncToken
|
|
447
|
+
} : {
|
|
448
|
+
...syncToken == null ? {} : {
|
|
449
|
+
eventsSyncToken: syncToken
|
|
450
|
+
},
|
|
451
|
+
relist: tracking == null ? undefined : {
|
|
452
|
+
...tracking,
|
|
453
|
+
pageToken: page.nextPageToken,
|
|
454
|
+
seen
|
|
455
|
+
}
|
|
259
456
|
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
457
|
+
};
|
|
458
|
+
const batch = eventBatch(calendarID, page.items, token, !complete, tracking != null || finalPage ? checkpointFor(nextTokens, complete, finalPage ? calendarID : undefined) : undefined);
|
|
459
|
+
if (tracking != null && finalPage) {
|
|
460
|
+
batch.sweep = {
|
|
461
|
+
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
462
|
+
scope: {
|
|
463
|
+
calendarID
|
|
464
|
+
},
|
|
465
|
+
keep: seen,
|
|
466
|
+
sourceFetchHLC: tracking.startedFetchHLC,
|
|
467
|
+
partition: calendarID,
|
|
468
|
+
provenance: tracking.relistID,
|
|
469
|
+
complete: true,
|
|
470
|
+
predicate: {
|
|
471
|
+
field: 'startAt',
|
|
472
|
+
from: window.windowStart ?? null,
|
|
473
|
+
to: window.windowEnd ?? null
|
|
474
|
+
},
|
|
475
|
+
kind: 'relist'
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
yield batch;
|
|
479
|
+
if (tracking != null) tracking = {
|
|
480
|
+
...tracking,
|
|
481
|
+
pageToken: page.nextPageToken,
|
|
482
|
+
seen
|
|
483
|
+
};
|
|
484
|
+
if (finalPage) calendarTokens[calendarID] = {
|
|
485
|
+
eventsSyncToken: page.nextSyncToken
|
|
486
|
+
};
|
|
487
|
+
}
|
|
289
488
|
};
|
|
489
|
+
if (relist != null) {
|
|
490
|
+
try {
|
|
491
|
+
yield* list(undefined, relist);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
if (!(err instanceof ConnectorError && err.httpStatus === 410)) throw err;
|
|
494
|
+
yield* list(undefined, undefined);
|
|
495
|
+
}
|
|
496
|
+
} else if (rollDue || storedToken == null) {
|
|
497
|
+
yield* list(undefined, undefined);
|
|
498
|
+
} else {
|
|
499
|
+
try {
|
|
500
|
+
yield* list(storedToken, undefined);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
if (!(err instanceof ConnectorError && err.httpStatus === 410)) throw err;
|
|
503
|
+
yield* list(undefined, undefined);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
290
506
|
}
|
|
291
|
-
|
|
292
|
-
// batch with hasMore: false and the composite checkpoint so the sync can complete.
|
|
293
|
-
if (allCalendarIDsToSync.length === 0) {
|
|
507
|
+
if (pendingCalendarIDs.length === 0) {
|
|
294
508
|
yield {
|
|
295
509
|
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
296
510
|
entities: [],
|
|
297
511
|
hasMore: false,
|
|
298
|
-
checkpoint:
|
|
299
|
-
calendarListSyncToken: newCalendarListSyncToken,
|
|
300
|
-
calendars: updatedCalendarTokens,
|
|
301
|
-
...windowStart != null ? {
|
|
302
|
-
windowStart
|
|
303
|
-
} : {}
|
|
304
|
-
}
|
|
512
|
+
checkpoint: checkpointFor(calendarTokens, true)
|
|
305
513
|
};
|
|
306
514
|
}
|
|
307
515
|
}
|
|
@@ -339,6 +547,71 @@ export function createGoogleCalendarProvider(config) {
|
|
|
339
547
|
}
|
|
340
548
|
});
|
|
341
549
|
}
|
|
550
|
+
async function verifyEntity(model, sourceID) {
|
|
551
|
+
const sourceFetchHLC = mintFetchHLC();
|
|
552
|
+
if (model !== CALENDAR_EVENT_MODEL_ID) {
|
|
553
|
+
try {
|
|
554
|
+
return await fetchEntity(model, sourceID);
|
|
555
|
+
} catch {
|
|
556
|
+
return {
|
|
557
|
+
outcome: 'uncertain',
|
|
558
|
+
sourceFetchHLC
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const { calendarID, eventID } = splitEventSourceID(sourceID);
|
|
564
|
+
try {
|
|
565
|
+
const event = await fetchEventResource({
|
|
566
|
+
accessToken: config.credential.accessToken,
|
|
567
|
+
calendarID,
|
|
568
|
+
eventID,
|
|
569
|
+
fetch: fetchFn
|
|
570
|
+
});
|
|
571
|
+
if (event.status === 'cancelled') return {
|
|
572
|
+
outcome: 'cancelled',
|
|
573
|
+
sourceFetchHLC
|
|
574
|
+
};
|
|
575
|
+
return {
|
|
576
|
+
present: true,
|
|
577
|
+
record: mapEventToEntity(event, calendarID),
|
|
578
|
+
sourceFetchHLC
|
|
579
|
+
};
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (!(error instanceof ConnectorError && error.httpStatus === 404)) {
|
|
582
|
+
return {
|
|
583
|
+
outcome: 'uncertain',
|
|
584
|
+
sourceFetchHLC
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const calendar = await getCalendarListEntry({
|
|
589
|
+
accessToken: config.credential.accessToken,
|
|
590
|
+
calendarID,
|
|
591
|
+
fetch: fetchFn
|
|
592
|
+
});
|
|
593
|
+
return calendar.deleted === true ? {
|
|
594
|
+
outcome: 'uncertain',
|
|
595
|
+
sourceFetchHLC
|
|
596
|
+
} : {
|
|
597
|
+
present: false,
|
|
598
|
+
sourceID,
|
|
599
|
+
sourceFetchHLC
|
|
600
|
+
};
|
|
601
|
+
} catch {
|
|
602
|
+
return {
|
|
603
|
+
outcome: 'uncertain',
|
|
604
|
+
sourceFetchHLC
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} catch {
|
|
609
|
+
return {
|
|
610
|
+
outcome: 'uncertain',
|
|
611
|
+
sourceFetchHLC
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
342
615
|
// Provider-authoritative re-read for write-back recovery. Unlike
|
|
343
616
|
// fetchEntity, the present:true branch also surfaces a providerETag when the
|
|
344
617
|
// provider reports one — the current revision an update's If-Match can
|
|
@@ -379,6 +652,7 @@ export function createGoogleCalendarProvider(config) {
|
|
|
379
652
|
],
|
|
380
653
|
fetchAll: fetchAllEntities,
|
|
381
654
|
fetchEntity,
|
|
655
|
+
verifyEntity,
|
|
382
656
|
reconcile,
|
|
383
657
|
async *fetchChanges (params) {
|
|
384
658
|
const checkpoint = params.checkpoint;
|
|
@@ -386,17 +660,7 @@ export function createGoogleCalendarProvider(config) {
|
|
|
386
660
|
yield* fetchAllEntities(params);
|
|
387
661
|
return;
|
|
388
662
|
}
|
|
389
|
-
|
|
390
|
-
yield* fetchIncrementalEntities(params, checkpoint);
|
|
391
|
-
} catch (err) {
|
|
392
|
-
// 410 Gone = the stored syncToken has expired; Google requires a full
|
|
393
|
-
// resync. Discard the checkpoint by delegating to fetchAll.
|
|
394
|
-
if (err instanceof ConnectorError && err.httpStatus === 410) {
|
|
395
|
-
yield* fetchAllEntities(params);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
throw err;
|
|
399
|
-
}
|
|
663
|
+
yield* fetchIncrementalEntities(params, checkpoint);
|
|
400
664
|
}
|
|
401
665
|
};
|
|
402
666
|
}
|