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
5859type 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+
7074let tray : Tray | undefined ;
7175let preferences : BrowserWindow | undefined ;
7276let state : Session = { email : "" , password : "" , region : "" } ;
73- let loopId : ReturnType < typeof setInterval > | undefined ;
77+ let timeoutId : ReturnType < typeof setTimeout > | undefined ;
7478let isAppQuitting = false ;
7579let 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 ( / D a t e \( ( .+ ) \) / ) || [ ] ;
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 ( / D a t e \( ( \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
502546const 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