Skip to content

Commit 62633b8

Browse files
committed
feat: second-sync with dexcom
1 parent fc64274 commit 62633b8

1 file changed

Lines changed: 73 additions & 22 deletions

File tree

src/index.ts

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
dialog,
88
ipcMain,
99
nativeImage,
10+
powerMonitor,
1011
safeStorage,
1112
shell,
1213
session,
@@ -56,7 +57,7 @@ export type Event =
5657
| { _kind: "error" };
5758

5859
type Glucose = {
59-
timestamp: string;
60+
date: { epoch: number; offset: { hours: number; minutes: number } };
6061
value: number | null;
6162
trend: string;
6263
};
@@ -65,12 +66,15 @@ type Response<T = unknown> =
6566
| { _kind: "ok"; data: T }
6667
| { _kind: "error"; data: unknown }
6768
| { _kind: "wrong-credentials" }
69+
| { _kind: "retry" }
6870
| { _kind: "fail" };
6971

72+
const DEXCOM_DELAY = 20000;
73+
7074
let tray: Tray | undefined;
7175
let preferences: BrowserWindow | undefined;
7276
let state: Session = { email: "", password: "", region: "" };
73-
let loopId: ReturnType<typeof setInterval> | undefined;
77+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
7478
let isAppQuitting = false;
7579
let isFirstLaunch = true;
7680

