Skip to content

Commit

Permalink
feat/skip-tasks-for-free-toggle-accounts (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikerourke authored Jan 17, 2024
2 parents 2715558 + e4eb697 commit 586cdee
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 4 deletions.
57 changes: 56 additions & 1 deletion src/api/apiRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function* fetchFromApi<T>(url: string, config: RequestInit): SagaIterator<T> {
const response = yield call(fetch, fullUrl, config);

if (!response.ok) {
throw new Error(response);
throw new ApiError(toolName, response);
}

const type = response.headers.get("content-type") ?? null;
Expand Down Expand Up @@ -266,3 +266,58 @@ function getApiUrl(
? "https://api.track.toggl.com/reports/api/v2"
: "https://api.track.toggl.com/api/v9";
}

export class ApiError extends Error {
readonly #toolName: ToolName;
readonly #response: Response;

constructor(toolName: ToolName, response: Response) {
super(`API error from ${toolName} to ${response.url}: ${response.status}`);

this.name = "ApiError";

this.#toolName = toolName;
this.#response = response;
}

public get headers(): Headers {
return this.#response.headers;
}

public get statusCode(): number {
return this.#response.status;
}

public get statusText(): string {
return this.#response.statusText;
}

public get url(): string {
return this.#response.url;
}

public get toolName(): ToolName {
return this.#toolName;
}

public toJson(): Record<string, any> {
const headers: Record<string, string> = {};

// We don't want this to blow up the app if it fails:
try {
// @ts-ignore
for (const [name, value] of this.#response.headers) {
headers[name] = value;
}
} catch {
// Do nothing.
}

return {
statusCode: this.statusCode,
statusText: this.statusText,
url: this.url,
headers,
};
}
}
27 changes: 24 additions & 3 deletions src/redux/tasks/sagas/togglTasksSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { SagaIterator } from "redux-saga";
import { call, delay, select } from "redux-saga/effects";

import {
ApiError,
fetchArray,
fetchEmpty,
fetchObject,
Expand All @@ -16,6 +17,7 @@ import {
targetProjectsByIdSelector,
} from "~/redux/projects/projectsSelectors";
import { userIdToLinkedIdSelector } from "~/redux/users/usersSelectors";
import { allFreeWorkspaceIdsSelector } from "~/redux/workspaces/workspacesSelectors";
import { EntityGroup, ToolName, type Project, type Task } from "~/types";
import { validStringify } from "~/utilities/textTransforms";

Expand Down Expand Up @@ -73,8 +75,23 @@ export function* fetchTogglTasksSaga(): SagaIterator<Task[]> {

const apiDelay = getApiDelayForTool(ToolName.Toggl);

const allFreeWorkspaceIds = yield select(allFreeWorkspaceIdsSelector);

const freeWorkspaceIds = new Set<string>();

for (const entry of Object.entries(togglProjectsTable)) {
const [workspaceId, projects] = entry as [string, Project[]];
// Toggl tasks can't be fetched for paid workspaces. Rather than make a
// bunch of requests that return 402, we skip them. We add it to a set
// rather than just continue out of the loop, so we can catch fetch errors
// as well and prevent further fetches:
if (allFreeWorkspaceIds.includes(workspaceId)) {
freeWorkspaceIds.add(workspaceId);
}

if (freeWorkspaceIds.has(workspaceId)) {
continue;
}

for (const project of projects) {
try {
Expand All @@ -86,9 +103,13 @@ export function* fetchTogglTasksSaga(): SagaIterator<Task[]> {

allTasks.push(...tasks);
} catch (err: AnyValid) {
if (err.status === 402) {
// User can't create or fetch tasks.
break;
// User can't create or fetch tasks because the workspace isn't paid:
if (err instanceof ApiError && err.statusCode === 402) {
// We save the workspace ID, so we can _continue_ through the loop
// rather than break, and we don't want to hit this error again for
// the workspace in the next iteration of the loop:
freeWorkspaceIds.add(workspaceId);
continue;
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/redux/workspaces/workspacesSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ export const targetWorkspacesSelector = createSelector(
},
);

export const allFreeWorkspaceIdsSelector = createSelector(
sourceWorkspacesSelector,
targetWorkspacesSelector,
(sourceWorkspaces, targetWorkspaces): string[] => {
const allFreeWorkspaceIds: string[] = [];

for (const workspace of [...sourceWorkspaces, ...targetWorkspaces]) {
if (!workspace.isPaid) {
allFreeWorkspaceIds.push(workspace.id);
}
}

return allFreeWorkspaceIds;
},
);

export const missingTargetWorkspacesSelector = createSelector(
includedSourceWorkspacesSelector,
(includedSourceWorkspaces): Workspace[] =>
Expand Down

0 comments on commit 586cdee

Please sign in to comment.