diff options
author | Determinant <ted.sybil@gmail.com> | 2019-02-13 01:11:31 -0500 |
---|---|---|
committer | Determinant <ted.sybil@gmail.com> | 2019-02-13 01:11:31 -0500 |
commit | c594888953151ddfb4ca04b7752bfd51edc1d6da (patch) | |
tree | 59b6d0b0f514f76d152eee9a4359c08110f73531 /src | |
parent | f28b818cc62c7fff67517a4147e64f08ebd73027 (diff) |
WIP: migrate to TypeScriptX
Diffstat (limited to 'src')
-rw-r--r-- | src/Analyze.tsx (renamed from src/Analyze.js) | 36 | ||||
-rw-r--r-- | src/Chart.tsx (renamed from src/Chart.js) | 0 | ||||
-rw-r--r-- | src/Dashboard.tsx (renamed from src/Dashboard.js) | 0 | ||||
-rw-r--r-- | src/Dialog.tsx (renamed from src/Dialog.js) | 0 | ||||
-rw-r--r-- | src/Logo.tsx (renamed from src/Logo.js) | 0 | ||||
-rw-r--r-- | src/PatternTable.tsx (renamed from src/PatternTable.js) | 0 | ||||
-rw-r--r-- | src/RegexField.tsx (renamed from src/RegexField.js) | 0 | ||||
-rw-r--r-- | src/Settings.tsx (renamed from src/Settings.js) | 16 | ||||
-rw-r--r-- | src/Snackbar.tsx (renamed from src/Snackbar.js) | 0 | ||||
-rw-r--r-- | src/background.ts (renamed from src/background.js) | 2 | ||||
-rw-r--r-- | src/decl.ts | 1 | ||||
-rw-r--r-- | src/duration.js | 21 | ||||
-rw-r--r-- | src/duration.ts | 26 | ||||
-rw-r--r-- | src/gapi.js | 335 | ||||
-rw-r--r-- | src/gapi.ts | 369 | ||||
-rw-r--r-- | src/index.js | 11 | ||||
-rw-r--r-- | src/index.tsx | 5 | ||||
-rw-r--r-- | src/msg.js | 97 | ||||
-rw-r--r-- | src/msg.ts | 87 | ||||
-rw-r--r-- | src/pattern.ts (renamed from src/pattern.js) | 46 | ||||
-rw-r--r-- | src/popup.tsx (renamed from src/popup.js) | 10 | ||||
-rw-r--r-- | src/serviceWorker.js | 135 | ||||
-rw-r--r-- | src/theme.tsx (renamed from src/theme.js) | 0 |
23 files changed, 560 insertions, 637 deletions
diff --git a/src/Analyze.js b/src/Analyze.tsx index 98e3ce2..5450998 100644 --- a/src/Analyze.js +++ b/src/Analyze.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import 'react-dates/initialize'; import 'react-dates/lib/css/_datepicker.css'; import { DateRangePicker } from 'react-dates'; -import { withStyles } from '@material-ui/core/styles'; +import { Theme, withStyles } from '@material-ui/core/styles'; import cyan from '@material-ui/core/colors/cyan'; import deepOrange from '@material-ui/core/colors/deepOrange'; import CssBaseline from '@material-ui/core/CssBaseline'; @@ -15,51 +15,55 @@ import Grid from '@material-ui/core/Grid'; import AddCircleIcon from '@material-ui/icons/AddCircle'; import IconButton from '@material-ui/core/IconButton'; import * as gapi from './gapi'; -import { msgType, MsgClient } from './msg'; +import { MsgType, MsgClient } from './msg'; import { Pattern, PatternEntry } from './pattern'; import { AnalyzePieChart, getChartData } from './Chart'; import PatternTable from './PatternTable'; import Snackbar from './Snackbar'; import AlertDialog from './Dialog'; +import moment from 'moment'; const default_chart_data = [ {name: 'Work', value: 10, color: cyan[300]}, {name: 'Wasted', value: 10, color: deepOrange[300]}]; -const styles = theme => ({ +const styles = (theme: Theme) => ({ buttonSpacer: { marginBottom: theme.spacing.unit * 4, }, }); class Analyze extends React.Component { + msgClient: MsgClient; + state = { - patterns: [], + patterns: [] as PatternEntry[], calendars: {}, - startDate: null, - endDate: null, + startDate: null as moment.Moment, + endDate: null as moment.Moment, patternGraphData: default_chart_data, calendarGraphData: default_chart_data, snackBarOpen: false, snackBarMsg: 'unknown', snackBarVariant: 'error', dialogOpen: false, - dialogMsg: {title: '', message: ''}, + dialogMsg: {title: '', message: ''} }; - constructor(props) { + constructor(props: any) { super(props); + this.msgClient = new MsgClient('main'); this.msgClient.sendMsg({ - type: msgType.getPatterns, + type: MsgType.getPatterns, data: { id: 'analyze' } }).then(msg => { this.setState({ patterns: msg.data.map(p => PatternEntry.inflate(p)) }); }); this.msgClient.sendMsg({ - type: msgType.getCalendars, + type: MsgType.getCalendars, data: { enabledOnly: true } }).then(msg => { this.setState({ calendars: msg.data }); @@ -71,20 +75,20 @@ class Analyze extends React.Component { this.dialogPromiseResolver = null; } - loadPatterns = patterns => { + loadPatterns = (patterns: PatternEntry[]) => { this.msgClient.sendMsg({ - type: msgType.updatePatterns, + type: MsgType.updatePatterns, data: { id: 'analyze', patterns: patterns.map(p => p.deflate()) } }).then(() => this.setState({ patterns })); }; - updatePattern = (field, idx, value) => { + updatePattern = (field: string, idx: number, value: PatternEntry[]) => { let patterns = this.state.patterns; patterns[idx][field] = value; this.loadPatterns(patterns); }; - removePattern = idx => { + removePattern = (idx: number) => { let patterns = this.state.patterns; patterns.splice(idx, 1); for (let i = 0; i < patterns.length; i++) @@ -99,8 +103,8 @@ class Analyze extends React.Component { this.loadPatterns(patterns); }; - getCalEvents = (id, start, end) => { - return this.msgClient.sendMsg({ type: msgType.getCalEvents, data: { id, + getCalEvents = (id: string, start: Date, end: Date) => { + return this.msgClient.sendMsg({ type: MsgType.getCalEvents, data: { id, start: start.getTime(), end: end.getTime() } }) .then(({ data }) => data.map(e => { diff --git a/src/Chart.js b/src/Chart.tsx index b1c36ed..b1c36ed 100644 --- a/src/Chart.js +++ b/src/Chart.tsx diff --git a/src/Dashboard.js b/src/Dashboard.tsx index 04ced46..04ced46 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.tsx diff --git a/src/Dialog.js b/src/Dialog.tsx index 7e24176..7e24176 100644 --- a/src/Dialog.js +++ b/src/Dialog.tsx diff --git a/src/Logo.js b/src/Logo.tsx index a4036a9..a4036a9 100644 --- a/src/Logo.js +++ b/src/Logo.tsx diff --git a/src/PatternTable.js b/src/PatternTable.tsx index 93be293..93be293 100644 --- a/src/PatternTable.js +++ b/src/PatternTable.tsx diff --git a/src/RegexField.js b/src/RegexField.tsx index e3fa9f4..e3fa9f4 100644 --- a/src/RegexField.js +++ b/src/RegexField.tsx diff --git a/src/Settings.js b/src/Settings.tsx index 2835483..83f1da6 100644 --- a/src/Settings.js +++ b/src/Settings.tsx @@ -20,7 +20,7 @@ import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import Checkbox from '@material-ui/core/Checkbox'; import * as gapi from './gapi'; -import { msgType, MsgClient } from './msg'; +import { MsgType, MsgClient } from './msg'; import { Pattern, PatternEntry } from './pattern'; import PatternTable from './PatternTable'; import Snackbar from './Snackbar'; @@ -137,21 +137,21 @@ class Settings extends React.Component { this.msgClient = new MsgClient('main'); this.msgClient.sendMsg({ - type: msgType.getPatterns, + type: MsgType.getPatterns, data: { id: 'main' } }).then(msg => { this.setState({ patterns: msg.data.map(p => PatternEntry.inflate(p)) }); }); this.msgClient.sendMsg({ - type: msgType.getCalendars, + type: MsgType.getCalendars, data: { enabledOnly: false } }).then(msg => { this.setState({ calendars: msg.data }); }); this.msgClient.sendMsg({ - type: msgType.getConfig, + type: MsgType.getConfig, data: ['trackedPeriods'] }).then(msg => { let config = { @@ -191,7 +191,7 @@ class Settings extends React.Component { var calendars = {...this.state.calendars}; calendars[id].enabled = !calendars[id].enabled; this.msgClient.sendMsg({ - type: msgType.updateCalendars, + type: MsgType.updateCalendars, data: calendars }).then(() => this.setState({ calendars })); } @@ -240,14 +240,14 @@ class Settings extends React.Component { calendars[id].enabled = this.state.calendars[id].enabled; } this.msgClient.sendMsg({ - type: msgType.updateCalendars, + type: MsgType.updateCalendars, data: calendars }).then(() => this.setState({ calendars })); }; loadPatterns = (patterns, id) => { this.msgClient.sendMsg({ - type: msgType.updatePatterns, + type: MsgType.updatePatterns, data: { id, patterns: patterns.map(p => p.deflate()) } }).then(() => this.setState({ patterns })); }; @@ -297,7 +297,7 @@ class Settings extends React.Component { updateTrackedPeriods = trackedPeriods => { this.msgClient.sendMsg({ - type: msgType.updateConfig, + type: MsgType.updateConfig, data: { trackedPeriods: trackedPeriods.map(p => ({ name: p.name, start: p.start.deflate(), diff --git a/src/Snackbar.js b/src/Snackbar.tsx index f17863c..f17863c 100644 --- a/src/Snackbar.js +++ b/src/Snackbar.tsx diff --git a/src/background.js b/src/background.ts index f22970e..2a23b57 100644 --- a/src/background.js +++ b/src/background.ts @@ -5,7 +5,7 @@ import moment from 'moment'; import { getChartData } from './Chart'; import { PatternEntry } from './pattern'; -let mainPatterns = []; +let mainPatterns: number[] = []; let analyzePatterns = []; let calendars = {}; let calData = {}; diff --git a/src/decl.ts b/src/decl.ts new file mode 100644 index 0000000..5b3daa3 --- /dev/null +++ b/src/decl.ts @@ -0,0 +1 @@ +declare module 'react-dates'; diff --git a/src/duration.js b/src/duration.js deleted file mode 100644 index 53de0ad..0000000 --- a/src/duration.js +++ /dev/null @@ -1,21 +0,0 @@ -import moment from 'moment'; - -export class Duration { - constructor(value, unit) { - this.value = value - this.unit = unit - } - - toMoment() { - let m = moment.duration(this.value, this.unit); - if (m.isValid()) return m; - return null; - } - - static days(n) { return new Duration(n, 'days'); } - static weeks(n) { return new Duration(n, 'weeks'); } - static months(n) { return new Duration(n, 'months'); } - - deflate() { return { value: this.value, unit: this.unit }; } - static inflate = obj => new Duration(obj.value, obj.unit); -} diff --git a/src/duration.ts b/src/duration.ts new file mode 100644 index 0000000..18849a0 --- /dev/null +++ b/src/duration.ts @@ -0,0 +1,26 @@ +import moment from 'moment'; + +export type TimeUnit = moment.unitOfTime.DurationConstructor; + +export class Duration { + value: number; + unit: TimeUnit; + constructor(value: number, unit: TimeUnit) { + this.value = value + this.unit = unit + } + + isValid() { return moment.duration(this.value, this.unit).isValid(); } + toMoment() { + let m = moment.duration(this.value, this.unit); + if (m.isValid()) return m; + return null; + } + + static days(n: number) { return new Duration(n, 'days'); } + static weeks(n: number) { return new Duration(n, 'weeks'); } + static months(n: number) { return new Duration(n, 'months'); } + + deflate() { return { value: this.value, unit: this.unit }; } + static inflate = (obj: { value: number, unit: TimeUnit }) => new Duration(obj.value, obj.unit); +} diff --git a/src/gapi.js b/src/gapi.js deleted file mode 100644 index 3938864..0000000 --- a/src/gapi.js +++ /dev/null @@ -1,335 +0,0 @@ -/* global chrome */ -import LRU from "lru-cache"; -const gapi_base = 'https://www.googleapis.com/calendar/v3'; - -const GApiError = Object.freeze({ - invalidSyncToken: Symbol("invalidSyncToken"), - notLoggedIn: Symbol("notLoggedIn"), - notLoggedOut: Symbol("notLoggedOut"), - otherError: Symbol("otherError"), -}); - -function to_params(dict) { - return Object.entries(dict).filter(([k, v]) => v).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&'); -} - -let loggedIn = null; - -function _getAuthToken(interactive = false) { - return new Promise(resolver => - chrome.identity.getAuthToken( - { interactive }, token => resolver([token, !chrome.runtime.lastError]))) - .then(([token, ok]) => { - if (ok) return token; - else throw GApiError.notLoggedIn; - }); -} - -function _removeCachedAuthToken(token) { - return new Promise(resolver => - chrome.identity.removeCachedAuthToken({ token }, () => resolver())); -} - -export function getLoggedIn() { - if (loggedIn === null) - { - return _getAuthToken(false) - .then(() => loggedIn = true) - .catch(() => loggedIn = false) - .then(() => loggedIn); - } - else return Promise.resolve(loggedIn); -} - -export function getAuthToken() { - return getLoggedIn().then(b => { - if (b) return _getAuthToken(false); - else throw GApiError.notLoggedIn; - }); -} - -export function login() { - return getLoggedIn().then(b => { - if (!b) return _getAuthToken(true).then(() => loggedIn = true); - else throw GApiError.notLoggedOut; - }); -} - -export function logout() { - return getAuthToken().then(token => { - return fetch(`https://accounts.google.com/o/oauth2/revoke?${to_params({ token })}`, - { method: 'GET', async: true }).then(response => { - //if (response.status === 200) - return _removeCachedAuthToken(token); - //else throw GApiError.otherError; - }); - }).then(() => loggedIn = false); -} - -export 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); -} - -export 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=null, timeMin=null, timeMax=null, resultsPerRequest=100) { - let results = []; - const singleFetch = (pageToken, syncToken) => fetch(`${gapi_base}/calendars/${calId}/events?${to_params({ - access_token: token, - pageToken, - syncToken, - timeMin, - timeMax, - 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.otherError; - }) - .then(data => { - results.push(...data.items); - if (data.nextPageToken) { - return singleFetch(data.nextPageToken, ''); - } else { - return ({ - nextSyncToken: data.nextSyncToken, - results - }); - } - }) - - return singleFetch('', syncToken); -} - -export class GCalendar { - constructor(calId, name, options={maxCachedItems: 100, nDaysPerSlot: 10, largeQuery: 10}) { - this.calId = calId; - this.name = name; - this.syncToken = ''; - this.cache = new LRU({ - max: options.maxCachedItems, - dispose: (k, v) => this.onRemoveSlot(k, v) - }); - this.eventMeta = {}; - this.options = options; - this.divider = 8.64e7 * this.options.nDaysPerSlot; - } - - get token() { return getAuthToken(); } - - dateToCacheKey(date) { - return Math.floor(date / this.divider); - } - - dateRangeToCacheKeys(range) { - return { - start: this.dateToCacheKey(range.start), - end: this.dateToCacheKey(new Date(range.end.getTime() - 1)) - }; - } - - getSlot(k) { - if (!this.cache.has(k)) - { - let res = {}; - this.cache.set(k, res); - return res; - } - else return this.cache.get(k); - } - - onRemoveSlot(k, v) { - for (let id in v) { - console.assert(this.eventMeta[id]); - let keys = this.eventMeta[id].keys; - keys.delete(k); - if (keys.size === 0) - delete this.eventMeta[id]; - } - } - - slotStartDate(k) { return new Date(k * this.divider); } - slotEndDate(k) { return new Date((k + 1) * this.divider); } - - addEvent(e, evict = false) { - //console.log('adding event', e); - if (this.eventMeta.hasOwnProperty(e.id)) - this.removeEvent(e); - let r = this.dateRangeToCacheKeys(e); - let ks = r.start; - let ke = r.end; - let t = this.cache.length; - let keys = new Set(); - for (let i = ks; i <= ke; i++) - { - keys.add(i); - if (!this.cache.has(i)) t++; - } - this.eventMeta[e.id] = { - keys, - summary: e.summary, - }; - if (!evict && t > this.options.maxCachedItems) return; - if (ks === ke) - this.getSlot(ks)[e.id] = { - start: e.start, - end: e.end, - id: e.id }; - else - { - this.getSlot(ks)[e.id] = { - start: e.start, - end: this.slotEndDate(ks), - id: e.id }; - this.getSlot(ke)[e.id] = { - start: this.slotStartDate(ke), - end: e.end, - id: e.id }; - for (let k = ks + 1; k < ke; k++) - this.getSlot(k)[e.id] = { - start: this.slotStartDate(k), - end: this.slotEndDate(k), - id: e.id}; - } - } - - removeEvent(e) { - let keys = this.eventMeta[e.id].keys; - console.assert(keys); - keys.forEach(k => delete this.getSlot(k)[e.id]); - delete this.eventMeta[e.id]; - } - - getSlotEvents(k, start, end) { - let s = this.getSlot(k); - //console.log(s); - 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: this.eventMeta[id].summary - }); - } - } - return results; - } - - getCachedEvents(_r) { - let r = this.dateRangeToCacheKeys(_r); - let ks = r.start; - let ke = r.end; - let results = this.getSlotEvents(ks, _r.start, _r.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, _r.start, _r.end)); - return results; - } - - sync() { - return this.token.then(token => getEvents(this.calId, token, this.syncToken).then(r => { - let pms = r.results.map(e => e.start ? Promise.resolve(e) : getEvent(this.calId, e.id, token)); - return Promise.all(pms).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); - }); - this.syncToken = r.nextSyncToken; - }); - })).catch(e => { - if (e === GApiError.invalidSyncToken) { - this.syncToken = ''; - this.sync(); - } else throw e; - }); - } - - getEvents(start, end) { - let r = this.dateRangeToCacheKeys({ start, end }); - let query = {}; - for (let k = r.start; k <= r.end; k++) - if (!this.cache.has(k)) - { - if (!query.hasOwnProperty('start')) - query.start = k; - query.end = k; - } - //console.log(`start: ${start} end: ${end}`); - if (query.hasOwnProperty('start')) - { - console.assert(query.start <= query.end); - if (query.end - query.start + 1 > this.options.largeQuery) { - console.log(`encounter large query, use direct fetch`); - return this.token.then(token => getEvents(this.calId, token, null, - start.toISOString(), end.toISOString()).then(r => { - let results = []; - r.results.forEach(e => { - console.assert(e.start); - e.start = new Date(e.start.dateTime); - e.end = new Date(e.end.dateTime); - results.push(e); - }); - return results.filter(e => !(e.start >= end || e.end <= start)).map(e => { - return { - id: e.id, - start: e.start < start ? start: e.start, - end: e.end > end ? end: e.end, - summary: e.summary, - }; - }); - })); - } - - console.log(`fetching short event list`); - return this.token.then(token => getEvents(this.calId, token, null, - this.slotStartDate(query.start).toISOString(), - this.slotEndDate(query.end).toISOString()).then(r => { - r.results.forEach(e => { - if (e.status === 'confirmed') - { - console.assert(e.start); - e.start = new Date(e.start.dateTime); - e.end = new Date(e.end.dateTime); - this.addEvent(e, true); - } - }); - if (this.syncToken === '') - this.syncToken = r.nextSyncToken; - })).then(() => this.sync()) - .then(() => this.getCachedEvents({ start, end })); - } - else - { - console.log(`cache hit`); - return this.sync().then(() => this.getCachedEvents({ start, end })); - } - } -} diff --git a/src/gapi.ts b/src/gapi.ts new file mode 100644 index 0000000..bf45ffc --- /dev/null +++ b/src/gapi.ts @@ -0,0 +1,369 @@ +/* global chrome */ + +import LRU from "lru-cache"; + +const gapiBase = 'https://www.googleapis.com/calendar/v3'; + +enum GApiError { + invalidSyncToken = "invalidSyncToken", + notLoggedIn = "notLoggedIn", + notLoggedOut = "notLoggedOut", + otherError = "otherError", +} + +function to_params(dict: Object) { + return Object.entries(dict).filter(([k, v] : string[]) => v) + .map(([k, v]: string[]) => ( + `${encodeURIComponent(k)}=${encodeURIComponent(v)}` + )).join('&'); +} + +let loggedIn = false; + +function _getAuthToken(interactive = false): Promise<string> { + return new Promise(resolver => + chrome.identity.getAuthToken( + { interactive }, token => resolver([token, !chrome.runtime.lastError]))) + .then(([token, ok] : [string, boolean]) => { + if (ok) return token; + else throw GApiError.notLoggedIn; + }); +} + +function _removeCachedAuthToken(token: string) { + return new Promise(resolver => + chrome.identity.removeCachedAuthToken({ token }, () => resolver())); +} + +export function getLoggedIn() { + if (loggedIn === null) + { + return _getAuthToken(false) + .then(() => loggedIn = true) + .catch(() => loggedIn = false) + .then(() => loggedIn); + } + else return Promise.resolve(loggedIn); +} + +export function getAuthToken(): Promise<string> { + return getLoggedIn().then(b => { + if (b) return _getAuthToken(false); + else throw GApiError.notLoggedIn; + }); +} + +export function login() { + return getLoggedIn().then(b => { + if (!b) return _getAuthToken(true).then(() => loggedIn = true); + else throw GApiError.notLoggedOut; + }); +} + +export async function logout() { + let token = await getAuthToken(); + let response = await fetch( + `https://accounts.google.com/o/oauth2/revoke?${to_params({ token })}`, { method: 'GET' }); + //if (response.status === 200) + await _removeCachedAuthToken(token); + //else throw GApiError.otherError; + loggedIn = false; +} + +export async function getCalendars(token: string) { + let response = await fetch( + `${gapiBase}/users/me/calendarList?${to_params({access_token: token})}`, { method: 'GET' }); + return (await response.json()).items; +} + +export async function getColors(token: string) { + let response = await fetch( + `${gapiBase}/colors?${to_params({access_token: token})}`, { method: 'GET' }); + return response.json(); +} + +async function getEvent(calId: string, eventId: string, token: string) { + let response = await fetch( + `${gapiBase}/calendars/${calId}/events/${eventId}?${to_params({access_token: token})}`, + { method: 'GET' }); + return response.json(); +} + +function getEvents(calId: string, token: string, + syncToken=null as string, + timeMin=null as string, + timeMax=null as string, + resultsPerRequest=100 as number): + Promise<{ results: any[], nextSyncToken: string }> { + let results = [] as any[]; + const singleFetch = async (pageToken: string, syncToken: string): + Promise<{nextSyncToken: string, results: any[]}> => { + let response = await fetch(`${gapiBase}/calendars/${calId}/events?${to_params({ + access_token: token, + pageToken, + syncToken, + timeMin, + timeMax, + maxResults: resultsPerRequest + })}`, { method: 'GET' }); + if (response.status === 200) + { + let data = await response.json(); + results.push(...data.items); + if (data.nextPageToken) { + return singleFetch(data.nextPageToken, ''); + } else { + return ({ + nextSyncToken: data.nextSyncToken, + results + }); + } + } + else if (response.status === 410) + throw GApiError.invalidSyncToken; + else throw GApiError.otherError; + }; + + return singleFetch('', syncToken); +} + +type GCalendarOptions = { + maxCachedItems: number, + nDaysPerSlot: number, + largeQuery: number +}; + +type GCalendarEvent = { + start: Date, + end: Date, + id: string +}; + +type GCalendarSlot = { [id: string]: GCalendarEvent }; + +export class GCalendar { + calId: string; + name: string; + syncToken: string; + cache: LRU<number, GCalendarSlot>; + eventMeta: { [id: string]: { keys: Set<number>, summary: string } }; + options: GCalendarOptions; + divider: number; + + constructor(calId: string, name: string, + options={maxCachedItems: 100, nDaysPerSlot: 10, largeQuery: 10}) { + this.calId = calId; + this.name = name; + this.syncToken = ''; + this.cache = new LRU<number, GCalendarSlot>({ + max: options.maxCachedItems, + dispose: (k, v) => this.onRemoveSlot(k, v) + }); + this.eventMeta = {}; + this.options = options; + this.divider = 8.64e7 * this.options.nDaysPerSlot; + } + + get token() { return getAuthToken(); } + + dateToCacheKey(date: Date) { + return Math.floor(date.getTime() / this.divider); + } + + dateRangeToCacheKeys(range: { start: Date, end: Date }) { + return { + start: this.dateToCacheKey(range.start), + end: this.dateToCacheKey(new Date(range.end.getTime() - 1)) + }; + } + + getSlot(k: number) { + if (!this.cache.has(k)) + { + let res = {}; + this.cache.set(k, res); + return res; + } + else return this.cache.get(k); + } + + onRemoveSlot(k: number, v: GCalendarSlot) { + for (let id in v) { + console.assert(this.eventMeta.hasOwnProperty(id)); + let keys = this.eventMeta[id].keys; + keys.delete(k); + if (keys.size === 0) + delete this.eventMeta[id]; + } + } + + slotStartDate(k: number) { return new Date(k * this.divider); } + slotEndDate(k: number) { return new Date((k + 1) * this.divider); } + + addEvent(e: {start: Date, end: Dat |