@@ -119,6 +123,13 @@ app.whenReady().then(() => {
119123

120124
ipcMain.handle("start", start);
121125

126+
powerMonitor.on("resume", () => {
127+
if (!timeoutId) return;
128+
clearTimeout(timeoutId);
129+
timeoutId = undefined;
130+
start_(state);
131+
});
132+
122133
tray = new Tray(nativeImage.createEmpty());
123134
const contextMenu = Menu.buildFromTemplate(menuTemplate());
124135
tray.setContextMenu(contextMenu);
@@ -251,13 +262,13 @@ const start = async (event: Electron.IpcMainInvokeEvent, session: Session) => {
251262
return null;
252263
}
253264

254-
if (loopId) {
255-
clearInterval(loopId);
265+
if (timeoutId) {
266+
clearTimeout(timeoutId);
267+
timeoutId = undefined;
256268
}
257-
const response = await start_(session);
269+
const response = await start_(session, true);
258270
switch (response._kind) {
259271
case "ok":
260-
loopId = setInterval(() => start_(session), 1000 * 60);
261272
hide();
262273
preferences.webContents.send("startResponseReceived", response);
263274
preferences.webContents.executeJavaScript(
@@ -277,20 +288,22 @@ const start = async (event: Electron.IpcMainInvokeEvent, session: Session) => {
277288
}
278289
};
279290

280-
const start_ = async (session: Session): Promise<Event> => {
291+
const start_ = async (session: Session, first = false): Promise<Event> => {
281292
if (!tray) return { _kind: "error" };
282293

283294
const glucose = await getGlucose(session);
284295
switch (glucose._kind) {
285296
case "ok": {
286297
const contextMenu = Menu.buildFromTemplate(
287-
menuTemplate(`Last glucose at ${glucose.data.timestamp}`)
298+
menuTemplate(`Last glucose at ${getTimestamp(glucose.data.date)}`)
288299
);
289300
tray.setContextMenu(contextMenu);
290301
state = { ...state, ...session };
291302
// const RED_FG = '\033[31;1m';
292303
// const RED_BG = '\033[41;1m';
293304
setWatcher(glucose.data);
305+
const delay = getDelay(glucose.data.date);
306+
timeoutId = setTimeout(() => start_(session), delay);
294307
return { _kind: "ok" };
295308
}
296309
case "no-glucose-in-5-minutes": {
@@ -299,6 +312,7 @@ const start_ = async (session: Session): Promise<Event> => {
299312
);
300313
tray.setContextMenu(contextMenu);
301314
setWatcher();
315+
timeoutId = setTimeout(() => start_(session), 1000 * 60);
302316
return { _kind: "ok" };
303317
}
304318
case "wrong-credentials": {
@@ -319,6 +333,17 @@ const start_ = async (session: Session): Promise<Event> => {
319333
setWatcher();
320334
return { _kind: "error" };
321335
}
336+
case "retry": {
337+
if (first) return { _kind: "error" };
338+
339+
const contextMenu = Menu.buildFromTemplate(
340+
menuTemplate(`Lost signal. Retrying..`)
341+
);
342+
tray.setContextMenu(contextMenu);
343+
setWatcher();
344+
timeoutId = setTimeout(() => start_(session), 1000 * 60);
345+
return { _kind: "error" };
346+
}
322347
}
323348
};
324349

@@ -414,6 +439,8 @@ const getSessionId = async ({
414439
return null;
415440
case "fail":
416441
return null;
442+
case "retry":
443+
return null;
417444
}
418445
};
419446

@@ -452,12 +479,13 @@ const getEstimatedGlucoseValues = async ({
452479

453480
return data
454481
.map((glucose) => {
455-
const timestamp = convertToLocalTime(glucose.DT);
456-
if (!timestamp) return null;
482+
const date = parseDate(glucose.DT);
483+
if (!date) return null;
484+
if (nextValueAt(date) < 0) return null;
457485
return {
458486
value: glucose.Value,
459487
trend: glucose.Trend,
460-
timestamp: timestamp,
488+
date,
461489
};
462490
})
463491
.filter(notNull);
@@ -482,21 +510,37 @@ const post = async (
482510
return { _kind: "error", data: json };
483511
}
484512
} catch (error) {
513+
const parsed = parseError(error);
514+
if (parsed.code === "ENETDOWN") return { _kind: "retry" };
515+
if (parsed.code === "ENOTFOUND") return { _kind: "retry" };
516+
if (parsed.code === "UND_ERR_CONNECT_TIMEOUT") return { _kind: "retry" };
485517
return { _kind: "fail" };
486518
}
487519
};
488520

489-
const convertToLocalTime = (dt: string): string | null => {
490-
const [_1, epochWithTz] = dt.match(/Date\((.+)\)/) || [];
491-
if (!epochWithTz) return null;
492-
const [_2, epoch, sign, offset] = epochWithTz.match(/(\d+)([-+])(\d+)/) || [];
493-
if (!epoch || !sign || !offset) return null;
494-
const date = new Date(parseInt(epoch, 10));
495-
const iso =
496-
date.toISOString().slice(0, -1) + (sign === "-" ? "+" : "-") + offset;
497-
const local = new Date(iso).toISOString().slice(0, -1) + `${sign}${offset}`;
498-
const [_3, timestamp] = local.match(/.+T(\d\d:\d\d):.+/) || [];
499-
return timestamp || null;
521+
const parseError = (error: unknown): { code: string } => {
522+
if (!error) return { code: "WHATEVER" };
523+
if (typeof error !== "object") return { code: "WHATEVER" };
524+
if (!("cause" in error)) return { code: "WHATEVER" };
525+
const cause = (error as { cause: Record<string, unknown> }).cause;
526+
return { code: (cause as { code: string }).code };
527+
};
528+
529+
const parseDate = (dt: string): null | Glucose["date"] => {
530+
const [_1, epoch, hours, minutes] = (
531+
dt.match(/Date\((\d+)\+(\d\d)(\d\d)\)/) || []
532+
).map((x) => parseInt(x, 10));
533+
if (epoch === undefined || hours === undefined || minutes === undefined) {
534+
return null;
535+
}
536+
return { epoch, offset: { hours, minutes } };
537+
};
538+
539+
const getTimestamp = (date: Glucose["date"]): string => {
540+
const offset = (date.offset.hours * 60 + date.offset.minutes) * 60 * 1000;
541+
const local = new Date(date.epoch + offset);
542+
const [_, timestamp] = local.toISOString().match(/.+T(\d\d:\d\d):.+/) || [];
543+
return timestamp as string;
500544
};
501545

502546
const host = (region: Exclude<Region, "">): string => {
@@ -694,3 +738,10 @@ const retrieveSession = async (preferences: BrowserWindow) => {
694738
isFirstLaunch = false;
695739
preferences.webContents.send("retrievedSession", state);
696740
};
741+
742+
const getDelay = (date: Glucose["date"]): number => {
743+
return Math.max(5000, nextValueAt(date));
744+
};
745+
746+
const nextValueAt = (date: Glucose["date"]): number =>
747+
date.epoch + 60 * 5 * 1000 + DEXCOM_DELAY - Date.now();

0 commit comments

Comments
 (0)