Refactor server requests

This commit is contained in:
Hakan Shehu
2024-10-23 09:43:44 +02:00
parent 987c3ab65e
commit f6e2c75974
12 changed files with 210 additions and 141 deletions

View File

@@ -0,0 +1,119 @@
import { BackoffCalculator } from '@/lib/backoff-calculator';
import { ServerAttributes } from '@/types/servers';
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
interface HttpClientRequestConfig extends AxiosRequestConfig {
serverDomain: string;
serverAttributes: string | ServerAttributes;
token?: string | null;
}
class HttpClient {
private readonly backoffs: Map<string, BackoffCalculator> = new Map();
constructor() {}
private async request<T>(
method: 'get' | 'post' | 'put' | 'delete',
path: string,
config: HttpClientRequestConfig,
): Promise<AxiosResponse<T>> {
if (this.backoffs.has(config.serverDomain)) {
const backoff = this.backoffs.get(config.serverDomain);
if (!backoff.canRetry()) {
throw new Error(`Backoff in progress for key: ${config.serverDomain}`);
}
}
try {
const axiosInstance = this.buildAxiosInstance(config);
const response = await axiosInstance.request<T>({
method,
url: path,
...config,
});
// Reset backoff if successful
if (this.backoffs.has(config.serverDomain)) {
this.backoffs.get(config.serverDomain).reset();
}
return response;
} catch (error: any) {
// If error is related to server availability, increase backoff
if (this.isServerError(error)) {
if (!this.backoffs.has(config.serverDomain)) {
this.backoffs.set(config.serverDomain, new BackoffCalculator());
}
const backoff = this.backoffs.get(config.serverDomain);
backoff.increaseError();
}
throw error;
}
}
private isServerError(error: any): boolean {
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
return true;
}
const status = error.response?.status;
return (status >= 500 && status < 600) || status === 429;
}
private buildAxiosInstance(config: HttpClientRequestConfig): AxiosInstance {
const parsedAttributes: ServerAttributes =
typeof config.serverAttributes === 'string'
? JSON.parse(config.serverAttributes)
: config.serverAttributes;
const protocol = parsedAttributes?.insecure ? 'http' : 'https';
const baseURL = `${protocol}://${config.serverDomain}`;
const instance = axios.create({ baseURL });
if (config.token) {
instance.defaults.headers.common['Authorization'] =
`Bearer ${config.token}`;
}
return instance;
}
public async get<T>(
path: string,
config: HttpClientRequestConfig,
): Promise<AxiosResponse<T, any>> {
return this.request<T>('get', path, config);
}
public async post<T>(
path: string,
data: any,
config: HttpClientRequestConfig,
): Promise<AxiosResponse<T, any>> {
return this.request<T>('post', path, {
...config,
data,
});
}
public async put<T>(
path: string,
data: any,
config: HttpClientRequestConfig,
): Promise<AxiosResponse<T, any>> {
return this.request<T>('put', path, {
...config,
data,
});
}
public async delete<T>(
path: string,
config: HttpClientRequestConfig,
): Promise<AxiosResponse<T, any>> {
return this.request<T>('delete', path, config);
}
}
export const httpClient = new HttpClient();

View File

