Skip to content

Commit

Permalink
More relaxed parsing of api url, fix race condition when putting hear…
Browse files Browse the repository at this point in the history
…tbeats back into queue
  • Loading branch information
alanhamlett committed Oct 8, 2024
1 parent 7aff50f commit 703a5f2
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 27 deletions.
3 changes: 0 additions & 3 deletions src/components/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ export default function Options(): JSX.Element {
const handleSubmit = async () => {
if (state.loading) return;
setState((oldState) => ({ ...oldState, loading: true }));
if (state.apiUrl.endsWith('/')) {
state.apiUrl = state.apiUrl.slice(0, -1);
}
await saveSettings({
allowList: state.allowList.filter((item) => !!item.trim()),
apiKey: state.apiKey,
Expand Down
56 changes: 42 additions & 14 deletions src/core/WakaTimeCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { changeExtensionStatus } from '../utils/changeExtensionStatus';
import getDomainFromUrl, { getDomain } from '../utils/getDomainFromUrl';
import { getOperatingSystem, IS_EDGE, IS_FIREFOX } from '../utils/operatingSystem';
import { getSettings, Settings } from '../utils/settings';
import { getApiUrl } from '../utils/user';

import config, { ExtensionStatus } from '../config/config';
import { EntityType, Heartbeat, HeartbeatsBulkResponse } from '../types/heartbeats';
Expand Down Expand Up @@ -166,7 +167,6 @@ class WakaTimeCore {
async sendHeartbeats(): Promise<void> {
const settings = await browser.storage.sync.get({
apiKey: config.apiKey,
apiUrl: config.apiUrl,
heartbeatApiEndPoint: config.heartbeatApiEndPoint,
hostname: '',
});
Expand All @@ -175,16 +175,8 @@ class WakaTimeCore {
return;
}

const heartbeats = (await (await this.db()).getAll(config.queueName, undefined, 25)) as
| Heartbeat[]
| undefined;
if (!heartbeats || heartbeats.length === 0) return;

await Promise.all(
heartbeats.map(async (heartbeat) => {
return (await this.db()).delete(config.queueName, heartbeat.id);
}),
);
const heartbeats = await this.getHeartbeatsFromQueue();
if (heartbeats.length === 0) return;

const userAgent = await this.getUserAgent();

Expand All @@ -204,7 +196,15 @@ class WakaTimeCore {
};
}

const url = `${settings.apiUrl}${settings.heartbeatApiEndPoint}?api_key=${settings.apiKey}`;
try {
await getApiUrl();
} catch (err: unknown) {
console.error(err);
return;
}

const apiUrl = await getApiUrl();
const url = `${apiUrl}${settings.heartbeatApiEndPoint}?api_key=${settings.apiKey}`;
const response = await fetch(url, request);
if (response.status === 401) {
await this.putHeartbeatsBackInQueue(heartbeats);
Expand Down Expand Up @@ -246,10 +246,38 @@ class WakaTimeCore {
}
}

async putHeartbeatsBackInQueue(heartbeats: Heartbeat[]): Promise<void> {
async getHeartbeatsFromQueue(): Promise<Heartbeat[]> {
const tx = (await this.db()).transaction(config.queueName, 'readwrite');

const heartbeats = (await tx.store.getAll(undefined, 25)) as Heartbeat[] | undefined;
if (!heartbeats || heartbeats.length === 0) return [];

await Promise.all(
heartbeats.map(async (heartbeat) => (await this.db()).add(config.queueName, heartbeat)),
heartbeats.map(async (heartbeat) => {
return tx.store.delete(heartbeat.id);
}),
);

await tx.done;

return heartbeats;
}

async putHeartbeatsBackInQueue(heartbeats: Heartbeat[]): Promise<void> {
await Promise.all(heartbeats.map(async (heartbeat) => this.putHeartbeatBackInQueue(heartbeat)));
}

async putHeartbeatBackInQueue(heartbeat: Heartbeat, tries = 0): Promise<void> {
try {
await (await this.db()).add(config.queueName, heartbeat);
} catch (err: unknown) {
if (tries < 10) {
return await this.putHeartbeatBackInQueue(heartbeat, tries + 1);
}
console.error(err);
console.error(`Unable to add heartbeat back into queue: ${heartbeat.id}`);
console.error(JSON.stringify(heartbeat));
}
}

async getUserAgent(): Promise<string> {
Expand Down
5 changes: 3 additions & 2 deletions src/reducers/currentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import axios, { AxiosResponse } from 'axios';
import browser from 'webextension-polyfill';
import config from '../config/config';
import { CurrentUser, User, UserPayload } from '../types/user';
import { getApiUrl } from '../utils/user';

interface setUserAction {
payload: User | undefined;
Expand All @@ -16,11 +17,11 @@ export const fetchCurrentUser = createAsyncThunk<User, string>(
`[${name}]`,
async (api_key = '') => {
const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
currentUserApiEndPoint: config.currentUserApiEndPoint,
});
const apiUrl = await getApiUrl();
const userPayload: AxiosResponse<UserPayload> = await axios.get(
`${items.apiUrl}${items.currentUserApiEndPoint}`,
`${apiUrl}${items.currentUserApiEndPoint}`,
{
params: { api_key },
},
Expand Down
39 changes: 31 additions & 8 deletions src/utils/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AnyAction, Dispatch } from '@reduxjs/toolkit';
import axios, { AxiosResponse } from 'axios';
import browser from 'webextension-polyfill';

import moment from 'moment';
import config from '../config/config';
Expand All @@ -9,18 +10,42 @@ import { GrandTotal, Summaries } from '../types/summaries';
import { ApiKeyPayload, AxiosUserResponse, User } from '../types/user';
import changeExtensionState from './changeExtensionStatus';

export const getApiUrl = async () => {
const settings = await browser.storage.sync.get({
apiUrl: config.apiUrl,
});
let apiUrl = (settings.apiUrl as string) || config.apiUrl;
const suffixes = [
'/',
'.bulk',
'.bulk',
'/users/current/heartbeats',
'/heartbeats',
'/heartbeat',
];
for (const suffix of suffixes) {
if (apiUrl.endsWith(suffix)) {
apiUrl = apiUrl.slice(0, -suffix.length);
}
}
if (!apiUrl.endsWith('/api/v1')) {
apiUrl = `${apiUrl}/api/v1`;
}
return apiUrl;
};

/**
* Checks if the user is logged in.
*
* @returns {*}
*/
const checkAuth = async (api_key = ''): Promise<User> => {
const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
currentUserApiEndPoint: config.currentUserApiEndPoint,
});
const apiUrl = await getApiUrl();
const userPayload: AxiosResponse<AxiosUserResponse> = await axios.get(
`${items.apiUrl}${items.currentUserApiEndPoint}`,
`${apiUrl}${items.currentUserApiEndPoint}`,
{ params: { api_key } },
);
return userPayload.data.data;
Expand Down Expand Up @@ -54,12 +79,11 @@ export const logUserIn = async (apiKey: string): Promise<void> => {
const fetchApiKey = async (): Promise<string> => {
try {
const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
currentUserApiEndPoint: config.currentUserApiEndPoint,
});

const apiUrl = await getApiUrl();
const apiKeyResponse: AxiosResponse<ApiKeyPayload> = await axios.post(
`${items.apiUrl}${items.currentUserApiEndPoint}/get_api_key`,
`${apiUrl}${items.currentUserApiEndPoint}/get_api_key`,
);
return apiKeyResponse.data.data.api_key;
} catch (err: unknown) {
Expand All @@ -69,13 +93,12 @@ const fetchApiKey = async (): Promise<string> => {

const getTotalTimeLoggedToday = async (api_key = ''): Promise<GrandTotal> => {
const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
summariesApiEndPoint: config.summariesApiEndPoint,
});

const apiUrl = await getApiUrl();
const today = moment().format('YYYY-MM-DD');
const summariesAxiosPayload: AxiosResponse<Summaries> = await axios.get(
`${items.apiUrl}${items.summariesApiEndPoint}`,
`${apiUrl}${items.summariesApiEndPoint}`,
{
params: {
api_key,
Expand Down

0 comments on commit 703a5f2

Please sign in to comment.