2024-06-06 13:16:59 +00:00
|
|
|
export interface ApiError {
|
2021-11-03 16:40:31 +00:00
|
|
|
status: number;
|
|
|
|
message: string;
|
2024-06-06 13:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type QueryParams = Record<string, string | number | boolean>;
|
2021-11-03 16:40:31 +00:00
|
|
|
|
2024-06-06 13:16:59 +00:00
|
|
|
export function encodeQueryString(_params: unknown = {}): string {
|
|
|
|
const __params = _params as QueryParams;
|
|
|
|
const params: QueryParams = {};
|
2021-11-03 16:40:31 +00:00
|
|
|
|
2024-06-06 13:16:59 +00:00
|
|
|
Object.keys(__params).forEach((key) => {
|
|
|
|
const val = __params[key];
|
2021-11-03 16:40:31 +00:00
|
|
|
if (val !== undefined) {
|
|
|
|
params[key] = val;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-06-06 13:16:59 +00:00
|
|
|
return Object.keys(params)
|
|
|
|
.sort()
|
|
|
|
.map((key) => {
|
|
|
|
const val = params[key];
|
|
|
|
return `${encodeURIComponent(key)}=${encodeURIComponent(val)}`;
|
|
|
|
})
|
|
|
|
.join('&');
|
2021-11-03 16:40:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default class ApiClient {
|
|
|
|
server: string;
|
|
|
|
|
|
|
|
token: string | null;
|
|
|
|
|
|
|
|
csrf: string | null;
|
|
|
|
|
|
|
|
onerror: ((err: ApiError) => void) | undefined;
|
|
|
|
|
|
|
|
constructor(server: string, token: string | null, csrf: string | null) {
|
|
|
|
this.server = server;
|
|
|
|
this.token = token;
|
|
|
|
this.csrf = csrf;
|
|
|
|
}
|
|
|
|
|
2024-06-10 10:13:13 +00:00
|
|
|
private async _request(method: string, path: string, data?: unknown): Promise<unknown> {
|
2023-10-08 15:49:13 +00:00
|
|
|
const res = await fetch(`${this.server}${path}`, {
|
|
|
|
method,
|
|
|
|
headers: {
|
2024-06-06 13:16:59 +00:00
|
|
|
...(method !== 'GET' && this.csrf !== null ? { 'X-CSRF-TOKEN': this.csrf } : {}),
|
|
|
|
...(this.token !== null ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
|
|
...(data !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
2023-10-08 15:49:13 +00:00
|
|
|
},
|
2024-06-06 13:16:59 +00:00
|
|
|
body: data !== undefined ? JSON.stringify(data) : undefined,
|
2023-10-08 15:49:13 +00:00
|
|
|
});
|
2021-11-03 16:40:31 +00:00
|
|
|
|
2023-10-08 15:49:13 +00:00
|
|
|
if (!res.ok) {
|
2024-08-07 16:47:13 +00:00
|
|
|
let message = res.statusText;
|
|
|
|
const resText = await res.text();
|
|
|
|
if (resText) {
|
|
|
|
message = `${res.statusText}: ${resText}`;
|
|
|
|
}
|
2023-10-08 15:49:13 +00:00
|
|
|
const error: ApiError = {
|
|
|
|
status: res.status,
|
2024-08-07 16:47:13 +00:00
|
|
|
message,
|
2023-10-08 15:49:13 +00:00
|
|
|
};
|
|
|
|
if (this.onerror) {
|
|
|
|
this.onerror(error);
|
|
|
|
}
|
2024-08-07 16:47:13 +00:00
|
|
|
throw new Error(message);
|
2021-11-03 16:40:31 +00:00
|
|
|
}
|
|
|
|
|
2023-10-08 15:49:13 +00:00
|
|
|
const contentType = res.headers.get('Content-Type');
|
2024-06-06 13:16:59 +00:00
|
|
|
if (contentType !== null && contentType.startsWith('application/json')) {
|
2023-10-08 15:49:13 +00:00
|
|
|
return res.json();
|
2021-11-03 16:40:31 +00:00
|
|
|
}
|
|
|
|
|
2023-10-08 15:49:13 +00:00
|
|
|
return res.text();
|
2021-11-03 16:40:31 +00:00
|
|
|
}
|
|
|
|
|
2024-07-07 08:15:18 +00:00
|
|
|
async _get(path: string) {
|
2024-06-10 10:13:13 +00:00
|
|
|
return this._request('GET', path);
|
2021-11-03 16:40:31 +00:00
|
|
|
}
|
|
|
|
|
2024-07-07 08:15:18 +00:00
|
|
|
async _post(path: string, data?: unknown) {
|
2021-11-03 16:40:31 +00:00
|
|
|
return this._request('POST', path, data);
|
|
|
|
}
|
|
|
|
|
2024-07-07 08:15:18 +00:00
|
|
|
async _patch(path: string, data?: unknown) {
|
2021-11-03 16:40:31 +00:00
|
|
|
return this._request('PATCH', path, data);
|
|
|
|
}
|
|
|
|
|
2024-07-07 08:15:18 +00:00
|
|
|
async _delete(path: string) {
|
2024-06-10 10:13:13 +00:00
|
|
|
return this._request('DELETE', path);
|
2021-11-03 16:40:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_subscribe<T>(path: string, callback: (data: T) => void, opts = { reconnect: true }) {
|
|
|
|
const query = encodeQueryString({
|
2024-06-06 13:16:59 +00:00
|
|
|
access_token: this.token ?? undefined,
|
2021-11-03 16:40:31 +00:00
|
|
|
});
|
|
|
|
let _path = this.server ? this.server + path : path;
|
2024-06-06 13:16:59 +00:00
|
|
|
_path = this.token !== null ? `${_path}?${query}` : _path;
|
2021-11-03 16:40:31 +00:00
|
|
|
|
|
|
|
const events = new EventSource(_path);
|
|
|
|
events.onmessage = (event) => {
|
2024-06-06 13:16:59 +00:00
|
|
|
const data = JSON.parse(event.data as string) as T;
|
2024-08-17 06:09:48 +00:00
|
|
|
// eslint-disable-next-line promise/prefer-await-to-callbacks
|
2021-11-03 16:40:31 +00:00
|
|
|
callback(data);
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!opts.reconnect) {
|
|
|
|
events.onerror = (err) => {
|
2024-05-13 20:58:21 +00:00
|
|
|
// TODO: check if such events really have a data property
|
2021-11-03 16:40:31 +00:00
|
|
|
if ((err as Event & { data: string }).data === 'eof') {
|
|
|
|
events.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
|
|
|
setErrorHandler(onerror: (err: ApiError) => void) {
|
|
|
|
this.onerror = onerror;
|
|
|
|
}
|
|
|
|
}
|