@@ -1,27 +1,8 @@
import { SelectServer } from '@/main/data/app/schema';
import { ServerAttributes } from '@/types/servers';
import axios, { AxiosInstance } from 'axios';
export const buildSynapseUrl = (server: SelectServer, deviceId: string) => {
const attributes = JSON.parse(server.attributes) as ServerAttributes;
const protocol = attributes?.insecure ? 'ws' : 'wss';
return `${protocol}://${server.domain}/v1/synapse?device_id=${deviceId}`;
};
export const buildAxiosInstance = (
domain: string,
attributes: string | ServerAttributes,
token?: string,
): AxiosInstance => {
const parsedAttributes: ServerAttributes =
typeof attributes === 'string' ? JSON.parse(attributes) : attributes;
const protocol = parsedAttributes?.insecure ? 'http' : 'https';
const baseURL = `${protocol}://${domain}`;
const instance = axios.create({ baseURL });
if (token) {
instance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
return instance;
};

View File

@@ -2,7 +2,7 @@ import { app, net } from 'electron';
import path from 'path';
import fs from 'fs';
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
class AvatarManager {
private readonly appPath: string;
@@ -35,15 +35,13 @@ class AvatarManager {
return new Response(null, { status: 404 });
}
const axios = buildAxiosInstance(
credentials.domain,
credentials.attributes,
credentials.token,
);
const response = await axios.get(`/v1/avatars/${avatarId}`, {
const response = await httpClient.get<any>(`/v1/avatars/${avatarId}`, {
serverDomain: credentials.domain,
serverAttributes: credentials.attributes,
token: credentials.token,
responseType: 'stream',
});
if (response.status !== 200 || !response.data) {
return new Response(null, { status: 404 });
}

View File

@@ -5,7 +5,7 @@ import FormData from 'form-data';
import { ServerFileUploadResponse } from '@/types/files';
import { WorkspaceCredentials } from '@/types/workspaces';
import { databaseManager } from './data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
import { LocalNodeAttributes } from '@/types/nodes';
import axios from 'axios';
@@ -103,15 +103,15 @@ class FileManager {
continue;
}
const accountAxios = buildAxiosInstance(
credentials.serverDomain,
credentials.serverAttributes,
credentials.token,
);
try {
const { data } = await accountAxios.post<ServerFileUploadResponse>(
const { data } = await httpClient.post<ServerFileUploadResponse>(
`/v1/files/${credentials.workspaceId}/${upload.node_id}`,
{},
{
serverDomain: credentials.serverDomain,
serverAttributes: credentials.serverAttributes,
token: credentials.token,
},
);
const presignedUrl = data.url;

View File

@@ -1,9 +1,9 @@
import fs from 'fs';
import FormData from 'form-data';
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { AvatarUploadMutationInput } from '@/operations/mutations/avatar-upload';
import { httpClient } from '@/lib/http-client';
interface AvatarUploadResponse {
id: string;
@@ -31,22 +31,19 @@ export class AvatarUploadMutationHandler
};
}
const axios = buildAxiosInstance(
credentials.domain,
credentials.attributes,
credentials.token,
);
const filePath = input.filePath;
const fileStream = fs.createReadStream(filePath);
const formData = new FormData();
formData.append('avatar', fileStream);
const { data } = await axios.post<AvatarUploadResponse>(
const { data } = await httpClient.post<AvatarUploadResponse>(
'/v1/avatars',
formData,
{
serverDomain: credentials.domain,
serverAttributes: credentials.attributes,
token: credentials.token,
headers: formData.getHeaders(),
},
);

View File

@@ -1,12 +1,12 @@
import { LoginOutput } from '@/types/accounts';
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { EmailLoginMutationInput } from '@/operations/mutations/email-login';
import {
MutationChange,
MutationHandler,
MutationResult,
} from '@/operations/mutations';
import { httpClient } from '@/lib/http-client';
export class EmailLoginMutationHandler
implements MutationHandler<EmailLoginMutationInput>
@@ -28,11 +28,17 @@ export class EmailLoginMutationHandler
};
}
const axios = buildAxiosInstance(server.domain, server.attributes);
const { data } = await axios.post<LoginOutput>('/v1/accounts/login/email', {
email: input.email,
password: input.password,
});
const { data } = await httpClient.post<LoginOutput>(
'/v1/accounts/login/email',
{
email: input.email,
password: input.password,
},
{
serverDomain: server.domain,
serverAttributes: server.attributes,
},
);
const changedTables: MutationChange[] = [];
await databaseManager.appDatabase.transaction().execute(async (trx) => {

View File

@@ -1,6 +1,6 @@
import { LoginOutput } from '@/types/accounts';
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
import { EmailRegisterMutationInput } from '@/operations/mutations/email-register';
import {
MutationChange,
@@ -28,14 +28,17 @@ export class EmailRegisterMutationHandler
};
}
const axios = buildAxiosInstance(server.domain, server.attributes);
const { data } = await axios.post<LoginOutput>(
const { data } = await httpClient.post<LoginOutput>(
'/v1/accounts/register/email',
{
name: input.name,
email: input.email,
password: input.password,
},
{
serverDomain: server.domain,
serverAttributes: server.attributes,
},
);
const changedTables: MutationChange[] = [];

View File

@@ -1,5 +1,5 @@
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
import { WorkspaceCreateMutationInput } from '@/operations/mutations/workspace-create';
import {
MutationChange,
@@ -34,16 +34,19 @@ export class WorkspaceCreateMutationHandler
throw new Error('Account not found');
}
const axios = buildAxiosInstance(
server.domain,
server.attributes,
account.token,
const { data } = await httpClient.post<WorkspaceOutput>(
`/v1/workspaces`,
{
name: input.name,
description: input.description,
avatar: input.avatar,
},
{
serverDomain: server.domain,
serverAttributes: server.attributes,
token: account.token,
},
);
const { data } = await axios.post<WorkspaceOutput>(`/v1/workspaces`, {
name: input.name,
description: input.description,
avatar: input.avatar,
});
await databaseManager.appDatabase
.insertInto('workspaces')

View File

@@ -1,5 +1,5 @@
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
import { WorkspaceUpdateMutationInput } from '@/operations/mutations/workspace-update';
import {
MutationChange,
@@ -34,18 +34,20 @@ export class WorkspaceUpdateMutationHandler
throw new Error('Account not found');
}
const axios = buildAxiosInstance(
server.domain,
server.attributes,
account.token,
const { data } = await httpClient.put<Workspace>(
`/v1/workspaces/${input.id}`,
{
name: input.name,
description: input.description,
avatar: input.avatar,
},
{
serverDomain: server.domain,
serverAttributes: server.attributes,
token: account.token,
},
);
const { data } = await axios.put<Workspace>(`/v1/workspaces/${input.id}`, {
name: input.name,
description: input.description,
avatar: input.avatar,
});
await databaseManager.appDatabase
.updateTable('workspaces')
.set({

View File

@@ -1,5 +1,5 @@
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
import { WorkspaceUserRoleUpdateMutationInput } from '@/operations/mutations/workspace-user-role-update';
import {
MutationChange,
@@ -56,16 +56,16 @@ export class WorkspaceUserRoleUpdateMutationHandler
};
}
const axios = buildAxiosInstance(
server.domain,
server.attributes,
account.token,
);
const { data } = await axios.put<WorkspaceUserRoleUpdateOutput>(
const { data } = await httpClient.put<WorkspaceUserRoleUpdateOutput>(
`/v1/workspaces/${workspace.workspace_id}/users/${input.userToUpdateId}`,
{
role: input.role,
},
{
serverDomain: server.domain,
serverAttributes: server.attributes,
token: account.token,
},
);
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(

View File

@@ -1,5 +1,5 @@
import { databaseManager } from '@/main/data/database-manager';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
import { WorkspaceUsersInviteMutationInput } from '@/operations/mutations/workspace-users-invite';
import {
MutationChange,
@@ -57,16 +57,16 @@ export class WorkspaceUsersInviteMutationHandler
};
}
const axios = buildAxiosInstance(
server.domain,
server.attributes,
account.token,
);
const { data } = await axios.post<WorkspaceUsersInviteOutput>(
const { data } = await httpClient.post<WorkspaceUsersInviteOutput>(
`/v1/workspaces/${workspace.workspace_id}/users`,
{
emails: input.emails,
},
{
serverDomain: server.domain,
serverAttributes: server.attributes,
token: account.token,
},
);
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(

View File

@@ -1,15 +1,13 @@
import { BackoffCalculator } from '@/lib/backoff-calculator';
import { buildAxiosInstance } from '@/lib/servers';
import { httpClient } from '@/lib/http-client';
import { databaseManager } from '@/main/data/database-manager';
import { ServerSyncResponse } from '@/types/sync';
import { WorkspaceCredentials } from '@/types/workspaces';
import { fileManager } from './file-manager';
import { fileManager } from '@/main/file-manager';
const EVENT_LOOP_INTERVAL = 1000;
class Synchronizer {
private initiated: boolean = false;
private readonly backoffs: Map<string, BackoffCalculator> = new Map();
constructor() {
this.executeEventLoop = this.executeEventLoop.bind(this);
@@ -53,22 +51,12 @@ class Synchronizer {
}
for (const account of accounts) {
const backoffKey = account.id;
if (this.backoffs.has(backoffKey)) {
const backoff = this.backoffs.get(backoffKey);
if (!backoff.canRetry()) {
return;
}
}
try {
const axios = buildAxiosInstance(
account.domain,
account.attributes,
account.token,
);
const { status } = await axios.delete(`/v1/accounts/logout`);
const { status } = await httpClient.delete(`/v1/accounts/logout`, {
serverDomain: account.domain,
serverAttributes: account.attributes,
token: account.token,
});
if (status !== 200) {
return;
@@ -79,17 +67,8 @@ class Synchronizer {
.deleteFrom('accounts')
.where('id', '=', account.id)
.execute();
if (this.backoffs.has(backoffKey)) {
this.backoffs.delete(backoffKey);
}
} catch (error) {
if (!this.backoffs.has(backoffKey)) {
this.backoffs.set(backoffKey, new BackoffCalculator());
}
const backoff = this.backoffs.get(account.id);
backoff.increaseError();
console.log('error', error);
}
}
}
@@ -110,14 +89,6 @@ class Synchronizer {
.execute();
for (const workspace of workspaces) {
const backoffKey = workspace.user_id;
if (this.backoffs.has(backoffKey)) {
const backoff = this.backoffs.get(backoffKey);
if (!backoff.canRetry()) {
return;
}
}
const credentials: WorkspaceCredentials = {
workspaceId: workspace.workspace_id,
accountId: workspace.account_id,
@@ -130,18 +101,8 @@ class Synchronizer {
try {
await this.checkForChanges(credentials);
// await fileManager.checkForUploads(credentials);
if (this.backoffs.has(backoffKey)) {
this.backoffs.delete(backoffKey);
}
} catch (error) {
console.log('error', error);
if (!this.backoffs.has(backoffKey)) {
this.backoffs.set(backoffKey, new BackoffCalculator());
}
const backoff = this.backoffs.get(backoffKey);
backoff.increaseError();
}
}
}
@@ -162,17 +123,16 @@ class Synchronizer {
return;
}
const axios = buildAxiosInstance(
credentials.serverDomain,
credentials.serverAttributes,
credentials.token,
);
const { data } = await axios.post<ServerSyncResponse>(
const { data } = await httpClient.post<ServerSyncResponse>(
`/v1/sync/${credentials.workspaceId}`,
{
changes: changes,
},
{
serverDomain: credentials.serverDomain,
serverAttributes: credentials.serverAttributes,
token: credentials.token,
},
);
const syncedChangeIds: number[] = [];