mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Refactor server requests
This commit is contained in:
119
desktop/src/lib/http-client.ts
Normal file
119
desktop/src/lib/http-client.ts
Normal 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();
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
Reference in New Issue
Block a user