Skip to content

Commit

Permalink
feat: connect to lowest ping relay
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitrovs committed Jul 30, 2021
1 parent d598ce4 commit 3a09419
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 81 deletions.
69 changes: 46 additions & 23 deletions lib/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,66 @@ import {
MessageStream,
UhstRelayClient,
} from './contracts/UhstRelayClient';
import { HostConfiguration, ClientConfiguration, ApiResponse } from './models';
import { NetworkClient } from './NetworkClient';
import { HostConfiguration, ClientConfiguration } from './models';
import { RelayUrlsProvider } from './RelayUrlsProvider';
import { RelayClient } from './RelayClient';
import { RelayClientProvider } from './RelayClientProvider';

const API_URL = 'https://api.uhst.io/v1/get-relay';
import { InvalidHostId, RelayUnreachable } from './UhstErrors';

export class ApiClient implements UhstRelayClient {
networkClient: NetworkClient;
relayUrlsProvider: RelayUrlsProvider;
relayClient: RelayClient;
constructor(
private relayClientProvider: RelayClientProvider,
networkClient?: NetworkClient
relayUrlsProvider?: RelayUrlsProvider
) {
this.networkClient = networkClient ?? new NetworkClient();
}

async getRelayUrl(hostId?: string): Promise<string> {
const apiResponse: ApiResponse = await this.networkClient.post(
API_URL,
hostId ? [`hostId=${hostId}`] : undefined
);
return apiResponse.url;
this.relayUrlsProvider = relayUrlsProvider ?? new RelayUrlsProvider();
}

async initHost(hostId?: string): Promise<HostConfiguration> {
this.relayClient = this.relayClientProvider.createRelayClient(
await this.getRelayUrl(hostId)
);
return this.relayClient.initHost(hostId);
const relayUrl = await this.relayUrlsProvider.getBestRelayUrl(hostId);
let hostConfiguration: HostConfiguration;
let exception = new RelayUnreachable();
try {
this.relayClient = this.relayClientProvider.createRelayClient(relayUrl);
hostConfiguration = await this.relayClient.initHost(hostId);
} catch (e) {
// handle network error here
exception = e;
}
return new Promise<HostConfiguration>((resolve, reject) => {
if (hostConfiguration) {
resolve(hostConfiguration);
} else {
reject(exception);
}
});
}

async initClient(hostId: string): Promise<ClientConfiguration> {
this.relayClient = this.relayClientProvider.createRelayClient(
await this.getRelayUrl(hostId)
);
return this.relayClient.initClient(hostId);
const relayUrls = await this.relayUrlsProvider.getRelayUrls(hostId);
let clientConfiguration: ClientConfiguration;
let exception = new RelayUnreachable();
for (const url of relayUrls) {
try {
this.relayClient = this.relayClientProvider.createRelayClient(url);
clientConfiguration = await this.relayClient.initClient(hostId);
break;
} catch (e) {
// handle network error here
exception = e;
if (e instanceof InvalidHostId) {
break;
}
}
}
return new Promise<ClientConfiguration>((resolve, reject) => {
if (clientConfiguration) {
resolve(clientConfiguration);
} else {
reject(exception);
}
});
}

sendMessage(token: string, message: any, sendUrl?: string): Promise<any> {
Expand Down
50 changes: 47 additions & 3 deletions lib/NetworkClient.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { NetworkError, NetworkUnreachable } from './UhstErrors';

const REQUEST_OPTIONS = {
method: 'POST'
method: 'POST',
};

export class NetworkClient {
async post(url: string, queryParams?: string[]): Promise<any> {
async post(
url: string,
queryParams?: string[],
timeout?: number
): Promise<any> {
try {
if (queryParams && queryParams.length > 0) {
url = `${url}?${queryParams.join('&')}`;
}
const response = await fetch(url, REQUEST_OPTIONS);
const response = timeout
? await this.fetchWithTimeout(url, { ...REQUEST_OPTIONS, timeout })
: await fetch(url, REQUEST_OPTIONS);
if (response.status == 200) {
return response.json();
} else {
Expand All @@ -21,4 +27,42 @@ export class NetworkClient {
throw new NetworkUnreachable(error);
}
}

async get(
url: string,
queryParams?: string[],
timeout?: number
): Promise<any> {
try {
if (queryParams && queryParams.length > 0) {
url = `${url}?${queryParams.join('&')}`;
}
const response = timeout
? await this.fetchWithTimeout(url, { timeout })
: await fetch(url);
if (response.status == 200) {
return response.json();
} else {
throw new NetworkError(response.status, `${response.statusText}`);
}
} catch (error) {
console.log(error);
throw new NetworkUnreachable(error);
}
}

async fetchWithTimeout(resource, options): Promise<any> {
const { timeout = 8000 } = options;

const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);

const response = await fetch(resource, {
...options,
signal: controller.signal,
});
clearTimeout(id);

return response;
}
}
63 changes: 63 additions & 0 deletions lib/RelayUrlsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Relay } from './models';
import { NetworkClient } from './NetworkClient';
import { RelayUnreachable } from './UhstErrors';

const RELAYS_LIST_URL =
'https://raw.githubusercontent.com/uhst/relays/main/list.json';

export class RelayUrlsProvider {
networkClient: NetworkClient;
constructor(networkClient?: NetworkClient) {
this.networkClient = networkClient ?? new NetworkClient();
}
async getRelayUrls(hostId?: string): Promise<string[]> {
const relays: Relay[] = await this.networkClient.get(RELAYS_LIST_URL);
if (hostId) {
// if hostId get all URLs for the hostId
const prefix = hostId.split('-')[0];
for (const relay of relays) {
if (relay.prefix === prefix) {
return relay.urls;
}
}
// there are no relays serving this prefix
return [];
}
// if no hostId, get all URLs
const urls: string[] = [];
relays.forEach((x) => x.urls.forEach((url) => urls.push(url)));
return urls;
}

async getBestRelayUrl(hostId?: string): Promise<string> {
const relayUrls = await this.getRelayUrls(hostId);
return new Promise<string>((resolve, reject) => {
let resolved = false;
let failed = 0;
if (relayUrls.length === 0) {
reject(new RelayUnreachable());
} else {
relayUrls
.sort(() => Math.random() - Math.random())
.slice(0, Math.min(relayUrls.length, 10))
.forEach(async (url) => {
try {
const response = await this.networkClient.post(url, [
'action=ping',
`timestamp=${Date.now()}`,
]);
if (!resolved) {
resolve(url);
resolved = true;
}
} catch (e) {
failed++;
if (!resolved && failed == relayUrls.length) {
reject(e);
}
}
});
}
});
}
}
3 changes: 0 additions & 3 deletions lib/models/ApiResponse.ts

This file was deleted.

4 changes: 4 additions & 0 deletions lib/models/Relay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Relay {
urls: string[],
prefix: string
}
2 changes: 1 addition & 1 deletion lib/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './ApiResponse';
export * from './Relay';
export * from './ClientConfiguration';
export * from './HostConfiguration';
export * from './HostMessage';
Expand Down
Loading

0 comments on commit 3a09419

Please sign in to comment.