-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.ts
More file actions
141 lines (115 loc) · 3.98 KB
/
client.ts
File metadata and controls
141 lines (115 loc) · 3.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import type { Config } from "../config/config.schema.js";
import type { AuthProvider } from "../auth/auth-provider.interface.js";
export interface Thing {
id: string;
name: string;
status?: string;
[key: string]: unknown;
}
export interface ListThingsParams {
filter?: string;
top: number;
}
export class NotFoundError extends Error {
public constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
}
export class ApiRequestError extends Error {
public readonly status: number;
public readonly details: string;
public constructor(status: number, details: string) {
super(`Request failed with status ${status}`);
this.name = "ApiRequestError";
this.status = status;
this.details = details;
}
}
export class ExampleApiClient {
private readonly config: Config;
private readonly authProvider: AuthProvider;
public constructor(config: Config, authProvider: AuthProvider) {
this.config = config;
this.authProvider = authProvider;
}
public async getThing(id: string, fields?: string[]): Promise<Thing> {
const searchParams = new URLSearchParams();
if (fields && fields.length > 0) {
searchParams.set("fields", fields.join(","));
}
const query = searchParams.toString();
const path = query ? `/things/${id}?${query}` : `/things/${id}`;
return this.request<Thing>(path, { method: "GET" });
}
public async listThings(params: ListThingsParams): Promise<Thing[]> {
const searchParams = new URLSearchParams();
if (params.filter) {
searchParams.set("filter", params.filter);
}
searchParams.set("top", String(params.top));
return this.request<Thing[]>(`/things?${searchParams.toString()}`, {
method: "GET"
});
}
public async deleteThing(id: string): Promise<void> {
await this.request<void>(`/things/${id}`, { method: "DELETE" });
}
private async request<T>(path: string, init: RequestInit, attempt = 0): Promise<T> {
const token = await this.authProvider.getAccessToken();
const controller = new AbortController();
const timeoutHandle = setTimeout(() => controller.abort(), this.config.timeoutMs);
try {
const response = await fetch(`${this.config.apiBaseUrl}${path}`, {
...init,
headers: {
"content-type": "application/json",
authorization: `Bearer ${token}`,
...(init.headers ?? {})
},
signal: controller.signal
});
if (response.status === 404) {
throw new NotFoundError(`Resource not found at path: ${path}`);
}
if (!response.ok) {
const details = await response.text();
if (this.shouldRetry(response.status) && attempt < this.config.maxRetries) {
await this.sleep(this.getBackoffDelay(attempt));
return this.request<T>(path, init, attempt + 1);
}
throw new ApiRequestError(response.status, details);
}
if (response.status === 204) {
return undefined as T;
}
const contentType = response.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
return (await response.json()) as T;
}
return (await response.text()) as T;
} catch (error) {
const isAbort = error instanceof Error && error.name === "AbortError";
const isRetryableNetworkError = error instanceof TypeError;
if ((isAbort || isRetryableNetworkError) && attempt < this.config.maxRetries) {
await this.sleep(this.getBackoffDelay(attempt));
return this.request<T>(path, init, attempt + 1);
}
throw error;
} finally {
clearTimeout(timeoutHandle);
}
}
private shouldRetry(statusCode: number): boolean {
return [429, 500, 502, 503, 504].includes(statusCode);
}
private getBackoffDelay(attempt: number): number {
const base = 250;
return base * Math.pow(2, attempt);
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
}