@debros/network-ts-sdk 0.4.2 → 0.6.0
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/LICENSE +1 -1
- package/README.md +1 -0
- package/dist/index.d.ts +60 -16
- package/dist/index.js +82 -11
- package/dist/index.js.map +1 -1
- package/package.json +26 -3
- package/src/auth/index.ts +3 -0
- package/src/cache/index.ts +14 -0
- package/src/core/http.ts +101 -6
- package/src/core/index.ts +10 -0
- package/src/core/interfaces/IAuthStrategy.ts +28 -0
- package/src/core/interfaces/IHttpTransport.ts +73 -0
- package/src/core/interfaces/IRetryPolicy.ts +20 -0
- package/src/core/interfaces/IWebSocketClient.ts +60 -0
- package/src/core/interfaces/index.ts +4 -0
- package/src/core/transport/AuthHeaderStrategy.ts +108 -0
- package/src/core/transport/RequestLogger.ts +116 -0
- package/src/core/transport/RequestRetryPolicy.ts +53 -0
- package/src/core/transport/TLSConfiguration.ts +53 -0
- package/src/core/transport/index.ts +4 -0
- package/src/core/ws.ts +39 -3
- package/src/db/index.ts +13 -0
- package/src/functions/index.ts +2 -0
- package/src/index.ts +10 -1
- package/src/network/index.ts +7 -0
- package/src/pubsub/index.ts +12 -0
- package/src/storage/index.ts +7 -0
- package/src/utils/codec.ts +68 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/platform.ts +44 -0
- package/src/utils/retry.ts +58 -0
package/src/core/http.ts
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { SDKError } from "../errors";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Context provided to the onNetworkError callback
|
|
5
|
+
*/
|
|
6
|
+
export interface NetworkErrorContext {
|
|
7
|
+
method: "GET" | "POST" | "PUT" | "DELETE" | "WS";
|
|
8
|
+
path: string;
|
|
9
|
+
isRetry: boolean;
|
|
10
|
+
attempt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Callback invoked when a network error occurs.
|
|
15
|
+
* Use this to trigger gateway failover or other error handling.
|
|
16
|
+
*/
|
|
17
|
+
export type NetworkErrorCallback = (
|
|
18
|
+
error: SDKError,
|
|
19
|
+
context: NetworkErrorContext
|
|
20
|
+
) => void;
|
|
21
|
+
|
|
3
22
|
export interface HttpClientConfig {
|
|
4
23
|
baseURL: string;
|
|
5
24
|
timeout?: number;
|
|
6
25
|
maxRetries?: number;
|
|
7
26
|
retryDelayMs?: number;
|
|
8
27
|
fetch?: typeof fetch;
|
|
28
|
+
/**
|
|
29
|
+
* Enable debug logging (includes full SQL queries and args). Default: false
|
|
30
|
+
*/
|
|
31
|
+
debug?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Callback invoked on network errors (after all retries exhausted).
|
|
34
|
+
* Use this to trigger gateway failover at the application layer.
|
|
35
|
+
*/
|
|
36
|
+
onNetworkError?: NetworkErrorCallback;
|
|
9
37
|
}
|
|
10
38
|
|
|
11
39
|
/**
|
|
@@ -39,6 +67,8 @@ export class HttpClient {
|
|
|
39
67
|
private fetch: typeof fetch;
|
|
40
68
|
private apiKey?: string;
|
|
41
69
|
private jwt?: string;
|
|
70
|
+
private debug: boolean;
|
|
71
|
+
private onNetworkError?: NetworkErrorCallback;
|
|
42
72
|
|
|
43
73
|
constructor(config: HttpClientConfig) {
|
|
44
74
|
this.baseURL = config.baseURL.replace(/\/$/, "");
|
|
@@ -47,6 +77,15 @@ export class HttpClient {
|
|
|
47
77
|
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
|
48
78
|
// Use provided fetch or create one with proper TLS configuration for staging certificates
|
|
49
79
|
this.fetch = config.fetch ?? createFetchWithTLSConfig();
|
|
80
|
+
this.debug = config.debug ?? false;
|
|
81
|
+
this.onNetworkError = config.onNetworkError;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Set the network error callback
|
|
86
|
+
*/
|
|
87
|
+
setOnNetworkError(callback: NetworkErrorCallback | undefined): void {
|
|
88
|
+
this.onNetworkError = callback;
|
|
50
89
|
}
|
|
51
90
|
|
|
52
91
|
setApiKey(apiKey?: string) {
|
|
@@ -223,7 +262,7 @@ export class HttpClient {
|
|
|
223
262
|
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(
|
|
224
263
|
2
|
|
225
264
|
)}ms`;
|
|
226
|
-
if (queryDetails) {
|
|
265
|
+
if (queryDetails && this.debug) {
|
|
227
266
|
console.log(logMessage);
|
|
228
267
|
console.log(`[HttpClient] ${queryDetails}`);
|
|
229
268
|
} else {
|
|
@@ -253,11 +292,32 @@ export class HttpClient {
|
|
|
253
292
|
2
|
|
254
293
|
)}ms:`;
|
|
255
294
|
console.error(errorMessage, error);
|
|
256
|
-
if (queryDetails) {
|
|
295
|
+
if (queryDetails && this.debug) {
|
|
257
296
|
console.error(`[HttpClient] ${queryDetails}`);
|
|
258
297
|
}
|
|
259
298
|
}
|
|
260
299
|
}
|
|
300
|
+
|
|
301
|
+
// Call the network error callback if configured
|
|
302
|
+
// This allows the app to trigger gateway failover
|
|
303
|
+
if (this.onNetworkError) {
|
|
304
|
+
// Convert native errors (TypeError, AbortError) to SDKError for the callback
|
|
305
|
+
const sdkError =
|
|
306
|
+
error instanceof SDKError
|
|
307
|
+
? error
|
|
308
|
+
: new SDKError(
|
|
309
|
+
error instanceof Error ? error.message : String(error),
|
|
310
|
+
0, // httpStatus 0 indicates network-level failure
|
|
311
|
+
"NETWORK_ERROR"
|
|
312
|
+
);
|
|
313
|
+
this.onNetworkError(sdkError, {
|
|
314
|
+
method,
|
|
315
|
+
path,
|
|
316
|
+
isRetry: false,
|
|
317
|
+
attempt: this.maxRetries, // All retries exhausted
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
261
321
|
throw error;
|
|
262
322
|
} finally {
|
|
263
323
|
clearTimeout(timeoutId);
|
|
@@ -397,6 +457,25 @@ export class HttpClient {
|
|
|
397
457
|
error
|
|
398
458
|
);
|
|
399
459
|
}
|
|
460
|
+
|
|
461
|
+
// Call the network error callback if configured
|
|
462
|
+
if (this.onNetworkError) {
|
|
463
|
+
const sdkError =
|
|
464
|
+
error instanceof SDKError
|
|
465
|
+
? error
|
|
466
|
+
: new SDKError(
|
|
467
|
+
error instanceof Error ? error.message : String(error),
|
|
468
|
+
0,
|
|
469
|
+
"NETWORK_ERROR"
|
|
470
|
+
);
|
|
471
|
+
this.onNetworkError(sdkError, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
path,
|
|
474
|
+
isRetry: false,
|
|
475
|
+
attempt: this.maxRetries,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
400
479
|
throw error;
|
|
401
480
|
} finally {
|
|
402
481
|
clearTimeout(timeoutId);
|
|
@@ -425,17 +504,33 @@ export class HttpClient {
|
|
|
425
504
|
const response = await this.fetch(url.toString(), fetchOptions);
|
|
426
505
|
if (!response.ok) {
|
|
427
506
|
clearTimeout(timeoutId);
|
|
428
|
-
const
|
|
507
|
+
const errorBody = await response.json().catch(() => ({
|
|
429
508
|
error: response.statusText,
|
|
430
509
|
}));
|
|
431
|
-
throw SDKError.fromResponse(response.status,
|
|
510
|
+
throw SDKError.fromResponse(response.status, errorBody);
|
|
432
511
|
}
|
|
433
512
|
return response;
|
|
434
513
|
} catch (error) {
|
|
435
514
|
clearTimeout(timeoutId);
|
|
436
|
-
|
|
437
|
-
|
|
515
|
+
|
|
516
|
+
// Call the network error callback if configured
|
|
517
|
+
if (this.onNetworkError) {
|
|
518
|
+
const sdkError =
|
|
519
|
+
error instanceof SDKError
|
|
520
|
+
? error
|
|
521
|
+
: new SDKError(
|
|
522
|
+
error instanceof Error ? error.message : String(error),
|
|
523
|
+
0,
|
|
524
|
+
"NETWORK_ERROR"
|
|
525
|
+
);
|
|
526
|
+
this.onNetworkError(sdkError, {
|
|
527
|
+
method: "GET",
|
|
528
|
+
path,
|
|
529
|
+
isRetry: false,
|
|
530
|
+
attempt: 0,
|
|
531
|
+
});
|
|
438
532
|
}
|
|
533
|
+
|
|
439
534
|
throw error;
|
|
440
535
|
}
|
|
441
536
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { HttpClient, type HttpClientConfig, type NetworkErrorCallback, type NetworkErrorContext } from "./http";
|
|
2
|
+
export { WSClient, type WSClientConfig } from "./ws";
|
|
3
|
+
export type { IHttpTransport, RequestOptions } from "./interfaces/IHttpTransport";
|
|
4
|
+
export type { IWebSocketClient } from "./interfaces/IWebSocketClient";
|
|
5
|
+
export type { IAuthStrategy, RequestContext } from "./interfaces/IAuthStrategy";
|
|
6
|
+
export type { IRetryPolicy } from "./interfaces/IRetryPolicy";
|
|
7
|
+
export { PathBasedAuthStrategy } from "./transport/AuthHeaderStrategy";
|
|
8
|
+
export { ExponentialBackoffRetryPolicy } from "./transport/RequestRetryPolicy";
|
|
9
|
+
export { RequestLogger } from "./transport/RequestLogger";
|
|
10
|
+
export { TLSConfiguration } from "./transport/TLSConfiguration";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request context for authentication
|
|
3
|
+
*/
|
|
4
|
+
export interface RequestContext {
|
|
5
|
+
path: string;
|
|
6
|
+
method: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Authentication strategy interface
|
|
11
|
+
* Provides abstraction for different authentication header strategies
|
|
12
|
+
*/
|
|
13
|
+
export interface IAuthStrategy {
|
|
14
|
+
/**
|
|
15
|
+
* Get authentication headers for a request
|
|
16
|
+
*/
|
|
17
|
+
getHeaders(context: RequestContext): Record<string, string>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set API key
|
|
21
|
+
*/
|
|
22
|
+
setApiKey(apiKey?: string): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set JWT token
|
|
26
|
+
*/
|
|
27
|
+
setJwt(jwt?: string): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Request options
|
|
3
|
+
*/
|
|
4
|
+
export interface RequestOptions {
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
query?: Record<string, string | number | boolean>;
|
|
7
|
+
timeout?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HTTP Transport abstraction interface
|
|
12
|
+
* Provides a testable abstraction layer for HTTP operations
|
|
13
|
+
*/
|
|
14
|
+
export interface IHttpTransport {
|
|
15
|
+
/**
|
|
16
|
+
* Perform GET request
|
|
17
|
+
*/
|
|
18
|
+
get<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Perform POST request
|
|
22
|
+
*/
|
|
23
|
+
post<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Perform PUT request
|
|
27
|
+
*/
|
|
28
|
+
put<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Perform DELETE request
|
|
32
|
+
*/
|
|
33
|
+
delete<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Upload file using multipart/form-data
|
|
37
|
+
*/
|
|
38
|
+
uploadFile<T = any>(
|
|
39
|
+
path: string,
|
|
40
|
+
formData: FormData,
|
|
41
|
+
options?: { timeout?: number }
|
|
42
|
+
): Promise<T>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get binary response (returns Response object for streaming)
|
|
46
|
+
*/
|
|
47
|
+
getBinary(path: string): Promise<Response>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get base URL
|
|
51
|
+
*/
|
|
52
|
+
getBaseURL(): string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get API key
|
|
56
|
+
*/
|
|
57
|
+
getApiKey(): string | undefined;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get current token (JWT or API key)
|
|
61
|
+
*/
|
|
62
|
+
getToken(): string | undefined;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set API key for authentication
|
|
66
|
+
*/
|
|
67
|
+
setApiKey(apiKey?: string): void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set JWT token for authentication
|
|
71
|
+
*/
|
|
72
|
+
setJwt(jwt?: string): void;
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry policy interface
|
|
3
|
+
* Provides abstraction for retry logic and backoff strategies
|
|
4
|
+
*/
|
|
5
|
+
export interface IRetryPolicy {
|
|
6
|
+
/**
|
|
7
|
+
* Determine if request should be retried
|
|
8
|
+
*/
|
|
9
|
+
shouldRetry(error: any, attempt: number): boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get delay before next retry attempt (in milliseconds)
|
|
13
|
+
*/
|
|
14
|
+
getDelay(attempt: number): number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get maximum number of retry attempts
|
|
18
|
+
*/
|
|
19
|
+
getMaxRetries(): number;
|
|
20
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Client abstraction interface
|
|
3
|
+
* Provides a testable abstraction layer for WebSocket operations
|
|
4
|
+
*/
|
|
5
|
+
export interface IWebSocketClient {
|
|
6
|
+
/**
|
|
7
|
+
* Connect to WebSocket server
|
|
8
|
+
*/
|
|
9
|
+
connect(): Promise<void>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Close WebSocket connection
|
|
13
|
+
*/
|
|
14
|
+
close(): void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Send data through WebSocket
|
|
18
|
+
*/
|
|
19
|
+
send(data: string): void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register message handler
|
|
23
|
+
*/
|
|
24
|
+
onMessage(handler: (data: string) => void): void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unregister message handler
|
|
28
|
+
*/
|
|
29
|
+
offMessage(handler: (data: string) => void): void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register error handler
|
|
33
|
+
*/
|
|
34
|
+
onError(handler: (error: Error) => void): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Unregister error handler
|
|
38
|
+
*/
|
|
39
|
+
offError(handler: (error: Error) => void): void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register close handler
|
|
43
|
+
*/
|
|
44
|
+
onClose(handler: () => void): void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unregister close handler
|
|
48
|
+
*/
|
|
49
|
+
offClose(handler: () => void): void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if WebSocket is connected
|
|
53
|
+
*/
|
|
54
|
+
isConnected(): boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get WebSocket URL
|
|
58
|
+
*/
|
|
59
|
+
get url(): string;
|
|
60
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { IAuthStrategy, RequestContext } from "../interfaces/IAuthStrategy";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authentication type for different operations
|
|
5
|
+
*/
|
|
6
|
+
type AuthType = "api-key-only" | "api-key-preferred" | "jwt-preferred" | "both";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Path-based authentication strategy
|
|
10
|
+
* Determines which auth credentials to use based on the request path
|
|
11
|
+
*/
|
|
12
|
+
export class PathBasedAuthStrategy implements IAuthStrategy {
|
|
13
|
+
private apiKey?: string;
|
|
14
|
+
private jwt?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mapping of path patterns to auth types
|
|
18
|
+
*/
|
|
19
|
+
private readonly authRules: Array<{ pattern: string; type: AuthType }> = [
|
|
20
|
+
// Database, PubSub, Proxy, Cache: prefer API key
|
|
21
|
+
{ pattern: "/v1/rqlite/", type: "api-key-only" },
|
|
22
|
+
{ pattern: "/v1/pubsub/", type: "api-key-only" },
|
|
23
|
+
{ pattern: "/v1/proxy/", type: "api-key-only" },
|
|
24
|
+
{ pattern: "/v1/cache/", type: "api-key-only" },
|
|
25
|
+
// Auth operations: prefer API key
|
|
26
|
+
{ pattern: "/v1/auth/", type: "api-key-preferred" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
constructor(apiKey?: string, jwt?: string) {
|
|
30
|
+
this.apiKey = apiKey;
|
|
31
|
+
this.jwt = jwt;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get authentication headers for a request
|
|
36
|
+
*/
|
|
37
|
+
getHeaders(context: RequestContext): Record<string, string> {
|
|
38
|
+
const headers: Record<string, string> = {};
|
|
39
|
+
const authType = this.detectAuthType(context.path);
|
|
40
|
+
|
|
41
|
+
switch (authType) {
|
|
42
|
+
case "api-key-only":
|
|
43
|
+
if (this.apiKey) {
|
|
44
|
+
headers["X-API-Key"] = this.apiKey;
|
|
45
|
+
} else if (this.jwt) {
|
|
46
|
+
// Fallback to JWT if no API key
|
|
47
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case "api-key-preferred":
|
|
52
|
+
if (this.apiKey) {
|
|
53
|
+
headers["X-API-Key"] = this.apiKey;
|
|
54
|
+
}
|
|
55
|
+
if (this.jwt) {
|
|
56
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case "jwt-preferred":
|
|
61
|
+
if (this.jwt) {
|
|
62
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
63
|
+
}
|
|
64
|
+
if (this.apiKey) {
|
|
65
|
+
headers["X-API-Key"] = this.apiKey;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case "both":
|
|
70
|
+
if (this.jwt) {
|
|
71
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
72
|
+
}
|
|
73
|
+
if (this.apiKey) {
|
|
74
|
+
headers["X-API-Key"] = this.apiKey;
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return headers;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Set API key
|
|
84
|
+
*/
|
|
85
|
+
setApiKey(apiKey?: string): void {
|
|
86
|
+
this.apiKey = apiKey;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set JWT token
|
|
91
|
+
*/
|
|
92
|
+
setJwt(jwt?: string): void {
|
|
93
|
+
this.jwt = jwt;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect auth type based on path
|
|
98
|
+
*/
|
|
99
|
+
private detectAuthType(path: string): AuthType {
|
|
100
|
+
for (const rule of this.authRules) {
|
|
101
|
+
if (path.includes(rule.pattern)) {
|
|
102
|
+
return rule.type;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Default: send both if available
|
|
106
|
+
return "both";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request logger for debugging HTTP operations
|
|
3
|
+
*/
|
|
4
|
+
export class RequestLogger {
|
|
5
|
+
private readonly debug: boolean;
|
|
6
|
+
|
|
7
|
+
constructor(debug: boolean = false) {
|
|
8
|
+
this.debug = debug;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Log successful request
|
|
13
|
+
*/
|
|
14
|
+
logSuccess(
|
|
15
|
+
method: string,
|
|
16
|
+
path: string,
|
|
17
|
+
duration: number,
|
|
18
|
+
queryDetails?: string
|
|
19
|
+
): void {
|
|
20
|
+
if (typeof console === "undefined") return;
|
|
21
|
+
|
|
22
|
+
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(2)}ms`;
|
|
23
|
+
|
|
24
|
+
if (queryDetails && this.debug) {
|
|
25
|
+
console.log(logMessage);
|
|
26
|
+
console.log(`[HttpClient] ${queryDetails}`);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(logMessage);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Log failed request
|
|
34
|
+
*/
|
|
35
|
+
logError(
|
|
36
|
+
method: string,
|
|
37
|
+
path: string,
|
|
38
|
+
duration: number,
|
|
39
|
+
error: any,
|
|
40
|
+
queryDetails?: string
|
|
41
|
+
): void {
|
|
42
|
+
if (typeof console === "undefined") return;
|
|
43
|
+
|
|
44
|
+
// Special handling for 404 on find-one (expected behavior)
|
|
45
|
+
const is404FindOne =
|
|
46
|
+
path === "/v1/rqlite/find-one" &&
|
|
47
|
+
error?.httpStatus === 404;
|
|
48
|
+
|
|
49
|
+
if (is404FindOne) {
|
|
50
|
+
console.warn(
|
|
51
|
+
`[HttpClient] ${method} ${path} returned 404 after ${duration.toFixed(2)}ms (expected for optional lookups)`
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const errorMessage = `[HttpClient] ${method} ${path} failed after ${duration.toFixed(2)}ms:`;
|
|
57
|
+
console.error(errorMessage, error);
|
|
58
|
+
|
|
59
|
+
if (queryDetails && this.debug) {
|
|
60
|
+
console.error(`[HttpClient] ${queryDetails}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract query details from request for logging
|
|
66
|
+
*/
|
|
67
|
+
extractQueryDetails(path: string, body?: any): string | null {
|
|
68
|
+
if (!this.debug) return null;
|
|
69
|
+
|
|
70
|
+
const isRqliteOperation = path.includes("/v1/rqlite/");
|
|
71
|
+
if (!isRqliteOperation || !body) return null;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const parsedBody = typeof body === "string" ? JSON.parse(body) : body;
|
|
75
|
+
|
|
76
|
+
// Direct SQL query
|
|
77
|
+
if (parsedBody.sql) {
|
|
78
|
+
let details = `SQL: ${parsedBody.sql}`;
|
|
79
|
+
if (parsedBody.args && parsedBody.args.length > 0) {
|
|
80
|
+
details += ` | Args: [${parsedBody.args
|
|
81
|
+
.map((a: any) => (typeof a === "string" ? `"${a}"` : a))
|
|
82
|
+
.join(", ")}]`;
|
|
83
|
+
}
|
|
84
|
+
return details;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Table-based query
|
|
88
|
+
if (parsedBody.table) {
|
|
89
|
+
let details = `Table: ${parsedBody.table}`;
|
|
90
|
+
if (parsedBody.criteria && Object.keys(parsedBody.criteria).length > 0) {
|
|
91
|
+
details += ` | Criteria: ${JSON.stringify(parsedBody.criteria)}`;
|
|
92
|
+
}
|
|
93
|
+
if (parsedBody.options) {
|
|
94
|
+
details += ` | Options: ${JSON.stringify(parsedBody.options)}`;
|
|
95
|
+
}
|
|
96
|
+
if (parsedBody.select) {
|
|
97
|
+
details += ` | Select: ${JSON.stringify(parsedBody.select)}`;
|
|
98
|
+
}
|
|
99
|
+
if (parsedBody.where) {
|
|
100
|
+
details += ` | Where: ${JSON.stringify(parsedBody.where)}`;
|
|
101
|
+
}
|
|
102
|
+
if (parsedBody.limit) {
|
|
103
|
+
details += ` | Limit: ${parsedBody.limit}`;
|
|
104
|
+
}
|
|
105
|
+
if (parsedBody.offset) {
|
|
106
|
+
details += ` | Offset: ${parsedBody.offset}`;
|
|
107
|
+
}
|
|
108
|
+
return details;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Failed to parse, ignore
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { IRetryPolicy } from "../interfaces/IRetryPolicy";
|
|
2
|
+
import { SDKError } from "../../errors";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Exponential backoff retry policy
|
|
6
|
+
* Retries failed requests with increasing delays
|
|
7
|
+
*/
|
|
8
|
+
export class ExponentialBackoffRetryPolicy implements IRetryPolicy {
|
|
9
|
+
private readonly maxRetries: number;
|
|
10
|
+
private readonly baseDelayMs: number;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* HTTP status codes that should trigger a retry
|
|
14
|
+
*/
|
|
15
|
+
private readonly retryableStatusCodes = [408, 429, 500, 502, 503, 504];
|
|
16
|
+
|
|
17
|
+
constructor(maxRetries: number = 3, baseDelayMs: number = 1000) {
|
|
18
|
+
this.maxRetries = maxRetries;
|
|
19
|
+
this.baseDelayMs = baseDelayMs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Determine if request should be retried
|
|
24
|
+
*/
|
|
25
|
+
shouldRetry(error: any, attempt: number): boolean {
|
|
26
|
+
// Don't retry if max attempts reached
|
|
27
|
+
if (attempt >= this.maxRetries) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Retry on retryable HTTP errors
|
|
32
|
+
if (error instanceof SDKError) {
|
|
33
|
+
return this.retryableStatusCodes.includes(error.httpStatus);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Don't retry other errors
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get delay before next retry (exponential backoff)
|
|
42
|
+
*/
|
|
43
|
+
getDelay(attempt: number): number {
|
|
44
|
+
return this.baseDelayMs * (attempt + 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get maximum number of retry attempts
|
|
49
|
+
*/
|
|
50
|
+
getMaxRetries(): number {
|
|
51
|
+
return this.maxRetries;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS Configuration for development/staging environments
|
|
3
|
+
*
|
|
4
|
+
* WARNING: Only use this in development/testing environments!
|
|
5
|
+
* DO NOT disable certificate validation in production.
|
|
6
|
+
*/
|
|
7
|
+
export class TLSConfiguration {
|
|
8
|
+
/**
|
|
9
|
+
* Create fetch function with proper TLS configuration
|
|
10
|
+
*/
|
|
11
|
+
static createFetchWithTLSConfig(): typeof fetch {
|
|
12
|
+
// Only allow insecure TLS in development
|
|
13
|
+
if (this.shouldAllowInsecure()) {
|
|
14
|
+
this.configureInsecureTLS();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return globalThis.fetch;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if insecure TLS should be allowed
|
|
22
|
+
*/
|
|
23
|
+
private static shouldAllowInsecure(): boolean {
|
|
24
|
+
// Check if we're in Node.js environment
|
|
25
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Only allow in non-production with explicit flag
|
|
30
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
31
|
+
const allowInsecure = process.env.DEBROS_ALLOW_INSECURE_TLS === "true";
|
|
32
|
+
|
|
33
|
+
return !isProduction && allowInsecure;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configure Node.js to allow insecure TLS
|
|
38
|
+
* WARNING: Only call in development!
|
|
39
|
+
*/
|
|
40
|
+
private static configureInsecureTLS(): void {
|
|
41
|
+
if (typeof process !== "undefined" && process.env) {
|
|
42
|
+
// Allow self-signed/staging certificates for development
|
|
43
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
44
|
+
|
|
45
|
+
if (typeof console !== "undefined") {
|
|
46
|
+
console.warn(
|
|
47
|
+
"[TLSConfiguration] WARNING: TLS certificate validation disabled for development. " +
|
|
48
|
+
"DO NOT use in production!"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|