@debros/network-ts-sdk 0.1.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -1
- package/dist/index.d.ts +392 -29
- package/dist/index.js +795 -96
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/auth/client.ts +144 -10
- package/src/cache/client.ts +203 -0
- package/src/core/http.ts +248 -13
- package/src/core/ws.ts +87 -80
- package/src/db/repository.ts +6 -2
- package/src/index.ts +44 -10
- package/src/network/client.ts +58 -4
- package/src/pubsub/client.ts +174 -28
- package/src/storage/client.ts +270 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { HttpClient } from "../core/http";
|
|
2
|
+
import { SDKError } from "../errors";
|
|
3
|
+
|
|
4
|
+
export interface CacheGetRequest {
|
|
5
|
+
dmap: string;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CacheGetResponse {
|
|
10
|
+
key: string;
|
|
11
|
+
value: any;
|
|
12
|
+
dmap: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CachePutRequest {
|
|
16
|
+
dmap: string;
|
|
17
|
+
key: string;
|
|
18
|
+
value: any;
|
|
19
|
+
ttl?: string; // Duration string like "1h", "30m"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CachePutResponse {
|
|
23
|
+
status: string;
|
|
24
|
+
key: string;
|
|
25
|
+
dmap: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CacheDeleteRequest {
|
|
29
|
+
dmap: string;
|
|
30
|
+
key: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CacheDeleteResponse {
|
|
34
|
+
status: string;
|
|
35
|
+
key: string;
|
|
36
|
+
dmap: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CacheMultiGetRequest {
|
|
40
|
+
dmap: string;
|
|
41
|
+
keys: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CacheMultiGetResponse {
|
|
45
|
+
results: Array<{
|
|
46
|
+
key: string;
|
|
47
|
+
value: any;
|
|
48
|
+
}>;
|
|
49
|
+
dmap: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CacheScanRequest {
|
|
53
|
+
dmap: string;
|
|
54
|
+
match?: string; // Optional regex pattern
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CacheScanResponse {
|
|
58
|
+
keys: string[];
|
|
59
|
+
count: number;
|
|
60
|
+
dmap: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CacheHealthResponse {
|
|
64
|
+
status: string;
|
|
65
|
+
service: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class CacheClient {
|
|
69
|
+
private httpClient: HttpClient;
|
|
70
|
+
|
|
71
|
+
constructor(httpClient: HttpClient) {
|
|
72
|
+
this.httpClient = httpClient;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check cache service health
|
|
77
|
+
*/
|
|
78
|
+
async health(): Promise<CacheHealthResponse> {
|
|
79
|
+
return this.httpClient.get("/v1/cache/health");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a value from cache
|
|
84
|
+
* Returns null if the key is not found (cache miss/expired), which is normal behavior
|
|
85
|
+
*/
|
|
86
|
+
async get(dmap: string, key: string): Promise<CacheGetResponse | null> {
|
|
87
|
+
try {
|
|
88
|
+
return await this.httpClient.post<CacheGetResponse>("/v1/cache/get", {
|
|
89
|
+
dmap,
|
|
90
|
+
key,
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Cache misses (404 or "key not found" messages) are normal behavior - return null instead of throwing
|
|
94
|
+
if (
|
|
95
|
+
error instanceof SDKError &&
|
|
96
|
+
(error.httpStatus === 404 ||
|
|
97
|
+
(error.httpStatus === 500 &&
|
|
98
|
+
error.message?.toLowerCase().includes("key not found")))
|
|
99
|
+
) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// Re-throw other errors (network issues, server errors, etc.)
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Put a value into cache
|
|
109
|
+
*/
|
|
110
|
+
async put(
|
|
111
|
+
dmap: string,
|
|
112
|
+
key: string,
|
|
113
|
+
value: any,
|
|
114
|
+
ttl?: string
|
|
115
|
+
): Promise<CachePutResponse> {
|
|
116
|
+
return this.httpClient.post<CachePutResponse>("/v1/cache/put", {
|
|
117
|
+
dmap,
|
|
118
|
+
key,
|
|
119
|
+
value,
|
|
120
|
+
ttl,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Delete a value from cache
|
|
126
|
+
*/
|
|
127
|
+
async delete(dmap: string, key: string): Promise<CacheDeleteResponse> {
|
|
128
|
+
return this.httpClient.post<CacheDeleteResponse>("/v1/cache/delete", {
|
|
129
|
+
dmap,
|
|
130
|
+
key,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get multiple values from cache in a single request
|
|
136
|
+
* Returns a map of key -> value (or null if not found)
|
|
137
|
+
* Gracefully handles 404 errors (endpoint not implemented) by returning empty results
|
|
138
|
+
*/
|
|
139
|
+
async multiGet(
|
|
140
|
+
dmap: string,
|
|
141
|
+
keys: string[]
|
|
142
|
+
): Promise<Map<string, any | null>> {
|
|
143
|
+
try {
|
|
144
|
+
if (keys.length === 0) {
|
|
145
|
+
return new Map();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const response = await this.httpClient.post<CacheMultiGetResponse>(
|
|
149
|
+
"/v1/cache/mget",
|
|
150
|
+
{
|
|
151
|
+
dmap,
|
|
152
|
+
keys,
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Convert array to Map
|
|
157
|
+
const resultMap = new Map<string, any | null>();
|
|
158
|
+
|
|
159
|
+
// First, mark all keys as null (cache miss)
|
|
160
|
+
keys.forEach((key) => {
|
|
161
|
+
resultMap.set(key, null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Then, update with found values
|
|
165
|
+
if (response.results) {
|
|
166
|
+
response.results.forEach(({ key, value }) => {
|
|
167
|
+
resultMap.set(key, value);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return resultMap;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Handle 404 errors silently (endpoint not implemented on backend)
|
|
174
|
+
// This is expected behavior when the backend doesn't support multiGet yet
|
|
175
|
+
if (error instanceof SDKError && error.httpStatus === 404) {
|
|
176
|
+
// Return map with all nulls silently - caller can fall back to individual gets
|
|
177
|
+
const resultMap = new Map<string, any | null>();
|
|
178
|
+
keys.forEach((key) => {
|
|
179
|
+
resultMap.set(key, null);
|
|
180
|
+
});
|
|
181
|
+
return resultMap;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Log and return empty results for other errors
|
|
185
|
+
const resultMap = new Map<string, any | null>();
|
|
186
|
+
keys.forEach((key) => {
|
|
187
|
+
resultMap.set(key, null);
|
|
188
|
+
});
|
|
189
|
+
console.error(`[CacheClient] Error in multiGet for ${dmap}:`, error);
|
|
190
|
+
return resultMap;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Scan keys in a distributed map, optionally matching a regex pattern
|
|
196
|
+
*/
|
|
197
|
+
async scan(dmap: string, match?: string): Promise<CacheScanResponse> {
|
|
198
|
+
return this.httpClient.post<CacheScanResponse>("/v1/cache/scan", {
|
|
199
|
+
dmap,
|
|
200
|
+
match,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/core/http.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class HttpClient {
|
|
|
19
19
|
|
|
20
20
|
constructor(config: HttpClientConfig) {
|
|
21
21
|
this.baseURL = config.baseURL.replace(/\/$/, "");
|
|
22
|
-
this.timeout = config.timeout ??
|
|
22
|
+
this.timeout = config.timeout ?? 60000; // Increased from 30s to 60s for pub/sub operations
|
|
23
23
|
this.maxRetries = config.maxRetries ?? 3;
|
|
24
24
|
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
|
25
25
|
this.fetch = config.fetch ?? globalThis.fetch;
|
|
@@ -27,20 +27,64 @@ export class HttpClient {
|
|
|
27
27
|
|
|
28
28
|
setApiKey(apiKey?: string) {
|
|
29
29
|
this.apiKey = apiKey;
|
|
30
|
-
|
|
30
|
+
// Don't clear JWT - allow both to coexist
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
setJwt(jwt?: string) {
|
|
34
34
|
this.jwt = jwt;
|
|
35
|
-
|
|
35
|
+
// Don't clear API key - allow both to coexist
|
|
36
|
+
if (typeof console !== "undefined") {
|
|
37
|
+
console.log(
|
|
38
|
+
"[HttpClient] JWT set:",
|
|
39
|
+
!!jwt,
|
|
40
|
+
"API key still present:",
|
|
41
|
+
!!this.apiKey
|
|
42
|
+
);
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
private getAuthHeaders(): Record<string, string> {
|
|
46
|
+
private getAuthHeaders(path: string): Record<string, string> {
|
|
39
47
|
const headers: Record<string, string> = {};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
|
|
49
|
+
// For database, pubsub, proxy, and cache operations, ONLY use API key to avoid JWT user context
|
|
50
|
+
// interfering with namespace-level authorization
|
|
51
|
+
const isDbOperation = path.includes("/v1/rqlite/");
|
|
52
|
+
const isPubSubOperation = path.includes("/v1/pubsub/");
|
|
53
|
+
const isProxyOperation = path.includes("/v1/proxy/");
|
|
54
|
+
const isCacheOperation = path.includes("/v1/cache/");
|
|
55
|
+
|
|
56
|
+
// For auth operations, prefer API key over JWT to ensure proper authentication
|
|
57
|
+
const isAuthOperation = path.includes("/v1/auth/");
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
isDbOperation ||
|
|
61
|
+
isPubSubOperation ||
|
|
62
|
+
isProxyOperation ||
|
|
63
|
+
isCacheOperation
|
|
64
|
+
) {
|
|
65
|
+
// For database/pubsub/proxy/cache operations: use only API key (preferred for namespace operations)
|
|
66
|
+
if (this.apiKey) {
|
|
67
|
+
headers["X-API-Key"] = this.apiKey;
|
|
68
|
+
} else if (this.jwt) {
|
|
69
|
+
// Fallback to JWT if no API key
|
|
70
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
71
|
+
}
|
|
72
|
+
} else if (isAuthOperation) {
|
|
73
|
+
// For auth operations: prefer API key over JWT (auth endpoints should use explicit API key)
|
|
74
|
+
if (this.apiKey) {
|
|
75
|
+
headers["X-API-Key"] = this.apiKey;
|
|
76
|
+
}
|
|
77
|
+
if (this.jwt) {
|
|
78
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// For other operations: send both JWT and API key
|
|
82
|
+
if (this.jwt) {
|
|
83
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
84
|
+
}
|
|
85
|
+
if (this.apiKey) {
|
|
86
|
+
headers["X-API-Key"] = this.apiKey;
|
|
87
|
+
}
|
|
44
88
|
}
|
|
45
89
|
return headers;
|
|
46
90
|
}
|
|
@@ -49,6 +93,10 @@ export class HttpClient {
|
|
|
49
93
|
return this.jwt || this.apiKey;
|
|
50
94
|
}
|
|
51
95
|
|
|
96
|
+
getApiKey(): string | undefined {
|
|
97
|
+
return this.apiKey;
|
|
98
|
+
}
|
|
99
|
+
|
|
52
100
|
async request<T = any>(
|
|
53
101
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
54
102
|
path: string,
|
|
@@ -56,8 +104,10 @@ export class HttpClient {
|
|
|
56
104
|
body?: any;
|
|
57
105
|
headers?: Record<string, string>;
|
|
58
106
|
query?: Record<string, string | number | boolean>;
|
|
107
|
+
timeout?: number; // Per-request timeout override
|
|
59
108
|
} = {}
|
|
60
109
|
): Promise<T> {
|
|
110
|
+
const startTime = performance.now(); // Track request start time
|
|
61
111
|
const url = new URL(this.baseURL + path);
|
|
62
112
|
if (options.query) {
|
|
63
113
|
Object.entries(options.query).forEach(([key, value]) => {
|
|
@@ -67,27 +117,114 @@ export class HttpClient {
|
|
|
67
117
|
|
|
68
118
|
const headers: Record<string, string> = {
|
|
69
119
|
"Content-Type": "application/json",
|
|
70
|
-
...this.getAuthHeaders(),
|
|
120
|
+
...this.getAuthHeaders(path),
|
|
71
121
|
...options.headers,
|
|
72
122
|
};
|
|
73
123
|
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
const requestTimeout = options.timeout ?? this.timeout; // Use override or default
|
|
126
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
127
|
+
|
|
74
128
|
const fetchOptions: RequestInit = {
|
|
75
129
|
method,
|
|
76
130
|
headers,
|
|
77
|
-
signal:
|
|
131
|
+
signal: controller.signal,
|
|
78
132
|
};
|
|
79
133
|
|
|
80
134
|
if (options.body !== undefined) {
|
|
81
135
|
fetchOptions.body = JSON.stringify(options.body);
|
|
82
136
|
}
|
|
83
137
|
|
|
84
|
-
|
|
138
|
+
try {
|
|
139
|
+
const result = await this.requestWithRetry(
|
|
140
|
+
url.toString(),
|
|
141
|
+
fetchOptions,
|
|
142
|
+
0,
|
|
143
|
+
startTime
|
|
144
|
+
);
|
|
145
|
+
const duration = performance.now() - startTime;
|
|
146
|
+
if (typeof console !== "undefined") {
|
|
147
|
+
console.log(
|
|
148
|
+
`[HttpClient] ${method} ${path} completed in ${duration.toFixed(2)}ms`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const duration = performance.now() - startTime;
|
|
154
|
+
if (typeof console !== "undefined") {
|
|
155
|
+
// Cache "key not found" (404 or error message) is expected behavior - don't log as error
|
|
156
|
+
const isCacheGetNotFound =
|
|
157
|
+
path === "/v1/cache/get" &&
|
|
158
|
+
error instanceof SDKError &&
|
|
159
|
+
(error.httpStatus === 404 ||
|
|
160
|
+
(error.httpStatus === 500 &&
|
|
161
|
+
error.message?.toLowerCase().includes("key not found")));
|
|
162
|
+
|
|
163
|
+
// "Not found" (404) for blocked_users is expected behavior - don't log as error
|
|
164
|
+
// This happens when checking if users are blocked (most users aren't blocked)
|
|
165
|
+
const isBlockedUsersNotFound =
|
|
166
|
+
path === "/v1/rqlite/find-one" &&
|
|
167
|
+
error instanceof SDKError &&
|
|
168
|
+
error.httpStatus === 404 &&
|
|
169
|
+
options.body &&
|
|
170
|
+
(() => {
|
|
171
|
+
try {
|
|
172
|
+
const body =
|
|
173
|
+
typeof options.body === "string"
|
|
174
|
+
? JSON.parse(options.body)
|
|
175
|
+
: options.body;
|
|
176
|
+
return body.table === "blocked_users";
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
|
|
182
|
+
// "Not found" (404) for conversation_participants is expected behavior - don't log as error
|
|
183
|
+
// This happens when checking if a user is a participant (e.g., on first group join)
|
|
184
|
+
const isConversationParticipantNotFound =
|
|
185
|
+
path === "/v1/rqlite/find-one" &&
|
|
186
|
+
error instanceof SDKError &&
|
|
187
|
+
error.httpStatus === 404 &&
|
|
188
|
+
options.body &&
|
|
189
|
+
(() => {
|
|
190
|
+
try {
|
|
191
|
+
const body =
|
|
192
|
+
typeof options.body === "string"
|
|
193
|
+
? JSON.parse(options.body)
|
|
194
|
+
: options.body;
|
|
195
|
+
return body.table === "conversation_participants";
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
isCacheGetNotFound ||
|
|
203
|
+
isBlockedUsersNotFound ||
|
|
204
|
+
isConversationParticipantNotFound
|
|
205
|
+
) {
|
|
206
|
+
// Log cache miss, non-blocked status, or non-participant status as debug/info, not error
|
|
207
|
+
// These are expected behaviors
|
|
208
|
+
} else {
|
|
209
|
+
console.error(
|
|
210
|
+
`[HttpClient] ${method} ${path} failed after ${duration.toFixed(
|
|
211
|
+
2
|
|
212
|
+
)}ms:`,
|
|
213
|
+
error
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
} finally {
|
|
219
|
+
clearTimeout(timeoutId);
|
|
220
|
+
}
|
|
85
221
|
}
|
|
86
222
|
|
|
87
223
|
private async requestWithRetry(
|
|
88
224
|
url: string,
|
|
89
225
|
options: RequestInit,
|
|
90
|
-
attempt: number = 0
|
|
226
|
+
attempt: number = 0,
|
|
227
|
+
startTime?: number // Track start time for timing across retries
|
|
91
228
|
): Promise<any> {
|
|
92
229
|
try {
|
|
93
230
|
const response = await this.fetch(url, options);
|
|
@@ -116,7 +253,7 @@ export class HttpClient {
|
|
|
116
253
|
await new Promise((resolve) =>
|
|
117
254
|
setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
|
118
255
|
);
|
|
119
|
-
return this.requestWithRetry(url, options, attempt + 1);
|
|
256
|
+
return this.requestWithRetry(url, options, attempt + 1, startTime);
|
|
120
257
|
}
|
|
121
258
|
throw error;
|
|
122
259
|
}
|
|
@@ -152,6 +289,104 @@ export class HttpClient {
|
|
|
152
289
|
return this.request<T>("DELETE", path, options);
|
|
153
290
|
}
|
|
154
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Upload a file using multipart/form-data
|
|
294
|
+
* This is a special method for file uploads that bypasses JSON serialization
|
|
295
|
+
*/
|
|
296
|
+
async uploadFile<T = any>(
|
|
297
|
+
path: string,
|
|
298
|
+
formData: FormData,
|
|
299
|
+
options?: {
|
|
300
|
+
timeout?: number;
|
|
301
|
+
}
|
|
302
|
+
): Promise<T> {
|
|
303
|
+
const startTime = performance.now(); // Track upload start time
|
|
304
|
+
const url = new URL(this.baseURL + path);
|
|
305
|
+
const headers: Record<string, string> = {
|
|
306
|
+
...this.getAuthHeaders(path),
|
|
307
|
+
// Don't set Content-Type - browser will set it with boundary
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const controller = new AbortController();
|
|
311
|
+
const requestTimeout = options?.timeout ?? this.timeout * 5; // 5x timeout for uploads
|
|
312
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
313
|
+
|
|
314
|
+
const fetchOptions: RequestInit = {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers,
|
|
317
|
+
body: formData,
|
|
318
|
+
signal: controller.signal,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const result = await this.requestWithRetry(
|
|
323
|
+
url.toString(),
|
|
324
|
+
fetchOptions,
|
|
325
|
+
0,
|
|
326
|
+
startTime
|
|
327
|
+
);
|
|
328
|
+
const duration = performance.now() - startTime;
|
|
329
|
+
if (typeof console !== "undefined") {
|
|
330
|
+
console.log(
|
|
331
|
+
`[HttpClient] POST ${path} (upload) completed in ${duration.toFixed(
|
|
332
|
+
2
|
|
333
|
+
)}ms`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const duration = performance.now() - startTime;
|
|
339
|
+
if (typeof console !== "undefined") {
|
|
340
|
+
console.error(
|
|
341
|
+
`[HttpClient] POST ${path} (upload) failed after ${duration.toFixed(
|
|
342
|
+
2
|
|
343
|
+
)}ms:`,
|
|
344
|
+
error
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
} finally {
|
|
349
|
+
clearTimeout(timeoutId);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get a binary response (returns Response object for streaming)
|
|
355
|
+
*/
|
|
356
|
+
async getBinary(path: string): Promise<Response> {
|
|
357
|
+
const url = new URL(this.baseURL + path);
|
|
358
|
+
const headers: Record<string, string> = {
|
|
359
|
+
...this.getAuthHeaders(path),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const controller = new AbortController();
|
|
363
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout * 5); // 5x timeout for downloads
|
|
364
|
+
|
|
365
|
+
const fetchOptions: RequestInit = {
|
|
366
|
+
method: "GET",
|
|
367
|
+
headers,
|
|
368
|
+
signal: controller.signal,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const response = await this.fetch(url.toString(), fetchOptions);
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
clearTimeout(timeoutId);
|
|
375
|
+
const error = await response.json().catch(() => ({
|
|
376
|
+
error: response.statusText,
|
|
377
|
+
}));
|
|
378
|
+
throw SDKError.fromResponse(response.status, error);
|
|
379
|
+
}
|
|
380
|
+
return response;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
clearTimeout(timeoutId);
|
|
383
|
+
if (error instanceof SDKError) {
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
155
390
|
getToken(): string | undefined {
|
|
156
391
|
return this.getAuthToken();
|
|
157
392
|
}
|