From 1583d5fbf3e9d70739519ca778f108939e6949c7 Mon Sep 17 00:00:00 2001 From: Ruslan Konviser Date: Sat, 30 Mar 2024 21:36:52 +0100 Subject: [PATCH] fix: more fixes in Desktop Timer --- apps/desktop-timer/package.json | 3 +- apps/desktop-timer/src/package.json | 3 +- apps/desktop/package.json | 4 +- apps/desktop/src/package.json | 3 +- .../services/server-connection.service.ts | 55 ++--- apps/server/package.json | 4 +- apps/server/src/package.json | 3 +- packages/desktop-libs/package.json | 3 +- .../src/lib/contexts/network-state-manager.ts | 2 + packages/desktop-libs/src/lib/desktop-ipc.ts | 177 +++++++++------ .../src/lib/desktop-os-inactivity-handler.ts | 2 + .../desktop-libs/src/lib/desktop-timer.ts | 208 ++++++++++-------- .../offline/desktop-offline-mode-handler.ts | 61 ++++- .../src/lib/offline/services/timer.service.ts | 17 +- .../concretes/activity-watch-connectivity.ts | 14 +- .../concretes/api-server-connectivity.ts | 29 ++- .../states/concretes/internet-connectivity.ts | 16 +- .../offline-sync/concretes/sequence-queue.ts | 77 ++++--- .../lib/services/server-connection.service.ts | 79 ++++--- .../time-tracker/time-tracker.component.ts | 92 +++++--- yarn.lock | 5 + 21 files changed, 548 insertions(+), 309 deletions(-) diff --git a/apps/desktop-timer/package.json b/apps/desktop-timer/package.json index f87f083a9ca..f4185afcbf3 100644 --- a/apps/desktop-timer/package.json +++ b/apps/desktop-timer/package.json @@ -86,7 +86,8 @@ "sqlite3": "^5.1.7", "squirrelly": "^8.0.8", "twing": "^5.0.2", - "underscore": "^1.13.3" + "underscore": "^1.13.3", + "undici": "^6.10.2" }, "optionalDependencies": { "node-linux": "^0.1.12", diff --git a/apps/desktop-timer/src/package.json b/apps/desktop-timer/src/package.json index 66dba5ba827..7467fec317f 100644 --- a/apps/desktop-timer/src/package.json +++ b/apps/desktop-timer/src/package.json @@ -166,6 +166,7 @@ "squirrelly": "^8.0.8", "tslib": "^2.3.0", "twing": "^5.0.2", - "underscore": "^1.13.3" + "underscore": "^1.13.3", + "undici": "^6.10.2" } } \ No newline at end of file diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a637b8443db..4897df2db9e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -77,6 +77,7 @@ "moment-duration-format": "^2.3.2", "moment-range": "^4.0.2", "moment-timezone": "^0.5.40", + "node-fetch": "^2.6.7", "node-static": "^0.7.11", "pg": "^8.11.3", "pg-query-stream": "^4.5.3", @@ -86,7 +87,8 @@ "sqlite3": "^5.1.7", "squirrelly": "^8.0.8", "twing": "^5.0.2", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "undici": "^6.10.2" }, "optionalDependencies": { "node-linux": "^0.1.12", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 4147a92d466..30dc3501f5f 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -178,7 +178,8 @@ "underscore": "^1.13.3", "screenshot-desktop": "^1.12.7", "mac-screen-capture-permissions": "^2.0.0", - "embedded-queue": "^0.0.11" + "embedded-queue": "^0.0.11", + "undici": "^6.10.2" }, "optionalDependencies": { "node-linux": "^0.1.12", diff --git a/apps/gauzy/src/app/@core/services/server-connection.service.ts b/apps/gauzy/src/app/@core/services/server-connection.service.ts index d029de30d61..45b0940178c 100644 --- a/apps/gauzy/src/app/@core/services/server-connection.service.ts +++ b/apps/gauzy/src/app/@core/services/server-connection.service.ts @@ -10,37 +10,42 @@ export class ServerConnectionService { const url = `${endPoint}/api`; return new Promise((resolve, reject) => { + console.log(`Checking server connection on URL in ServerConnectionService in @core/services: ${url}`); + try { - console.log(`Checking server connection on URL in ServerConnectionService in @core/services: ${url}`); + if (endPoint !== 'http://localhost:3000') { + const requestObservable = this.httpClient.get(url); - const requestObservable = this.httpClient.get(url); + if (!requestObservable) { + console.error('Failed to create an Observable from the HTTP request.'); + reject('Failed to create an Observable from the HTTP request.'); + return; + } - if (!requestObservable) { - console.error('Failed to create an Observable from the HTTP request.'); - reject('Failed to create an Observable from the HTTP request.'); - return; + requestObservable.subscribe({ + next: (resp: any) => { + console.log( + `Server connection status in ServerConnectionService for URL ${url} is: ${resp.status}` + ); + this.store.serverConnection = resp.status; + resolve(true); + }, + error: (err) => { + console.error( + `Error checking server connection in ServerConnectionService for URL ${url}`, + err + ); + this.store.serverConnection = err.status; + reject(err); + } + }); + } else { + console.log(`Skip checking server connection for URL ${url}`); + resolve(true); } - - requestObservable.subscribe({ - next: (resp: any) => { - console.log( - `Server connection status in ServerConnectionService for URL ${url} is: ${resp.status}` - ); - this.store.serverConnection = resp.status; - resolve(true); - }, - error: (err) => { - console.error( - `Error checking server connection in ServerConnectionService for URL ${url}`, - err - ); - this.store.serverConnection = err.status; - reject(); - } - }); } catch (error) { console.error(`Error checking server connection in ServerConnectionService for URL ${url}`, error); - reject(); + reject(error); } }); } diff --git a/apps/server/package.json b/apps/server/package.json index 6abd31f774e..4ff661302bd 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -76,13 +76,15 @@ "moment-range": "^4.0.2", "moment-timezone": "^0.5.40", "node-static": "^0.7.11", + "node-fetch": "^2.6.7", "pg": "^8.11.3", "pg-query-stream": "^4.5.3", "rxjs": "^7.4.0", "sqlite3": "^5.1.7", "squirrelly": "^8.0.8", "twing": "^5.0.2", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "undici": "^6.10.2" }, "optionalDependencies": { "node-linux": "^0.1.12", diff --git a/apps/server/src/package.json b/apps/server/src/package.json index 026867855ec..25d86f2845d 100755 --- a/apps/server/src/package.json +++ b/apps/server/src/package.json @@ -173,7 +173,8 @@ "squirrelly": "^8.0.8", "tslib": "^2.3.0", "twing": "^5.0.2", - "underscore": "^1.13.3" + "underscore": "^1.13.3", + "undici": "^6.10.2" }, "optionalDependencies": { "node-linux": "^0.1.12", diff --git a/packages/desktop-libs/package.json b/packages/desktop-libs/package.json index fe1c5a78793..20fe950e83f 100644 --- a/packages/desktop-libs/package.json +++ b/packages/desktop-libs/package.json @@ -48,7 +48,8 @@ "screenshot-desktop": "^1.12.7", "sound-play": "1.1.0", "sqlite3": "^5.1.7", - "underscore": "^1.13.3" + "underscore": "^1.13.3", + "undici": "^6.10.2" }, "devDependencies": { "@types/node": "^17.0.33", diff --git a/packages/desktop-libs/src/lib/contexts/network-state-manager.ts b/packages/desktop-libs/src/lib/contexts/network-state-manager.ts index ac0effb290b..53bc31ff214 100644 --- a/packages/desktop-libs/src/lib/contexts/network-state-manager.ts +++ b/packages/desktop-libs/src/lib/contexts/network-state-manager.ts @@ -2,9 +2,11 @@ import { INetworkState } from '../interfaces'; export class NetworkStateManager { private _state: INetworkState; + constructor(state?: INetworkState) { this._state = state; } + public set state(value: INetworkState) { this._state = value; } diff --git a/packages/desktop-libs/src/lib/desktop-ipc.ts b/packages/desktop-libs/src/lib/desktop-ipc.ts index b77cf257ef0..fa0e151214d 100644 --- a/packages/desktop-libs/src/lib/desktop-ipc.ts +++ b/packages/desktop-libs/src/lib/desktop-ipc.ts @@ -6,7 +6,6 @@ import { notifyScreenshot, takeshot } from './desktop-screenshot'; import { resetPermissions } from 'mac-screen-capture-permissions'; import * as _ from 'underscore'; import { ScreenCaptureNotification, loginPage } from '@gauzy/desktop-window'; -// Import logging for electron and override default console logging import log from 'electron-log'; import NotificationDesktop from './desktop-notifier'; import { DesktopPowerManager } from './desktop-power-manager'; @@ -74,8 +73,8 @@ export function ipcMainHandler(store, startServer, knex, config, timeTrackerWind try { log.info('Return Time Sheet'); - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { type: 'update-timer-time-slot', data: { @@ -96,8 +95,8 @@ export function ipcMainHandler(store, startServer, knex, config, timeTrackerWind try { log.info('Return Toggle API'); - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { type: 'update-timer-time-slot', data: { @@ -116,13 +115,17 @@ export function ipcMainHandler(store, startServer, knex, config, timeTrackerWind ipcMain.on('failed_synced_timeslot', async (event, arg) => { try { log.info('Failed Synced TimeSlot'); + const interval = new Interval(arg.params); interval.screenshots = arg.params.b64Imgs; interval.stoppedAt = new Date(); interval.synced = false; interval.timerId = timerHandler.lastTimer?.id; + await intervalService.create(interval.toObject()); + await countIntervalQueue(timeTrackerWindow, false); + await latestScreenshots(timeTrackerWindow); } catch (error) { log.info('Error on failed synced TimeSlot', error); @@ -241,46 +244,61 @@ export function ipcMainHandler(store, startServer, knex, config, timeTrackerWind log.info('Update Synced Timer'); try { if (!arg.id) { - const { id } = await timerService.findLastCapture(); - arg = { - ...arg, - id - }; + const lastCapture = await timerService.findLastCapture(); + + if (lastCapture && lastCapture.id) { + const { id } = lastCapture; + arg = { + ...arg, + id + }; + } } - if (!offlineMode.enabled) { - await timerService.update( - new Timer({ - id: arg.id, - synced: true, - ...(arg.lastTimer && { - timelogId: arg.lastTimer?.id, - timesheetId: arg.lastTimer?.timesheetId - }), - ...(arg.lastTimer?.startedAt && { - startedAt: new Date(arg.lastTimer?.startedAt) - }), - ...(!arg.lastTimer && { - synced: false, - isStartedOffline: arg.isStartedOffline, - isStoppedOffline: arg.isStoppedOffline - }), - ...(arg.timeSlotId && { - timeslotId: arg.timeSlotId + + if (arg.id) { + if (!offlineMode.enabled) { + console.log('Update Synced Timer Online'); + await timerService.update( + new Timer({ + id: arg.id, + synced: true, + ...(arg.lastTimer && { + timelogId: arg.lastTimer?.id, + timesheetId: arg.lastTimer?.timesheetId + }), + ...(arg.lastTimer?.startedAt && { + startedAt: new Date(arg.lastTimer?.startedAt) + }), + ...(!arg.lastTimer && { + synced: false, + isStartedOffline: arg.isStartedOffline, + isStoppedOffline: arg.isStoppedOffline + }), + ...(arg.timeSlotId && { + timeslotId: arg.timeSlotId + }) }) - }) - ); - } else { - await timerService.update( - new Timer({ - id: arg.id, - ...(arg.startedAt && { - startedAt: new Date(arg.startedAt) + ); + } else { + console.log('Update Synced Timer Offline'); + await timerService.update( + new Timer({ + id: arg.id, + ...(arg.startedAt && { + startedAt: new Date(arg.startedAt) + }) }) - }) - ); + ); + } + } else { + console.log('No arg.id found'); } + + console.log('Count Interval Queue'); await countIntervalQueue(timeTrackerWindow, true); + if (!isQueueThreadTimerLocked) { + console.log('sequentialSyncQueue'); await sequentialSyncQueue(timeTrackerWindow); } } catch (error) { @@ -517,11 +535,11 @@ export function ipcTimer( if (!collections.length) return; try { - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { - data: collections, - type: ActivityWatchEventTableList.WINDOW + type: ActivityWatchEventTableList.WINDOW, + data: collections }, knex ); @@ -537,11 +555,11 @@ export function ipcTimer( if (!collections.length) return; try { - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { - data: collections, - type: ActivityWatchEventTableList.AFK + type: ActivityWatchEventTableList.AFK, + data: collections }, knex ); @@ -557,11 +575,11 @@ export function ipcTimer( if (!collections.length) return; try { - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { - data: collections, - type: ActivityWatchEventTableList.FIREFOX + type: ActivityWatchEventTableList.FIREFOX, + data: collections }, knex ); @@ -577,11 +595,11 @@ export function ipcTimer( if (!collections.length) return; try { - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { - data: collections, - type: ActivityWatchEventTableList.CHROME + type: ActivityWatchEventTableList.CHROME, + data: collections }, knex ); @@ -603,8 +621,8 @@ export function ipcTimer( ActivityWatchEventManager.onRemoveLocalData(async (_, value: any) => { try { - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { type: 'remove-window-events' }, @@ -636,11 +654,11 @@ export function ipcTimer( const collections: IDesktopEvent[] = ActivityWatchEventAdapter.collections(result); if (!collections.length) return; try { - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { - data: collections, - type: ActivityWatchEventTableList.EDGE + type: ActivityWatchEventTableList.EDGE, + data: collections }, knex ); @@ -653,8 +671,8 @@ export function ipcTimer( ipcMain.on('remove_wakatime_local_data', async (event, arg) => { try { if (arg.idsWakatime && arg.idsWakatime.length > 0) { - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { type: 'remove-wakatime-events', data: arg.idsWakatime @@ -674,22 +692,43 @@ export function ipcTimer( // Check api connection before to stop if (!arg.isEmergency) { - log.info('Check API Connection Before Stop Timer...'); - await offlineMode.connectivity(); + // We check connectivity before stop timer, but we don't block the process for now... + // Instead, we should notify the user that timer might not stop correctly and retry stop the timer after connection to API is restored + setTimeout(async () => { + log.info('Check API Connection During Stop Timer...'); + + await offlineMode.connectivity(); + + if (offlineMode.enabled) { + console.log('Offline Mode: Timer might not stop correctly'); + timeTrackerWindow.webContents.send('emergency_stop'); + + // We may want to show some notification to user that timer might not stop correctly, but not with Error, more like notification popup + // throw new Error('Cannot establish connection to API during Timer Stop'); + } else { + console.log('API working well During Stop Timer'); + } + }, 10); } + console.log('Continue stopping timer ...'); + // Stop Timer const timerResponse = await timerHandler.stopTimer(setupWindow, timeTrackerWindow, knex, arg.quitApp); + console.log('Timer Stopped ...'); + settingWindow.webContents.send('app_setting_update', { setting: LocalStore.getStore('appSetting') }); if (powerManagerPreventSleep) { + console.log('Stop Prevent Display Sleep'); powerManagerPreventSleep.stop(); } if (powerManagerDetectInactivity) { + console.log('Stop Inactivity Detection'); powerManagerDetectInactivity.stopInactivityDetection(); } @@ -705,14 +744,14 @@ export function ipcTimer( try { log.info(`Return To Timeslot Last Timeslot ID: ${arg.timeSlotId} and Timer ID: ${arg.timerId}`); - await timerHandler.createQueue( - 'sqlite-queue', + await timerHandler.processWithQueue( + `gauzy-queue`, { + type: 'update-timer-time-slot', data: { id: arg.timerId, timeSlotId: arg.timeSlotId - }, - type: 'update-timer-time-slot' + } }, knex ); diff --git a/packages/desktop-libs/src/lib/desktop-os-inactivity-handler.ts b/packages/desktop-libs/src/lib/desktop-os-inactivity-handler.ts index 4fc1651eca4..4d6900c9251 100644 --- a/packages/desktop-libs/src/lib/desktop-os-inactivity-handler.ts +++ b/packages/desktop-libs/src/lib/desktop-os-inactivity-handler.ts @@ -170,7 +170,9 @@ export class DesktopOsInactivityHandler { public async updateViewOffline(params: any): Promise { const offlineMode = DesktopOfflineModeHandler.instance; + await offlineMode.connectivity(); + if (offlineMode.enabled) { this._powerManager.window.webContents.send('update_view', params); } diff --git a/packages/desktop-libs/src/lib/desktop-timer.ts b/packages/desktop-libs/src/lib/desktop-timer.ts index 361ffbf7006..5f26800ff02 100644 --- a/packages/desktop-libs/src/lib/desktop-timer.ts +++ b/packages/desktop-libs/src/lib/desktop-timer.ts @@ -44,7 +44,6 @@ export default class TimerHandler { listener = false; nextScreenshot = 0; queue: any = null; - queueType: any = {}; appName = app.getName(); _eventCounter = new DesktopEventCounter(); @@ -81,7 +80,9 @@ export default class TimerHandler { this._activeWindow.start(); const appSetting = LocalStore.getStore('appSetting'); + appSetting.timerStarted = true; + LocalStore.updateApplicationSetting(appSetting); this.notificationDesktop.timerActionNotification(true); @@ -138,8 +139,8 @@ export default class TimerHandler { try { const appSetting = LocalStore.getStore('appSetting'); - await this.createQueue( - `sqlite-queue-${process.env.NAME}`, + await this.processWithQueue( + `gauzy-queue`, { type: 'update-duration-timer', data: { @@ -290,6 +291,7 @@ export default class TimerHandler { } updateToggle(setupWindow, knex, isStop) { + console.log('Update Toggle Timer'); const params: any = { ...LocalStore.beforeRequestParams() }; @@ -297,6 +299,7 @@ export default class TimerHandler { if (isStop) params.manualTimeSlot = true; setupWindow.webContents.send('update_toggle_timer', params); + console.log('Update Toggle Timer End'); } /* @@ -545,7 +548,7 @@ export default class TimerHandler { } async stopTimer(setupWindow, timeTrackerWindow, knex, quitApp) { - console.log('Stop Timer'); + console.log('TimerHandler -> Stop Timer'); const appSetting = LocalStore.getStore('appSetting'); @@ -654,105 +657,128 @@ export default class TimerHandler { } } - async createQueue(type, data, knex) { + private async ProcessQueueMessage(job, knex) { + await new Promise(async (resolve) => { + const windowService = new ActivityWatchWindowService(); + + const typeJob = job.data.type; + + try { + switch (typeJob) { + case ActivityWatchEventTableList.WINDOW: + { + console.log('Processing Window Event'); + await windowService.save(job.data.data); + } + break; + + case ActivityWatchEventTableList.AFK: + { + console.log('Processing AFK Event'); + const afkService = new ActivityWatchAfkService(); + await afkService.save(job.data.data); + } + break; + + case ActivityWatchEventTableList.CHROME: + { + console.log('Processing Chrome Event'); + const chromeService = new ActivityWatchChromeService(); + await chromeService.save(job.data.data); + } + break; + + case ActivityWatchEventTableList.FIREFOX: + { + console.log('Processing Firefox Event'); + const firefoxService = new ActivityWatchFirefoxService(); + await firefoxService.save(job.data.data); + } + break; + + case ActivityWatchEventTableList.EDGE: + { + console.log('Processing Edge Event'); + const edgeService = new ActivityWatchEdgeService(); + await edgeService.save(job.data.data); + } + break; + + case 'remove-window-events': + console.log('Removing Window Events'); + await windowService.clear(); + break; + + case 'remove-wakatime-events': + console.log('Removing Wakatime Events'); + await metaData.removeActivity(knex, { + idsWakatime: job.data.data + }); + break; + + case 'update-duration-timer': + const pUpdate = { + id: job.data.data.id, + duration: job.data.data.duration, + ...(this._offlineMode.enabled && { synced: false }) + }; + + console.log( + `Updating Timer Duration ${JSON.stringify(pUpdate)} - Offline Mode: ${ + this._offlineMode.enabled + }` + ); + + await this._timerService.update(new Timer(pUpdate)); + + break; + + case 'update-timer-time-slot': + const pUpdateSlot = { + id: job.data.data.id, + timeslotId: job.data.data.timeSlotId, + timesheetId: job.data.data.timeSheetId + }; + + console.log(`Updating Timer Time Slot ${JSON.stringify(pUpdateSlot)}`); + + await this._timerService.update(new Timer(pUpdateSlot)); + + break; + + default: + console.log('Unknown Job Type'); + break; + } + + resolve(true); + } catch (error) { + console.error('failed insert window activity', error); + resolve(false); + } + }); + } + + async processWithQueue(type, data, knex) { const queName = `${type}-${this.appName}`; - console.log(`createQueue Called for ${queName}`); + console.log(`processWithQueue Called for ${queName}`); if (!this.queue) { - console.log(`Creating Queue ${queName}`); + console.log(`Initializing Queue ${queName}`); + this.queue = await EmbeddedQueue.Queue.createQueue({ inMemoryOnly: true }); - } - if (!this.queueType[queName]) { - this.queueType[queName] = this.queue; + console.log(`Queue initialized ${queName}`); this.queue.process( queName, async (job) => { console.log(`Processing Job for ${queName}`); - await new Promise(async (resolve) => { - const windowService = new ActivityWatchWindowService(); - const typeJob = job.data.type; - try { - switch (typeJob) { - case ActivityWatchEventTableList.WINDOW: - { - console.log('Processing Window Event'); - await windowService.save(job.data.data); - } - break; - case ActivityWatchEventTableList.AFK: - { - console.log('Processing AFK Event'); - const afkService = new ActivityWatchAfkService(); - await afkService.save(job.data.data); - } - break; - case ActivityWatchEventTableList.CHROME: - { - console.log('Processing Chrome Event'); - const chromeService = new ActivityWatchChromeService(); - await chromeService.save(job.data.data); - } - break; - case ActivityWatchEventTableList.FIREFOX: - { - console.log('Processing Firefox Event'); - const firefoxService = new ActivityWatchFirefoxService(); - await firefoxService.save(job.data.data); - } - break; - case ActivityWatchEventTableList.EDGE: - { - console.log('Processing Edge Event'); - const edgeService = new ActivityWatchEdgeService(); - await edgeService.save(job.data.data); - } - break; - case 'remove-window-events': - console.log('Removing Window Events'); - await windowService.clear(); - break; - case 'remove-wakatime-events': - console.log('Removing Wakatime Events'); - await metaData.removeActivity(knex, { - idsWakatime: job.data.data - }); - break; - case 'update-duration-timer': - console.log('Updating Timer Duration'); - await this._timerService.update( - new Timer({ - id: job.data.data.id, - duration: job.data.data.duration, - ...(this._offlineMode.enabled && { synced: false }) - }) - ); - break; - case 'update-timer-time-slot': - console.log('Updating Timer Time Slot'); - await this._timerService.update( - new Timer({ - id: job.data.data.id, - timeslotId: job.data.data.timeSlotId, - timesheetId: job.data.data.timeSheetId - }) - ); - break; - default: - console.log('Unknown Job Type'); - break; - } - - resolve(true); - } catch (error) { - console.error('failed insert window activity', error); - resolve(false); - } - }); + await this.ProcessQueueMessage(job, knex); }, + // concurrency is 1 1 ); @@ -763,7 +789,7 @@ export default class TimerHandler { }); } - // create "adder" type job + // create job and add to queue await this.queue.createJob({ type: queName, data: data diff --git a/packages/desktop-libs/src/lib/offline/desktop-offline-mode-handler.ts b/packages/desktop-libs/src/lib/offline/desktop-offline-mode-handler.ts index 40592037d44..f331701fc74 100644 --- a/packages/desktop-libs/src/lib/offline/desktop-offline-mode-handler.ts +++ b/packages/desktop-libs/src/lib/offline/desktop-offline-mode-handler.ts @@ -7,7 +7,13 @@ import { ApiServerConnectivity } from '../states'; export class DesktopOfflineModeHandler extends EventEmitter implements IOfflineMode { private static _instance: IOfflineMode; private _isEnabled: boolean; + + /** + * Timer used for pinging the server in offline mode. + * TODO: destroy on exit from app? + */ private _pingTimer: any; + private _PING_INTERVAL: number; private _startedAt: Date; private _stoppedAt: Date; @@ -16,47 +22,77 @@ export class DesktopOfflineModeHandler extends EventEmitter implements IOfflineM */ private constructor() { super(); + // Initial status this._isEnabled = false; + // Initial server ping timer this._pingTimer = null; - // Default ping timer interval - this._PING_INTERVAL = 30 * 1000; + + // Default ping timer interval set to 40 seconds + this._PING_INTERVAL = 40 * 1000; this.on('connection-restored', () => { + console.log('[OfflineModeHandler]: Connection restored'); // Date on cancels offline mode this._stoppedAt = new Date(Date.now()); }); } public async connectivity(): Promise { + let isEmitted = false; + try { - // Check connectivity + console.log('[OfflineModeHandler]: Checking connection...'); + const networkContext = new NetworkStateManager(new ApiServerConnectivity()); const connectivityEstablished = await networkContext.established(); - console.log('[NetworkContext]: ', connectivityEstablished); + console.log('[OfflineModeHandler]. Connectivity Established: ', connectivityEstablished); // If connection is not restored if (!connectivityEstablished) { + console.log('[OfflineModeHandler]:', 'Connection still not restored'); + if (!this._isEnabled) { + console.log('[OfflineModeHandler]:', 'Entering offline mode...'); + // Date on trigger offline mode this._startedAt = new Date(Date.now()); + // Turn offline mode on this._isEnabled = true; + // Notify that offline mode is enabled this.emit('offline'); + + isEmitted = true; + + console.log('[OfflineModeHandler]:', 'Offline mode enabled'); } - console.log('[OfflineModeHandler]:', 'Connection still not restored'); } else { // Check again before restore - (await networkContext.established()) - ? this.restore() // Call offline mode restore routine - : console.log('Waiting...'); // or waiting + if (await networkContext.established()) { + // Call offline mode restore routine + this.restore(); + } else { + console.log('Waiting...'); // or waiting + } } } catch (error) { console.log('CONNECTIVITY_ERROR', error); + + if (!isEmitted) { + // Date on trigger offline mode + this._startedAt = new Date(Date.now()); + + // Turn offline mode on + this._isEnabled = true; + + // Notify that offline mode is enabled + this.emit('offline'); + } } } /** @@ -68,19 +104,26 @@ export class DesktopOfflineModeHandler extends EventEmitter implements IOfflineM await this.connectivity(); }, this._PING_INTERVAL); } + /** * Cancels offline mode * @returns {void} */ public restore(): void { - // Connection restored if (!this._isEnabled) { + console.log('[OfflineModeHandler]: Connection already working, no need to restore.'); return; } + + console.log('[OfflineModeHandler]: Restoring connection...'); + // Reset state this._isEnabled = false; + // Notify about reconnection event this.emit('connection-restored'); + + console.log('[OfflineModeHandler]: Connection restored'); } /** * Returns status of offline mode (true if enabled) diff --git a/packages/desktop-libs/src/lib/offline/services/timer.service.ts b/packages/desktop-libs/src/lib/offline/services/timer.service.ts index 98cdd4bc426..db5f93bd4c5 100644 --- a/packages/desktop-libs/src/lib/offline/services/timer.service.ts +++ b/packages/desktop-libs/src/lib/offline/services/timer.service.ts @@ -15,7 +15,12 @@ export class TimerService implements ITimerService { public async findLastOne(): Promise { try { const user = await this._userService.retrieve(); - return await this._timerDAO.lastTimer(user.employeeId); + + if (user && user.employeeId) { + return await this._timerDAO.lastTimer(user.employeeId); + } else { + return null; + } } catch (error) { console.error(error); return null; @@ -25,9 +30,14 @@ export class TimerService implements ITimerService { public async findLastCapture(): Promise { try { const user = await this._userService.retrieve(); - return await this._timerDAO.lastCapture(user.employeeId); + + if (user && user.employeeId) { + return await this._timerDAO.lastCapture(user.employeeId); + } else { + return null; + } } catch (error) { - console.error(error); + console.error('Cannot find last Capture', error); return null; } } @@ -37,6 +47,7 @@ export class TimerService implements ITimerService { if (!timer.id) { return console.error('WARN[TIMER_SERVICE]: No timer data, cannot update'); } + await this._timerDAO.update(timer.id, timer.toObject()); } catch (error) { throw new AppError('[TIMER_SERVICE]', error); diff --git a/packages/desktop-libs/src/lib/states/concretes/activity-watch-connectivity.ts b/packages/desktop-libs/src/lib/states/concretes/activity-watch-connectivity.ts index 3bb71280cff..f0472f23e49 100644 --- a/packages/desktop-libs/src/lib/states/concretes/activity-watch-connectivity.ts +++ b/packages/desktop-libs/src/lib/states/concretes/activity-watch-connectivity.ts @@ -6,10 +6,22 @@ export class ActivityWatchConnectivity implements INetworkState { public async established(): Promise { try { const config = LocalStore.getStore('configs'); - const res = await fetch(config.awHost + '/api'); + + const url = config.awHost + '/api'; + + console.log('[ActivityWatchConnectivity]: Checking server connectivity to url: ', url); + + const res = await fetch(url, { + headers: { + 'Cache-Control': 'no-cache' + }, + timeout: 5000 + }); + if (res.ok) { return true; } + return false; } catch (error) { console.error('[ActivityWatchConnectivity]: ', error); diff --git a/packages/desktop-libs/src/lib/states/concretes/api-server-connectivity.ts b/packages/desktop-libs/src/lib/states/concretes/api-server-connectivity.ts index 5465f942a91..cebe51694fd 100644 --- a/packages/desktop-libs/src/lib/states/concretes/api-server-connectivity.ts +++ b/packages/desktop-libs/src/lib/states/concretes/api-server-connectivity.ts @@ -1,17 +1,38 @@ import { INetworkState } from '../../interfaces'; import { LocalStore } from '../../desktop-store'; -import fetch from 'node-fetch'; -import dns from 'node:dns'; +import { fetch, Agent } from 'undici'; +import dns from 'node:dns'; dns.setDefaultResultOrder('ipv4first'); export class ApiServerConnectivity implements INetworkState { public async established(): Promise { try { - const res = await fetch(LocalStore.getServerUrl() + '/api'); - if (res.ok) { + const url = LocalStore.getServerUrl() + '/api'; + + console.log('[ApiServerConnectivity]: Checking server connectivity to url: ', url); + + // if more than 30 sec to get response, then we think no connectivity for now to API servers + const res = await fetch(url, { + headers: { + 'Cache-Control': 'no-cache' + }, + dispatcher: new Agent({ + connect: { + timeout: 30 * 1000 + } + }) + }); + + if (res?.ok && res?.status === 200) { + const data = await res.json(); + console.log('[ApiServerConnectivity]: Server connectivity check response: ', data); + return true; + } else { + console.log('[ApiServerConnectivity]: Server connectivity failed: ', res?.statusText); } + return false; } catch (error) { console.error('[ApiServerConnectivity]: ', error); diff --git a/packages/desktop-libs/src/lib/states/concretes/internet-connectivity.ts b/packages/desktop-libs/src/lib/states/concretes/internet-connectivity.ts index 2e29785ca82..e1bedaec59a 100644 --- a/packages/desktop-libs/src/lib/states/concretes/internet-connectivity.ts +++ b/packages/desktop-libs/src/lib/states/concretes/internet-connectivity.ts @@ -1,15 +1,27 @@ import { INetworkState } from '../../interfaces'; -import fetch, { Headers } from 'node-fetch'; +import { Agent, fetch, Headers } from 'undici'; export class InternetConnectivity implements INetworkState { public async established(): Promise { try { + console.log('[InternetConnectivity]: Checking internet connectivity...'); + const headers = new Headers(); headers.append('Cache-Control', 'no-cache'); - const res = await fetch('https://www.google.com'); + + const res = await fetch('https://www.google.com', { + headers, + dispatcher: new Agent({ + connect: { + timeout: 30 * 1000 + } + }) + }); + if (res.ok) { return true; } + return false; } catch (error) { console.error('[InternetConnectivity]: ', error); diff --git a/packages/desktop-ui-lib/src/lib/offline-sync/concretes/sequence-queue.ts b/packages/desktop-ui-lib/src/lib/offline-sync/concretes/sequence-queue.ts index 8b78472dfca..aa6a516e52a 100644 --- a/packages/desktop-ui-lib/src/lib/offline-sync/concretes/sequence-queue.ts +++ b/packages/desktop-ui-lib/src/lib/offline-sync/concretes/sequence-queue.ts @@ -1,12 +1,5 @@ import { ITimeSlot } from '@gauzy/contracts'; -import { - asapScheduler, - concatMap, - defer, - of, - repeat, - timer as synchronizer, -} from 'rxjs'; +import { asapScheduler, concatMap, defer, of, repeat, timer as synchronizer } from 'rxjs'; import { TimeSlotQueueService } from '../time-slot-queue.service'; import { ElectronService } from '../../electron/services'; import { ErrorHandlerService, Store } from '../../services'; @@ -14,11 +7,7 @@ import { TimeTrackerStatusService } from '../../time-tracker/time-tracker-status import { TimeTrackerService } from '../../time-tracker/time-tracker.service'; import { TimeSlotQueue } from './time-slot-queue'; import { OfflineQueue } from '../interfaces/offline-queue'; -import { - BlockedSequenceState, - CompletedSequenceState, - InProgressSequenceState, -} from './states'; +import { BlockedSequenceState, CompletedSequenceState, InProgressSequenceState } from './states'; import { BACKGROUND_SYNC_OFFLINE_INTERVAL } from '../../constants/app.constants'; export interface ISequence { @@ -26,6 +15,9 @@ export interface ISequence { intervals: ITimeSlot[]; } +/** + * SequenceQueue + */ export class SequenceQueue extends OfflineQueue { constructor( protected _electronService: ElectronService, @@ -38,6 +30,7 @@ export class SequenceQueue extends OfflineQueue { super(); this.state = new BlockedSequenceState(this); } + public async synchronize({ timer, intervals }: ISequence): Promise { try { console.log('🛠 - Preprocessing time slot'); @@ -47,17 +40,21 @@ export class SequenceQueue extends OfflineQueue { taskId: timer.taskId, projectId: timer.projectId, organizationId: this._store.organizationId, - tenantId: this._store.tenantId, + tenantId: this._store.tenantId }; + let latest = null; + if (timer.isStartedOffline) { console.log('⏱ - Silent start'); latest = await this._timeTrackerService.toggleApiStart({ ...timer, - ...params, + ...params }); } + console.log('🛠 - Create queue'); + // Create the queue const timeSlotQueue = new TimeSlotQueue( this._timeTrackerService, @@ -65,6 +62,7 @@ export class SequenceQueue extends OfflineQueue { this._electronService, this._store ); + // append data to queue; if (intervals.length > 0) { for (const interval of intervals) timeSlotQueue.enqueue(interval); @@ -75,64 +73,75 @@ export class SequenceQueue extends OfflineQueue { console.log('✅ - End processing time slot queue'); // End processing } + if (timer.isStoppedOffline) { console.log('⏱ - Silent stop'); latest = await this._timeTrackerService.toggleApiStop({ ...timer, - ...params, + ...params }); } + const status = await this._timeTrackerStatusService.status(); + asapScheduler.schedule(async () => { try { - await this._electronService.ipcRenderer.invoke( - 'UPDATE_SYNCED_TIMER', - { - lastTimer: latest - ? latest - : { + await this._electronService.ipcRenderer.invoke('UPDATE_SYNCED_TIMER', { + lastTimer: latest + ? latest + : { ...timer, - id: status.lastLog.id, - }, - ...timer, - } - ); + id: status.lastLog.id + }, + ...timer + }); console.log('⏱ - local database updated'); } catch (error) { + console.error('🚨 - Error updating local database', error); this._errorHandlerService.handleError(error); } }); } catch (error) { + console.error('🚨 - Error processing time slot queue', error); this._timeSlotQueueService.viewQueueStateUpdater = { size: this.queue.size, - inProgress: false, + inProgress: false }; } } + public process(): Promise { + console.log('🚀 - Sequence processing started'); + return new Promise((resolve) => { // Create an observable to process the queue const process$ = defer(() => of(true)).pipe( concatMap(() => this.dequeue()), repeat({ - delay: () => synchronizer(BACKGROUND_SYNC_OFFLINE_INTERVAL), + delay: () => synchronizer(BACKGROUND_SYNC_OFFLINE_INTERVAL) }) ); + console.log('🚀 - Sequence processing observable created'); + // Subscribe to the observable const subscription = process$.subscribe({ - next: () => console.log('✅ - Sequence done'), + next: () => console.log('✅ - Sequence done') }); + console.log('🚀 - Sequence processing observable subscribed'); + // Unsubscribe and resolve the promise when the queue is completed this.state$.subscribe((state) => { + console.log('🚀 - Sequence state updated'); + this._timeSlotQueueService.viewQueueStateUpdater = { size: this.queue.size, - inProgress: state instanceof InProgressSequenceState, + inProgress: state instanceof InProgressSequenceState }; - if ( - state instanceof CompletedSequenceState - ) { + + if (state instanceof CompletedSequenceState) { + console.log('🚀 - Sequence processing completed'); subscription.unsubscribe(); resolve(); } diff --git a/packages/desktop-ui-lib/src/lib/services/server-connection.service.ts b/packages/desktop-ui-lib/src/lib/services/server-connection.service.ts index e9f162879f1..221c38e6463 100644 --- a/packages/desktop-ui-lib/src/lib/services/server-connection.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/server-connection.service.ts @@ -12,44 +12,55 @@ export class ServerConnectionService { return new Promise((resolve, reject) => { console.log(`Checking server connection in ServerConnectionService in desktop-ui-lib on URL: ${url}`); - const requestObservable = this.httpClient.get(url); + if (endPoint !== 'http://localhost:3000') { + try { + const requestObservable = this.httpClient.get(url); - if (!requestObservable) { - console.error('Failed to create an Observable from the HTTP request.'); - reject('Failed to create an Observable from the HTTP request.'); - return; - } - - requestObservable.subscribe({ - next: (resp: any) => { - if (resp) { - this.store.serverConnection = resp.status; - console.log( - `Server connection status in ServerConnectionService in desktop-ui-lib for URL: ${url} is: `, - resp.status - ); - resolve(true); - } else { - console.log('Server connection resp empty'); - resolve(false); + if (!requestObservable) { + console.error('Failed to create an Observable from the HTTP request.'); + reject('Failed to create an Observable from the HTTP request.'); + return; } - }, - error: (err) => { - console.error(`Error checking server connection in ServerConnectionService for URL: ${url}`, err); - if (this.store.userId) { - console.log( - `We were unable to connect to the server, but we have a user id ${this.store.userId}.` - ); - resolve(true); - } else { - this.store.serverConnection = err.status; - reject(err); - } + requestObservable.subscribe({ + next: (resp: any) => { + if (resp) { + this.store.serverConnection = resp.status; + console.log( + `Server connection status in ServerConnectionService in desktop-ui-lib for URL: ${url} is: `, + resp.status + ); + resolve(true); + } else { + console.log('Server connection resp empty'); + resolve(false); + } + }, + error: (err) => { + console.error( + `Error checking server connection in ServerConnectionService for URL: ${url}`, + err + ); + + if (this.store.userId) { + console.log( + `We were unable to connect to the server, but we have a user id ${this.store.userId}.` + ); + resolve(true); + } else { + this.store.serverConnection = err.status; + reject(err); + } + } + }); + } catch (error) { + console.error(`Error checking server connection in ServerConnectionService for URL: ${url}`, error); + reject(error); } - }); - }).catch((e) => { - console.error(`Error checking server connection in ServerConnectionService for URL: ${url}`, e); + } else { + console.log(`Skip checking server connection for URL: ${url}`); + resolve(true); + } }); } } diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts index 7a3dbf6cd80..bb4a33e5b97 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts @@ -547,10 +547,12 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { private async _toggle(timer: any, onClick: boolean) { try { const { lastTimer, isStarted } = timer; + const isRemote = this._timeTrackerStatus.remoteTimer && this.xor(!isStarted, this._timeTrackerStatus.remoteTimer.running) && this._startMode === TimerStartMode.REMOTE; + const params = { token: this.token, note: this.note, @@ -561,8 +563,11 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { organizationContactId: this.organizationContactId, apiHost: this.apiHost }; + let timelog = null; + console.log('[TIMER_STATE]', lastTimer); + if (isStarted) { if (!this._isOffline && !this._remoteSleepLock) { try { @@ -577,6 +582,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this._loggerService.log.error(error); } } + this.loading = false; } else { if (!this._isOffline) { @@ -595,7 +601,9 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this._loggerService.log.error(error); } } + this.start$.next(false); + this.loading = false; } @@ -611,7 +619,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } }); } catch (error) { - this.loading = false; let messageError = error.message; if (messageError.includes('Http failure response')) { messageError = `Can't connect to api server`; @@ -622,6 +629,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { status: 'danger' }); this._loggerService.log.info(`Timer Toggle Catch: ${moment().format()}`, error); + this.loading = false; } } @@ -1226,9 +1234,11 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { }); }); }); + if (!this._isReady) { this.electronService.ipcRenderer.send('time_tracker_ready'); } + this.electronService.ipcRenderer.on('remove_idle_time', (event, arg) => { this._ngZone.run(async () => { try { @@ -1362,7 +1372,10 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this.electronService.ipcRenderer.on('emergency_stop', (event, arg) => { this._ngZone.run(async () => { + console.log('Emergency stop'); + if (this.start) { + console.log('Emergency stop timer'); await this.stopTimer(!this.isRemoteTimer, true); } }); @@ -1611,17 +1624,20 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { const config = { quitApp: this.quitApp, isEmergency }; if (this._startMode === TimerStartMode.MANUAL) { - console.log('Taking screen capture'); - const activities = await this.electronService.ipcRenderer.invoke('TAKE_SCREEN_CAPTURE', config); - console.log('Stopping timer'); const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config); - console.log('Sending activities'); - await this.sendActivities(activities); - console.log('Toggling timer'); await this._toggle(timer, onClick); + + // when we stop timer, let's try to make final screenshot in background after tiny delay of 1 sec + setTimeout(async () => { + console.log('Taking screen capture'); + const activities = await this.electronService.ipcRenderer.invoke('TAKE_SCREEN_CAPTURE', config); + + console.log('Sending activities'); + await this.sendActivities(activities); + }, 1000); } else { console.log('Stopping timer'); const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config); @@ -1638,12 +1654,15 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { if (this._isSpecialLogout) { this._isSpecialLogout = false; + // wait 3 sec and logout await this.logout(); } if (this.quitApp) { - console.log('Quitting app from stopTimer'); - this.electronService.remote.app.quit(); + console.log('Quitting app from stopTimer after 3 seconds delay'); + setTimeout(() => { + this.electronService.remote.app.quit(); + }, 3000); } } catch (error) { console.log('[ERROR_STOP_TIMER]', error); @@ -2496,9 +2515,14 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } public async logout() { - await firstValueFrom(this._authStrategy.logout()); - this.electronService.ipcRenderer.send(this._isRestartAndUpdate ? 'restart_and_update' : 'navigate_to_login'); - localStorage.clear(); + // we wait 3 sec and then logout + setTimeout(async () => { + await firstValueFrom(this._authStrategy.logout()); + this.electronService.ipcRenderer.send( + this._isRestartAndUpdate ? 'restart_and_update' : 'navigate_to_login' + ); + localStorage.clear(); + }, 3000); } public async restart(): Promise { @@ -2506,24 +2530,32 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { if (this.isRemoteTimer) { return; } - try { - // lock restart process - this._isLockSyncProcess = true; - // resolve promise and add debounce time to avoid riding - await lastValueFrom( - from(this.toggleStart(false)).pipe( - debounceTime(200), - concatMap(() => this.toggleStart(true)), - untilDestroyed(this) - ) - ); - } catch (error) { - // force stop timer - await this.stopTimer(false, true); - } finally { - // unlock restart process - this._isLockSyncProcess = false; - } + + // wait 3 sec and then restart + setTimeout(async () => { + try { + // lock restart process + this._isLockSyncProcess = true; + // resolve promise and add debounce time to avoid riding + await lastValueFrom( + from(this.toggleStart(false)).pipe( + debounceTime(200), + concatMap(() => this.toggleStart(true)), + untilDestroyed(this) + ) + ); + } catch (error) { + // force stop timer + try { + await this.stopTimer(false, true); + } catch (e) { + console.error('Error in force stop timer', e); + } + } finally { + // unlock restart process + this._isLockSyncProcess = false; + } + }, 3000); } public async updateOrganizationTeamEmployee(): Promise { diff --git a/yarn.lock b/yarn.lock index bfb79414507..8a90dcb6636 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36820,6 +36820,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.10.2.tgz#13fe0c099d0055ea16417bd3fef6ccd661210743" + integrity sha512-HcVuBy7ACaDejIMdwCzAvO22OsiE6ir6ziTIr9kAE0vB+PheVe29ZvRN8p7FXCO2uZHTjEoUs5bPiFpuc/hwwQ== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"