-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathclient.ts
134 lines (114 loc) · 2.82 KB
/
client.ts
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
import { IAuthProvider } from "./auth/types.ts";
import { QueryParams, searchParamsFromObj } from "./utils.ts";
export const API_PREFIX = "https://api.spotify.com/v1";
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
interface SpotifyRawError {
error: { message: string; status: number };
}
export interface ISpoitfyError extends Error {
readonly status: number;
readonly message: string;
}
export class SpotifyError extends Error implements ISpoitfyError {
public readonly status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
interface FetchOpts {
method?: HTTPMethod;
body?: Record<string, unknown>;
query?: QueryParams;
}
export interface ISpotifyClient {
fetch(
baseURL: string,
returnType: "void",
opts?: FetchOpts,
): Promise<void>;
fetch<
R extends unknown,
>(
baseURL: string,
returnType: "json",
opts?: FetchOpts,
): Promise<R>;
}
export class SpotifyClient {
#authProvider: IAuthProvider | string;
constructor(
{ authProvider }: {
authProvider: IAuthProvider | string;
},
) {
this.#authProvider = authProvider;
}
async fetch(
baseURL: string,
returnType: "void",
opts?: FetchOpts,
): Promise<void>;
async fetch<R = unknown>(
baseURL: string,
returnType: "json",
opts?: FetchOpts,
): Promise<R>;
async fetch<
R extends unknown,
>(
baseURL: string,
returnType: "void" | "json",
{ body, query, method }: FetchOpts = {},
) {
const url = new URL(API_PREFIX + baseURL);
if (query) {
url.search = searchParamsFromObj(query).toString();
}
const serializedBody = body ? JSON.stringify(body) : undefined;
let authRetry = false;
const call = async (token: string): Promise<Response> => {
const res = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": `Bearer ${token}`,
},
body: serializedBody,
});
if (!res.ok) {
let error: SpotifyRawError;
try {
error = await res.json() as SpotifyRawError;
} catch (_) {
throw new SpotifyError(
"Unable to read response body(not a json value)",
res.status,
);
}
const { error: { message } } = error;
if (
res.status === 401 && typeof this.#authProvider !== "string" &&
!authRetry
) {
const access_token = await this.#authProvider.getAccessToken(
true,
);
authRetry = true;
return await call(access_token);
}
throw new SpotifyError(message, res.status);
}
return res;
};
const access_token = typeof this.#authProvider === "string"
? this.#authProvider
: await this.#authProvider.getAccessToken();
const res = await call(access_token);
if (returnType === "json") {
return await res.json() as R;
}
if (res.body) await res.body.cancel();
}
}