@debros/orama 0.122.4-nightly
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 +21 -0
- package/README.md +665 -0
- package/dist/index.d.ts +1334 -0
- package/dist/index.js +2553 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/auth/client.ts +276 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/types.ts +62 -0
- package/src/cache/client.ts +203 -0
- package/src/cache/index.ts +14 -0
- package/src/core/http.ts +541 -0
- 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 +246 -0
- package/src/db/client.ts +126 -0
- package/src/db/index.ts +13 -0
- package/src/db/qb.ts +111 -0
- package/src/db/repository.ts +128 -0
- package/src/db/types.ts +67 -0
- package/src/errors.ts +38 -0
- package/src/functions/client.ts +62 -0
- package/src/functions/index.ts +2 -0
- package/src/functions/types.ts +21 -0
- package/src/index.ts +201 -0
- package/src/network/client.ts +119 -0
- package/src/network/index.ts +7 -0
- package/src/pubsub/client.ts +361 -0
- package/src/pubsub/index.ts +12 -0
- package/src/pubsub/types.ts +46 -0
- package/src/storage/client.ts +272 -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/vault/auth.ts +98 -0
- package/src/vault/client.ts +197 -0
- package/src/vault/crypto/aes.ts +271 -0
- package/src/vault/crypto/hkdf.ts +42 -0
- package/src/vault/crypto/index.ts +27 -0
- package/src/vault/crypto/shamir.ts +173 -0
- package/src/vault/index.ts +65 -0
- package/src/vault/quorum.ts +16 -0
- package/src/vault/transport/fanout.ts +94 -0
- package/src/vault/transport/guardian.ts +285 -0
- package/src/vault/transport/index.ts +19 -0
- package/src/vault/transport/types.ts +101 -0
- package/src/vault/types.ts +62 -0
|
@@ -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: (code: number, reason: string) => void): void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unregister close handler
|
|
48
|
+
*/
|
|
49
|
+
offClose(handler: (code: number, reason: string) => 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
|
+
}
|
package/src/core/ws.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import WebSocket from "isomorphic-ws";
|
|
2
|
+
import { SDKError } from "../errors";
|
|
3
|
+
import { NetworkErrorCallback } from "./http";
|
|
4
|
+
|
|
5
|
+
export interface WSClientConfig {
|
|
6
|
+
wsURL: string;
|
|
7
|
+
timeout?: number;
|
|
8
|
+
authToken?: string;
|
|
9
|
+
WebSocket?: typeof WebSocket;
|
|
10
|
+
/**
|
|
11
|
+
* Callback invoked on WebSocket errors.
|
|
12
|
+
* Use this to trigger gateway failover at the application layer.
|
|
13
|
+
*/
|
|
14
|
+
onNetworkError?: NetworkErrorCallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type WSMessageHandler = (data: string) => void;
|
|
18
|
+
export type WSErrorHandler = (error: Error) => void;
|
|
19
|
+
export type WSCloseHandler = (code: number, reason: string) => void;
|
|
20
|
+
export type WSOpenHandler = () => void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Simple WebSocket client with minimal abstractions
|
|
24
|
+
* No complex reconnection, no failover - keep it simple
|
|
25
|
+
* Gateway failover is handled at the application layer
|
|
26
|
+
*/
|
|
27
|
+
export class WSClient {
|
|
28
|
+
private wsURL: string;
|
|
29
|
+
private timeout: number;
|
|
30
|
+
private authToken?: string;
|
|
31
|
+
private WebSocketClass: typeof WebSocket;
|
|
32
|
+
private onNetworkError?: NetworkErrorCallback;
|
|
33
|
+
|
|
34
|
+
private ws?: WebSocket;
|
|
35
|
+
private messageHandlers: Set<WSMessageHandler> = new Set();
|
|
36
|
+
private errorHandlers: Set<WSErrorHandler> = new Set();
|
|
37
|
+
private closeHandlers: Set<WSCloseHandler> = new Set();
|
|
38
|
+
private openHandlers: Set<WSOpenHandler> = new Set();
|
|
39
|
+
private isClosed = false;
|
|
40
|
+
|
|
41
|
+
constructor(config: WSClientConfig) {
|
|
42
|
+
this.wsURL = config.wsURL;
|
|
43
|
+
this.timeout = config.timeout ?? 30000;
|
|
44
|
+
this.authToken = config.authToken;
|
|
45
|
+
this.WebSocketClass = config.WebSocket ?? WebSocket;
|
|
46
|
+
this.onNetworkError = config.onNetworkError;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set the network error callback
|
|
51
|
+
*/
|
|
52
|
+
setOnNetworkError(callback: NetworkErrorCallback | undefined): void {
|
|
53
|
+
this.onNetworkError = callback;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the current WebSocket URL
|
|
58
|
+
*/
|
|
59
|
+
get url(): string {
|
|
60
|
+
return this.wsURL;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Connect to WebSocket server
|
|
65
|
+
*/
|
|
66
|
+
connect(): Promise<void> {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
try {
|
|
69
|
+
const wsUrl = this.buildWSUrl();
|
|
70
|
+
this.ws = new this.WebSocketClass(wsUrl);
|
|
71
|
+
this.isClosed = false;
|
|
72
|
+
|
|
73
|
+
const timeout = setTimeout(() => {
|
|
74
|
+
this.ws?.close();
|
|
75
|
+
const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT");
|
|
76
|
+
|
|
77
|
+
// Call the network error callback if configured
|
|
78
|
+
if (this.onNetworkError) {
|
|
79
|
+
this.onNetworkError(error, {
|
|
80
|
+
method: "WS",
|
|
81
|
+
path: this.wsURL,
|
|
82
|
+
isRetry: false,
|
|
83
|
+
attempt: 0,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
reject(error);
|
|
88
|
+
}, this.timeout);
|
|
89
|
+
|
|
90
|
+
this.ws.addEventListener("open", () => {
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
console.log("[WSClient] Connected to", this.wsURL);
|
|
93
|
+
this.openHandlers.forEach((handler) => handler());
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.ws.addEventListener("message", (event: Event) => {
|
|
98
|
+
const msgEvent = event as MessageEvent;
|
|
99
|
+
this.messageHandlers.forEach((handler) => handler(msgEvent.data));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.ws.addEventListener("error", (event: Event) => {
|
|
103
|
+
console.error("[WSClient] WebSocket error:", event);
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
// Extract useful details from the event — raw Event objects don't serialize
|
|
106
|
+
const details: Record<string, any> = { type: event.type };
|
|
107
|
+
if ("message" in event) {
|
|
108
|
+
details.message = (event as ErrorEvent).message;
|
|
109
|
+
}
|
|
110
|
+
const error = new SDKError("WebSocket error", 0, "WS_ERROR", details);
|
|
111
|
+
|
|
112
|
+
// Call the network error callback if configured
|
|
113
|
+
if (this.onNetworkError) {
|
|
114
|
+
this.onNetworkError(error, {
|
|
115
|
+
method: "WS",
|
|
116
|
+
path: this.wsURL,
|
|
117
|
+
isRetry: false,
|
|
118
|
+
attempt: 0,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.errorHandlers.forEach((handler) => handler(error));
|
|
123
|
+
reject(error);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.ws.addEventListener("close", (event: Event) => {
|
|
127
|
+
clearTimeout(timeout);
|
|
128
|
+
const closeEvent = event as CloseEvent;
|
|
129
|
+
const code = closeEvent.code ?? 1006;
|
|
130
|
+
const reason = closeEvent.reason ?? "";
|
|
131
|
+
console.log(`[WSClient] Connection closed (code: ${code}, reason: ${reason || "none"})`);
|
|
132
|
+
this.closeHandlers.forEach((handler) => handler(code, reason));
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
reject(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build WebSocket URL with auth token
|
|
142
|
+
*/
|
|
143
|
+
private buildWSUrl(): string {
|
|
144
|
+
let url = this.wsURL;
|
|
145
|
+
|
|
146
|
+
if (this.authToken) {
|
|
147
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
148
|
+
const paramName = this.authToken.startsWith("ak_") ? "api_key" : "token";
|
|
149
|
+
// API keys contain a colon (ak_xxx:namespace) that must not be percent-encoded
|
|
150
|
+
const encodedToken = this.authToken.startsWith("ak_")
|
|
151
|
+
? this.authToken
|
|
152
|
+
: encodeURIComponent(this.authToken);
|
|
153
|
+
url += `${separator}${paramName}=${encodedToken}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return url;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Register message handler
|
|
161
|
+
*/
|
|
162
|
+
onMessage(handler: WSMessageHandler): () => void {
|
|
163
|
+
this.messageHandlers.add(handler);
|
|
164
|
+
return () => this.messageHandlers.delete(handler);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Unregister message handler
|
|
169
|
+
*/
|
|
170
|
+
offMessage(handler: WSMessageHandler): void {
|
|
171
|
+
this.messageHandlers.delete(handler);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Register error handler
|
|
176
|
+
*/
|
|
177
|
+
onError(handler: WSErrorHandler): () => void {
|
|
178
|
+
this.errorHandlers.add(handler);
|
|
179
|
+
return () => this.errorHandlers.delete(handler);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Unregister error handler
|
|
184
|
+
*/
|
|
185
|
+
offError(handler: WSErrorHandler): void {
|
|
186
|
+
this.errorHandlers.delete(handler);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Register close handler
|
|
191
|
+
*/
|
|
192
|
+
onClose(handler: WSCloseHandler): () => void {
|
|
193
|
+
this.closeHandlers.add(handler);
|
|
194
|
+
return () => this.closeHandlers.delete(handler);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Unregister close handler
|
|
199
|
+
*/
|
|
200
|
+
offClose(handler: WSCloseHandler): void {
|
|
201
|
+
this.closeHandlers.delete(handler);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Register open handler
|
|
206
|
+
*/
|
|
207
|
+
onOpen(handler: WSOpenHandler): () => void {
|
|
208
|
+
this.openHandlers.add(handler);
|
|
209
|
+
return () => this.openHandlers.delete(handler);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Send data through WebSocket
|
|
214
|
+
*/
|
|
215
|
+
send(data: string): void {
|
|
216
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
217
|
+
throw new SDKError("WebSocket is not connected", 0, "WS_NOT_CONNECTED");
|
|
218
|
+
}
|
|
219
|
+
this.ws.send(data);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Close WebSocket connection
|
|
224
|
+
*/
|
|
225
|
+
close(): void {
|
|
226
|
+
if (this.isClosed) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.isClosed = true;
|
|
230
|
+
this.ws?.close();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if WebSocket is connected
|
|
235
|
+
*/
|
|
236
|
+
isConnected(): boolean {
|
|
237
|
+
return !this.isClosed && this.ws?.readyState === WebSocket.OPEN;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Update auth token
|
|
242
|
+
*/
|
|
243
|
+
setAuthToken(token?: string): void {
|
|
244
|
+
this.authToken = token;
|
|
245
|
+
}
|
|
246
|
+
}
|