aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDeterminant <ted.sybil@gmail.com>2019-02-17 21:45:55 -0500
committerDeterminant <ted.sybil@gmail.com>2019-02-17 21:45:55 -0500
commite285bc7722f776155b7ae4477bfa7f8c6601a22b (patch)
tree492ab0d880ebb78826b9cd253282664b2882b31c /src
parentea3cbbe3fab9ac6882d4d25c258cb175081c66d7 (diff)
support offline cache for calendar events
Diffstat (limited to 'src')
-rw-r--r--src/background.ts72
-rw-r--r--src/gapi.ts131
2 files changed, 166 insertions, 37 deletions
diff --git a/src/background.ts b/src/background.ts
index e4e2824..39c10c8 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -18,6 +18,8 @@ let config = {
};
let mainGraphData: GraphData[] = [];
let dirtyMetadata = false;
+let dirtyCalData = false;
+let loadPromise: Promise<void> = null;
enum ChromeError {
storageGetError = "storageGetError",
@@ -45,13 +47,13 @@ async function loadMetadata() {
console.log("no saved metadata");
else
{
- console.log('metadata loaded');
config = {
trackedPeriods: items.config.trackedPeriods.map((p: TrackedPeriodFlat) => TrackedPeriod.inflate(p))
};
calendars = items.calendars;
mainPatterns = items.mainPatterns.map((p: PatternEntryFlat) => PatternEntry.inflate(p));
analyzePatterns = items.analyzePatterns.map((p: PatternEntryFlat) => PatternEntry.inflate(p));
+ console.log('metadata loaded');
}
} catch (_) {
console.error("error while loading saved metadata");
@@ -70,16 +72,53 @@ async function saveMetadata() {
console.log('metadata saved');
}
+async function loadCachedCals() {
+ try {
+ let items = await chromeStorageGet(['calData']);
+ if (!items.hasOwnProperty('calData'))
+ console.log("no cached cals");
+ else
+ {
+ let calDataFlat: {[id: string]: gapi.GCalendarFlat} = items.calData;
+ console.log(calDataFlat);
+ for (let id in calDataFlat) {
+ calData[id] = gapi.GCalendar.inflate(calDataFlat[id]);
+ }
+ console.log("cached cals loaded");
+ }
+ } catch (e) {
+ console.log(e);
+ console.error("error while loading cached cals");
+ }
+}
+
async function saveCachedCals() {
+ let calDataFlat: {[id: string]: gapi.GCalendarFlat} = {};
+ for (let id in calData) {
+ if (!(calendars.hasOwnProperty(id) &&
+ calendars[id].enabled)) continue;
+ calDataFlat[id] = calData[id].deflate();
+ }
+ try {
+ await chromeStorageSet({ calData: calDataFlat });
+ console.log('cached data saved');
+ } catch (_) {
+ console.log("failed to save cached data");
+ }
}
-async function getCalEvents(id: string, start: Date, end: Date) {
+function getCalData(id: string) {
if (!calData.hasOwnProperty(id))
calData[id] = new gapi.GCalendar(id, calendars[id].name);
+ return calData[id];
+}
+
+async function getCalEvents(id: string, start: Date, end: Date) {
+ let gcal = getCalData(id);
try {
- let res = await calData[id].getEvents(new Date(start), new Date(end));
- console.log(res);
- return res;
+ let res = await gcal.getEvents(new Date(start), new Date(end));
+ dirtyCalData = res.changed;
+ return res.events;
} catch(err) {
console.log(`cannot load calendar ${id}`, err);
calendars[id].enabled = false;
@@ -126,9 +165,23 @@ function updateMainGraphData() {
async function pollSync() {
console.log('poll');
+ /* sync all enabled calendars */
+ let pms = [];
+ for (let id in calendars) {
+ if (!calendars[id].enabled) continue;
+ pms.push(getCalData(id).sync());
+ }
+ await Promise.all(pms);
+ /* update the tracked graph data */
await updateMainGraphData();
+ pms = [];
+ /* save the storage if state is changed */
if (dirtyMetadata)
- await saveMetadata().then(() => dirtyMetadata = false);
+ pms.push(saveMetadata().then(() => dirtyMetadata = false));
+ if (dirtyCalData)
+ pms.push(saveCachedCals().then(() => dirtyCalData = false));
+ await Promise.all(pms);
+ /* setup the next loop */
return new Promise(resolver => (
window.setTimeout(() => { resolver(); pollSync();}, 10000)
));
@@ -204,7 +257,10 @@ function handleMsg(port: chrome.runtime.Port) {
(async () => {
await (msg.data.sync ? updateMainGraphData().then(() => {}) : Promise.resolve());
if (mainGraphData.length === 0)
+ {
+ await loadPromise;
await updateMainGraphData();
+ }
port.postMessage(msg.genResp(mainGraphData.map(d => ({
name: d.name,
start: d.start.toISOString(),
@@ -219,8 +275,8 @@ function handleMsg(port: chrome.runtime.Port) {
});
}
-(async () => {
- await loadMetadata();
+loadPromise = (async () => {
+ await Promise.all([loadMetadata(), loadCachedCals()]);
pollSync();
})();
diff --git a/src/gapi.ts b/src/gapi.ts
index d601e64..2252230 100644
--- a/src/gapi.ts
+++ b/src/gapi.ts
@@ -147,15 +147,39 @@ export type GCalendarOptions = {
largeQuery: number
};
-type Event = {
- start: Date,
- end: Date,
+type EventFlat = {
+ start: number,
+ end: number,
id: string
};
+class Event {
+ start: Date;
+ end: Date;
+ id: string;
+
+ constructor(start: Date, end: Date, id: string) {
+ this.start = start;
+ this.end = end;
+ this.id = id;
+ }
+
+ deflate() {
+ return {
+ start: this.start.getTime(),
+ end: this.end.getTime(),
+ id: this.id
+ };
+ }
+
+ static inflate = (obj: EventFlat) => (
+ new Event(new Date(obj.start), new Date(obj.end), obj.id)
+ );
+}
+
export type GCalendarEventFlat = {
- start: string,
- end: string,
+ start: number,
+ end: number,
id: string,
summary: string
};
@@ -175,8 +199,8 @@ export class GCalendarEvent {
deflate() {
return {
- start: this.start.toISOString(),
- end: this.end.toISOString(),
+ start: this.start.getTime(),
+ end: this.end.getTime(),
id: this.id,
summary: this.summary
};
@@ -188,6 +212,17 @@ export class GCalendarEvent {
}
type GCalendarSlot = { [id: string]: Event };
+type GCalendarSlotFlat = { [id: string]: EventFlat };
+
+export type GCalendarFlat = {
+ calId: string,
+ name: string,
+ syncToken: string,
+ cache: {k: number, v: GCalendarSlotFlat, e: number}[],
+ eventMeta: { [id: string]: { keys: number[], summary: string } },
+ options: GCalendarOptions,
+ divider: number
+};
export class GCalendar {
calId: string;
@@ -212,6 +247,53 @@ export class GCalendar {
this.divider = 8.64e7 * this.options.nDaysPerSlot;
}
+ deflate() {
+ let cache = this.cache.dump().map(t => {
+ let slot: GCalendarSlotFlat = {};
+ for (let id in t.v)
+ slot[id] = t.v[id].deflate();
+ return { k: t.k, v: slot, e: t.e };
+ });
+
+ let eventMeta: { [id: string]: { keys: number[], summary: string } } = {};
+ for (let id in this.eventMeta) {
+ let m = this.eventMeta[id];
+ eventMeta[id] = { keys: Array.from(m.keys), summary: m.summary };
+ }
+
+ return {
+ calId: this.calId,
+ name: this.name,
+ syncToken: this.syncToken,
+ cache,
+ eventMeta,
+ options: this.options,
+ divider: this.divider
+ }
+ }
+
+ static inflate(obj: GCalendarFlat) {
+ let cache = obj.cache.map(t => {
+ let slot: GCalendarSlot = {};
+ for (let id in t.v)
+ slot[id] = Event.inflate(t.v[id]);
+ return { k: t.k, v: slot, e: t.e };
+ });
+
+ let eventMeta: { [id: string]: { keys: Set<number>, summary: string } } = {};
+ for (let id in obj.eventMeta) {
+ let m = obj.eventMeta[id];
+ eventMeta[id] = { keys: new Set(m.keys), summary: m.summary };
+ }
+
+ let gcal = new GCalendar(obj.calId, obj.name, obj.options);
+ gcal.syncToken = obj.syncToken;
+ gcal.cache.load(cache);
+ gcal.eventMeta = eventMeta;
+ gcal.divider = obj.divider;
+ return gcal;
+ }
+
get token() { return getAuthToken(); }
dateToCacheKey(date: Date) {
@@ -268,25 +350,13 @@ export class GCalendar {
};
if (!evict && t > this.options.maxCachedItems) return;
if (ks === ke)
- this.getSlot(ks)[e.id] = {
- start: e.start,
- end: e.end,
- id: e.id };
+ this.getSlot(ks)[e.id] = new Event(e.start, e.end, 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 };
+ this.getSlot(ks)[e.id] = new Event(e.start, this.slotEndDate(ks), e.id);
+ this.getSlot(ke)[e.id] = new Event(this.slotStartDate(ke), e.end, 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};
+ this.getSlot(k)[e.id] = new Event(this.slotStartDate(k), this.slotEndDate(k), e.id);
}
}
@@ -357,7 +427,7 @@ export class GCalendar {
}
}
- async getEvents(start: Date, end: Date): Promise<GCalendarEvent[]> {
+ async getEvents(start: Date, end: Date, sync = false): Promise<{ events: GCalendarEvent[], changed: boolean }> {
let r = this.dateRangeToCacheKeys({ start, end });
let query = {
start: null as number,
@@ -379,7 +449,7 @@ export class GCalendar {
let token = await this.token;
let r = await getEvents(this.calId, token, null,
start.toISOString(), end.toISOString());
- return r.results.map(e => {
+ let events = r.results.map(e => {
console.assert(e.start);
e.start = new Date(e.start.dateTime);
e.end = new Date(e.end.dateTime);
@@ -391,6 +461,7 @@ export class GCalendar {
e.id,
e.summary)
));
+ return { events, changed: false };
}
console.log(`fetching short event list`);
@@ -409,14 +480,16 @@ export class GCalendar {
});
if (this.syncToken === '')
this.syncToken = r.nextSyncToken;
- await this.sync();
- return this.getCachedEvents({ start, end });
+ if (sync) await this.sync();
+ let events = await this.getCachedEvents({ start, end });
+ return { events, changed: true };
}
else
{
console.log(`cache hit`);
- await this.sync();
- return this.getCachedEvents({ start, end });
+ if (sync) await this.sync();
+ let events = await this.getCachedEvents({ start, end });
+ return { events, changed: false };
}
}
}