diff --git a/package-lock.json b/package-lock.json index cb76daf..9a37ac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "classnames": "^2.3.2", "create-react-class": "^15.7.0", "font-awesome": "4.7.0", + "idb": "^7.1.1", "jquery": "^3.6.3", "moment": "^2.29.4", "react": "^18.2.0", @@ -11899,6 +11900,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -32397,6 +32403,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/package.json b/package.json index 284ffef..b8afa43 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "classnames": "^2.3.2", "create-react-class": "^15.7.0", "font-awesome": "4.7.0", + "idb": "^7.1.1", "jquery": "^3.6.3", "moment": "^2.29.4", "react": "^18.2.0", diff --git a/src/background.ts b/src/background.ts index 6d43701..7a2d2fb 100644 --- a/src/background.ts +++ b/src/background.ts @@ -10,11 +10,7 @@ browser.alarms.onAlarm.addListener(async (alarm) => { // Checks if the user is online and if there are cached heartbeats requests, // if so then procedd to send these payload to wakatime api if (navigator.onLine) { - const { cachedHeartbeats } = await browser.storage.sync.get({ - cachedHeartbeats: [], - }); - await browser.storage.sync.set({ cachedHeartbeats: [] }); - await WakaTimeCore.sendCachedHeartbeatsRequest(cachedHeartbeats as Record[]); + await WakaTimeCore.sendCachedHeartbeatsRequest(); } } }); @@ -54,3 +50,11 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => { } } }); + +/** + * Creates IndexedDB + * https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API + */ +self.addEventListener('activate', async () => { + await WakaTimeCore.createDB(); +}); diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 2bed7d6..9109acd 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -1,6 +1,9 @@ +/* eslint-disable no-fallthrough */ +/* eslint-disable default-case */ import axios, { AxiosResponse } from 'axios'; import moment from 'moment'; import browser, { Tabs } from 'webextension-polyfill'; +import { IDBPDatabase, openDB } from 'idb'; import { AxiosUserResponse, User } from '../types/user'; import config from '../config/config'; import { SummariesPayload, GrandTotal } from '../types/summaries'; @@ -11,10 +14,36 @@ import getDomainFromUrl from '../utils/getDomainFromUrl'; class WakaTimeCore { tabsWithDevtoolsOpen: Tabs.Tab[]; + db: IDBPDatabase | undefined; constructor() { this.tabsWithDevtoolsOpen = []; } + /** + * Creates a IndexDB using idb https://github.com/jakearchibald/idb + * a library that adds promises to IndexedDB and makes it easy to use + */ + async createDB() { + const dbConnection = await openDB('wakatime', 1, { + upgrade(db, oldVersion) { + // Create a store of objects + const store = db.createObjectStore('cacheHeartbeats', { + // The `time` property of the object will be the key, and be incremented automatically + keyPath: 'time', + }); + // Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded. + switch (oldVersion) { + case 0: + // Placeholder to execute when database is created (oldVersion is 0) + case 1: + // Create an index called `type` based on the `type` property of objects in the store + store.createIndex('time', 'time'); + } + }, + }); + this.db = dbConnection; + } + setTabsWithDevtoolsOpen(tabs: Tabs.Tab[]): void { this.tabsWithDevtoolsOpen = tabs; } @@ -278,21 +307,18 @@ class WakaTimeCore { * @param method * @returns {*} */ - async sendPostRequestToApi(payload: Record, apiKey = '') { + async sendPostRequestToApi(payload: Record, apiKey = ''): Promise { try { const response = await fetch(`${config.heartbeatApiUrl}?api_key=${apiKey}`, { body: JSON.stringify(payload), method: 'POST', }); - const data = await response.json(); - return data; + await response.json(); } catch (err: unknown) { - // Stores the payload of the request to be send later - const { cachedHeartbeats } = await browser.storage.sync.get({ - cachedHeartbeats: [], - }); - cachedHeartbeats.push(payload); - await browser.storage.sync.set({ cachedHeartbeats }); + if (this.db) { + await this.db.add('cacheHeartbeats', payload); + } + await changeExtensionState('notSignedIn'); } } @@ -301,27 +327,27 @@ class WakaTimeCore { * Sends cached heartbeats request to wakatime api * @param requests */ - async sendCachedHeartbeatsRequest(requests: Record[]): Promise { + async sendCachedHeartbeatsRequest(): Promise { const apiKey = await this.getApiKey(); if (!apiKey) { return changeExtensionState('notLogging'); } - const chunkSize = 50; // Create batches of max 50 request - for (let i = 0; i < requests.length; i += chunkSize) { - const chunk = requests.slice(i, i + chunkSize); - const requestsPromises: Promise[] = []; - chunk.forEach((request) => - requestsPromises.push( - fetch(`${config.heartbeatApiUrl}?api_key=${apiKey}`, { - body: JSON.stringify(request), - method: 'POST', - }), - ), - ); - try { - await Promise.all(requestsPromises); - } catch (error: unknown) { - console.log('Error sending heartbeats'); + + if (this.db) { + const requests = await this.db.getAll('cacheHeartbeats'); + await this.db.clear('cacheHeartbeats'); + const chunkSize = 50; // Create batches of max 50 request + for (let i = 0; i < requests.length; i += chunkSize) { + const chunk = requests.slice(i, i + chunkSize); + const requestsPromises: Promise[] = []; + chunk.forEach((request: Record) => + requestsPromises.push(this.sendPostRequestToApi(request, apiKey)), + ); + try { + await Promise.all(requestsPromises); + } catch (error: unknown) { + console.log('Error sending heartbeats'); + } } } }