From 580990a5eb4a79892c48e3a3fce3386fe80e6cc2 Mon Sep 17 00:00:00 2001 From: Determinant Date: Sat, 2 Feb 2019 22:15:54 -0500 Subject: finish cache design --- public/gapi.js | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 public/gapi.js (limited to 'public/gapi.js') diff --git a/public/gapi.js b/public/gapi.js new file mode 100644 index 0000000..f5ab73e --- /dev/null +++ b/public/gapi.js @@ -0,0 +1,182 @@ +/* global chrome */ +const gapi_base = 'https://www.googleapis.com/calendar/v3'; + +const GApiError = { + invalidSyncToken: 1, + otherError: 2, +}; + +function to_params(dict) { + return Object.entries(dict).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&'); +} + +function getAuthToken() { + return new Promise(resolver => + chrome.identity.getAuthToken( + {interactive: true}, token => resolver(token))); +} + +function getCalendars(token) { + return fetch(`${gapi_base}/users/me/calendarList?${to_params({access_token: token})}`, + { method: 'GET', async: true }) + .then(response => response.json()) + .then(data => data.items); +} + +function getColors(token) { + return fetch(`${gapi_base}/colors?${to_params({access_token: token})}`, + { method: 'GET', async: true }) + .then(response => response.json()); +} + +function getEvent(calId, eventId, token) { + return fetch(`${gapi_base}/calendars/${calId}/events/${eventId}?${to_params({access_token: token})}`, + { method: 'GET', async: true }) + .then(response => response.json()); +} + +function getEvents(calId, token, syncToken, resultsPerRequest=100) { + let results = []; + const singleFetch = (pageToken, syncToken) => fetch(`${gapi_base}/calendars/${calId}/events?${to_params({ + access_token: token, + pageToken, + syncToken, + maxResults: resultsPerRequest + })}`, { method: 'GET', async: true }) + .then(response => { + if (response.status === 200) + return response.json(); + else if (response.status == 410) + throw GApiError.invalidSyncToken; + else throw GApiError.otherErrors; + }) + .then(data => { + results.push(...data.items); + if (data.nextPageToken) { + return singleFetch(data.nextPageToken, ''); + } else { + return ({ + nextSyncToken: data.nextSyncToken, + results + }); + } + }) + + return singleFetch('', syncToken); +} + +class GCalendar { + constructor(calId, name) { + this.calId = calId; + this.name = name; + this.token = getAuthToken(); + this.syncToken = ''; + this.cache = {}; + } + + static dateToCacheKey(date) { + return Math.floor(date / 8.64e7); + } + + getSlot(k) { + if (!this.cache[k]) + this.cache[k] = {}; + return this.cache[k]; + } + + static slotStartDate(k) { return new Date(k * 8.64e7); } + static slotEndDate(k) { return new Date((k + 1) * 8.64e7); } + + addEvent(e) { + let ks = GCalendar.dateToCacheKey(e.start); + let ke = GCalendar.dateToCacheKey(new Date(e.end.getTime() - 1)); + if (ks === ke) + this.getSlot(ks)[e.id] = { + start: e.start, + end: e.end, + id: e.id, + summary: e.summary}; + else + { + this.getSlot(ks)[e.id] = { + start: e.start, + end: GCalendar.slotEndDate(ks), + id: e.id, + summary: e.summary}; + this.getSlot(ke)[e.id] = { + start: GCalendar.slotStartDate(ke), + end: e.end, + id: e.id, + summary: e.summary}; + for (let k = ks + 1; k < ke; k++) + this.getSlot(k)[e.id] = { + start: GCalendar.slotStartDate(k), + end: GCalendar.slotEndDate(k), + id: e.id, + summary: e.summary}; + } + } + + removeEvent(e) { + let ks = GCalendar.dateToCacheKey(e.start); + let ke = GCalendar.dateToCacheKey(new Date(e.end.getTime() - 1)); + for (let k = ks; k <= ke; k++) + delete this.getSlot(k)[e.id]; + } + + getSlotEvents(k, start, end) { + let s = this.getSlot(k); + let results = []; + for (let id in s) { + if (!(s[id].start >= end || s[id].end <= start)) + { + results.push({ + id, + start: s[id].start < start ? start: s[id].start, + end: s[id].end > end ? end: s[id].end, + summary: s[id].summary + }); + } + } + return results; + } + + getCachedEvents(start, end) { + let ks = GCalendar.dateToCacheKey(start); + let ke = GCalendar.dateToCacheKey(new Date(end.getTime() - 1)); + let results = this.getSlotEvents(ks, start, end); + for (let k = ks + 1; k < ke; k++) + { + let s = this.getSlot(k); + for (let id in s) + results.push(s[id]); + } + if (ke > ks) + results.push(...this.getSlotEvents(ke, start, end)); + return results; + } + + sync() { + return this.token.then(token => getEvents(this.calId, token, this.syncToken).then(r => { + this.syncToken = r.nextSyncToken; + let pm_results = r.results.map(e => e.start ? Promise.resolve(e) : getEvent(this.calId, e.id, token)); + return Promise.all(pm_results).then(results => results.forEach(e => { + e.start = new Date(e.start.dateTime); + e.end = new Date(e.end.dateTime); + if (e.status === 'confirmed') + this.addEvent(e); + else if (e.status === 'cancelled') + this.removeEvent(e); + })); + })).catch(e => { + if (e == GApiError.invalidSyncToken) { + this.syncToken = ''; + this.sync(); + } else throw e; + }); + } + + getEvents(start, end) { + return this.sync().then(() => this.getCachedEvents(start, end)); + } +} -- cgit v1.2.3