@debros/network-ts-sdk 0.1.4 → 0.2.5

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/src/core/ws.ts CHANGED
@@ -4,10 +4,6 @@ import { SDKError } from "../errors";
4
4
  export interface WSClientConfig {
5
5
  wsURL: string;
6
6
  timeout?: number;
7
- maxReconnectAttempts?: number;
8
- reconnectDelayMs?: number;
9
- heartbeatIntervalMs?: number;
10
- authMode?: "header" | "query";
11
7
  authToken?: string;
12
8
  WebSocket?: typeof WebSocket;
13
9
  }
@@ -15,54 +11,53 @@ export interface WSClientConfig {
15
11
  export type WSMessageHandler = (data: string) => void;
16
12
  export type WSErrorHandler = (error: Error) => void;
17
13
  export type WSCloseHandler = () => void;
14
+ export type WSOpenHandler = () => void;
18
15
 
16
+ /**
17
+ * Simple WebSocket client with minimal abstractions
18
+ * No complex reconnection, no heartbeats - keep it simple
19
+ */
19
20
  export class WSClient {
20
21
  private url: string;
21
22
  private timeout: number;
22
- private maxReconnectAttempts: number;
23
- private reconnectDelayMs: number;
24
- private heartbeatIntervalMs: number;
25
- private authMode: "header" | "query";
26
23
  private authToken?: string;
27
24
  private WebSocketClass: typeof WebSocket;
28
25
 
29
26
  private ws?: WebSocket;
30
- private reconnectAttempts = 0;
31
- private heartbeatInterval?: NodeJS.Timeout;
32
27
  private messageHandlers: Set<WSMessageHandler> = new Set();
33
28
  private errorHandlers: Set<WSErrorHandler> = new Set();
34
29
  private closeHandlers: Set<WSCloseHandler> = new Set();
35
- private isManuallyClosed = false;
30
+ private openHandlers: Set<WSOpenHandler> = new Set();
31
+ private isClosed = false;
36
32
 
37
33
  constructor(config: WSClientConfig) {
38
34
  this.url = config.wsURL;
39
35
  this.timeout = config.timeout ?? 30000;
40
- this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5;
41
- this.reconnectDelayMs = config.reconnectDelayMs ?? 1000;
42
- this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? 30000;
43
- this.authMode = config.authMode ?? "header";
44
36
  this.authToken = config.authToken;
45
37
  this.WebSocketClass = config.WebSocket ?? WebSocket;
46
38
  }
47
39
 
40
+ /**
41
+ * Connect to WebSocket server
42
+ */
48
43
  connect(): Promise<void> {
49
44
  return new Promise((resolve, reject) => {
50
45
  try {
51
46
  const wsUrl = this.buildWSUrl();
52
47
  this.ws = new this.WebSocketClass(wsUrl);
53
-
54
- // Note: Custom headers via ws library in Node.js are not sent with WebSocket upgrade requests
55
- // so we rely on query parameters for authentication
48
+ this.isClosed = false;
56
49
 
57
50
  const timeout = setTimeout(() => {
58
51
  this.ws?.close();
59
- reject(new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT"));
52
+ reject(
53
+ new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT")
54
+ );
60
55
  }, this.timeout);
61
56
 
62
57
  this.ws.addEventListener("open", () => {
63
58
  clearTimeout(timeout);
64
- this.reconnectAttempts = 0;
65
- this.startHeartbeat();
59
+ console.log("[WSClient] Connected to", this.url);
60
+ this.openHandlers.forEach((handler) => handler());
66
61
  resolve();
67
62
  });
68
63
 
@@ -72,24 +67,16 @@ export class WSClient {
72
67
  });
73
68
 
74
69
  this.ws.addEventListener("error", (event: Event) => {
70
+ console.error("[WSClient] WebSocket error:", event);
75
71
  clearTimeout(timeout);
76
- const error = new SDKError(
77
- "WebSocket error",
78
- 500,
79
- "WS_ERROR",
80
- event
81
- );
72
+ const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
82
73
  this.errorHandlers.forEach((handler) => handler(error));
83
74
  });
84
75
 
85
76
  this.ws.addEventListener("close", () => {
86
77
  clearTimeout(timeout);
87
- this.stopHeartbeat();
88
- if (!this.isManuallyClosed) {
89
- this.attemptReconnect();
90
- } else {
91
- this.closeHandlers.forEach((handler) => handler());
92
- }
78
+ console.log("[WSClient] Connection closed");
79
+ this.closeHandlers.forEach((handler) => handler());
93
80
  });
94
81
  } catch (error) {
95
82
  reject(error);
@@ -97,86 +84,106 @@ export class WSClient {
97
84
  });
98
85
  }
99
86
 
87
+ /**
88
+ * Build WebSocket URL with auth token
89
+ */
100
90
  private buildWSUrl(): string {
101
91
  let url = this.url;
102
-
103
- // Always append auth token as query parameter for compatibility
104
- // Works in both Node.js and browser environments
92
+
105
93
  if (this.authToken) {
106
94
  const separator = url.includes("?") ? "&" : "?";
107
95
  const paramName = this.authToken.startsWith("ak_") ? "api_key" : "token";
108
96
  url += `${separator}${paramName}=${encodeURIComponent(this.authToken)}`;
109
97
  }
110
-
111
- return url;
112
- }
113
98
 
114
- private startHeartbeat() {
115
- this.heartbeatInterval = setInterval(() => {
116
- if (this.ws?.readyState === WebSocket.OPEN) {
117
- this.ws.send(JSON.stringify({ type: "ping" }));
118
- }
119
- }, this.heartbeatIntervalMs);
120
- }
121
-
122
- private stopHeartbeat() {
123
- if (this.heartbeatInterval) {
124
- clearInterval(this.heartbeatInterval);
125
- this.heartbeatInterval = undefined;
126
- }
127
- }
128
-
129
- private attemptReconnect() {
130
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
131
- this.reconnectAttempts++;
132
- const delayMs = this.reconnectDelayMs * this.reconnectAttempts;
133
- setTimeout(() => {
134
- this.connect().catch((error) => {
135
- this.errorHandlers.forEach((handler) => handler(error));
136
- });
137
- }, delayMs);
138
- } else {
139
- this.closeHandlers.forEach((handler) => handler());
140
- }
99
+ return url;
141
100
  }
142
101
 
143
- onMessage(handler: WSMessageHandler) {
102
+ /**
103
+ * Register message handler
104
+ */
105
+ onMessage(handler: WSMessageHandler): () => void {
144
106
  this.messageHandlers.add(handler);
145
107
  return () => this.messageHandlers.delete(handler);
146
108
  }
147
109
 
148
- onError(handler: WSErrorHandler) {
110
+ /**
111
+ * Unregister message handler
112
+ */
113
+ offMessage(handler: WSMessageHandler): void {
114
+ this.messageHandlers.delete(handler);
115
+ }
116
+
117
+ /**
118
+ * Register error handler
119
+ */
120
+ onError(handler: WSErrorHandler): () => void {
149
121
  this.errorHandlers.add(handler);
150
122
  return () => this.errorHandlers.delete(handler);
151
123
  }
152
124
 
153
- onClose(handler: WSCloseHandler) {
125
+ /**
126
+ * Unregister error handler
127
+ */
128
+ offError(handler: WSErrorHandler): void {
129
+ this.errorHandlers.delete(handler);
130
+ }
131
+
132
+ /**
133
+ * Register close handler
134
+ */
135
+ onClose(handler: WSCloseHandler): () => void {
154
136
  this.closeHandlers.add(handler);
155
137
  return () => this.closeHandlers.delete(handler);
156
138
  }
157
139
 
158
- send(data: string) {
140
+ /**
141
+ * Unregister close handler
142
+ */
143
+ offClose(handler: WSCloseHandler): void {
144
+ this.closeHandlers.delete(handler);
145
+ }
146
+
147
+ /**
148
+ * Register open handler
149
+ */
150
+ onOpen(handler: WSOpenHandler): () => void {
151
+ this.openHandlers.add(handler);
152
+ return () => this.openHandlers.delete(handler);
153
+ }
154
+
155
+ /**
156
+ * Send data through WebSocket
157
+ */
158
+ send(data: string): void {
159
159
  if (this.ws?.readyState !== WebSocket.OPEN) {
160
- throw new SDKError(
161
- "WebSocket is not connected",
162
- 500,
163
- "WS_NOT_CONNECTED"
164
- );
160
+ throw new SDKError("WebSocket is not connected", 500, "WS_NOT_CONNECTED");
165
161
  }
166
162
  this.ws.send(data);
167
163
  }
168
164
 
169
- close() {
170
- this.isManuallyClosed = true;
171
- this.stopHeartbeat();
165
+ /**
166
+ * Close WebSocket connection
167
+ */
168
+ close(): void {
169
+ if (this.isClosed) {
170
+ return;
171
+ }
172
+ this.isClosed = true;
172
173
  this.ws?.close();
173
174
  }
174
175
 
176
+ /**
177
+ * Check if WebSocket is connected
178
+ */
175
179
  isConnected(): boolean {
176
- return this.ws?.readyState === WebSocket.OPEN;
180
+ return !this.isClosed && this.ws?.readyState === WebSocket.OPEN;
177
181
  }
178
182
 
179
- setAuthToken(token?: string) {
183
+ /**
184
+ * Update auth token
185
+ */
186
+ setAuthToken(token?: string): void {
180
187
  this.authToken = token;
181
188
  }
182
189
  }
package/src/index.ts CHANGED
@@ -4,7 +4,11 @@ import { DBClient } from "./db/client";
4
4
  import { PubSubClient } from "./pubsub/client";
5
5
  import { NetworkClient } from "./network/client";
6
6
  import { WSClientConfig } from "./core/ws";
7
- import { StorageAdapter, MemoryStorage, LocalStorageAdapter } from "./auth/types";
7
+ import {
8
+ StorageAdapter,
9
+ MemoryStorage,
10
+ LocalStorageAdapter,
11
+ } from "./auth/types";
8
12
 
9
13
  export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
10
14
  apiKey?: string;
@@ -38,8 +42,9 @@ export function createClient(config: ClientConfig): Client {
38
42
  });
39
43
 
40
44
  // Derive WebSocket URL from baseURL if not explicitly provided
41
- const wsURL = config.wsConfig?.wsURL ??
42
- config.baseURL.replace(/^http/, 'ws').replace(/\/$/, '');
45
+ const wsURL =
46
+ config.wsConfig?.wsURL ??
47
+ config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
43
48
 
44
49
  const db = new DBClient(httpClient);
45
50
  const pubsub = new PubSubClient(httpClient, {
@@ -67,11 +72,7 @@ export { PubSubClient, Subscription } from "./pubsub/client";
67
72
  export { NetworkClient } from "./network/client";
68
73
  export { SDKError } from "./errors";
69
74
  export { MemoryStorage, LocalStorageAdapter } from "./auth/types";
70
- export type {
71
- StorageAdapter,
72
- AuthConfig,
73
- WhoAmI,
74
- } from "./auth/types";
75
+ export type { StorageAdapter, AuthConfig, WhoAmI } from "./auth/types";
75
76
  export type * from "./db/types";
76
77
  export type {
77
78
  Message,
@@ -79,4 +80,9 @@ export type {
79
80
  ErrorHandler,
80
81
  CloseHandler,
81
82
  } from "./pubsub/client";
82
- export type { PeerInfo, NetworkStatus } from "./network/client";
83
+ export type {
84
+ PeerInfo,
85
+ NetworkStatus,
86
+ ProxyRequest,
87
+ ProxyResponse,
88
+ } from "./network/client";
@@ -7,9 +7,25 @@ export interface PeerInfo {
7
7
  }
8
8
 
9
9
  export interface NetworkStatus {
10
- healthy: boolean;
11
- peers: number;
12
- uptime?: number;
10
+ node_id: string;
11
+ connected: boolean;
12
+ peer_count: number;
13
+ database_size: number;
14
+ uptime: number;
15
+ }
16
+
17
+ export interface ProxyRequest {
18
+ url: string;
19
+ method: string;
20
+ headers?: Record<string, string>;
21
+ body?: string;
22
+ }
23
+
24
+ export interface ProxyResponse {
25
+ status_code: number;
26
+ headers: Record<string, string>;
27
+ body: string;
28
+ error?: string;
13
29
  }
14
30
 
15
31
  export class NetworkClient {
@@ -35,7 +51,9 @@ export class NetworkClient {
35
51
  * Get network status.
36
52
  */
37
53
  async status(): Promise<NetworkStatus> {
38
- const response = await this.httpClient.get<NetworkStatus>("/v1/status");
54
+ const response = await this.httpClient.get<NetworkStatus>(
55
+ "/v1/network/status"
56
+ );
39
57
  return response;
40
58
  }
41
59
 
@@ -62,4 +80,40 @@ export class NetworkClient {
62
80
  async disconnect(peerId: string): Promise<void> {
63
81
  await this.httpClient.post("/v1/network/disconnect", { peer_id: peerId });
64
82
  }
83
+
84
+ /**
85
+ * Proxy an HTTP request through the Anyone network.
86
+ * Requires authentication (API key or JWT).
87
+ *
88
+ * @param request - The proxy request configuration
89
+ * @returns The proxied response
90
+ * @throws {SDKError} If the Anyone proxy is not available or the request fails
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const response = await client.network.proxyAnon({
95
+ * url: 'https://api.example.com/data',
96
+ * method: 'GET',
97
+ * headers: {
98
+ * 'Accept': 'application/json'
99
+ * }
100
+ * });
101
+ *
102
+ * console.log(response.status_code); // 200
103
+ * console.log(response.body); // Response data
104
+ * ```
105
+ */
106
+ async proxyAnon(request: ProxyRequest): Promise<ProxyResponse> {
107
+ const response = await this.httpClient.post<ProxyResponse>(
108
+ "/v1/proxy/anon",
109
+ request
110
+ );
111
+
112
+ // Check if the response contains an error
113
+ if (response.error) {
114
+ throw new Error(`Proxy request failed: ${response.error}`);
115
+ }
116
+
117
+ return response;
118
+ }
65
119
  }
@@ -4,13 +4,64 @@ import { WSClient, WSClientConfig } from "../core/ws";
4
4
  export interface Message {
5
5
  data: string;
6
6
  topic: string;
7
- timestamp?: number;
7
+ timestamp: number;
8
+ }
9
+
10
+ export interface RawEnvelope {
11
+ data: string; // base64-encoded
12
+ timestamp: number;
13
+ topic: string;
14
+ }
15
+
16
+ // Cross-platform base64 encoding/decoding utilities
17
+ function base64Encode(str: string): string {
18
+ if (typeof Buffer !== "undefined") {
19
+ return Buffer.from(str).toString("base64");
20
+ } else if (typeof btoa !== "undefined") {
21
+ return btoa(
22
+ encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) =>
23
+ String.fromCharCode(parseInt(p1, 16))
24
+ )
25
+ );
26
+ }
27
+ throw new Error("No base64 encoding method available");
28
+ }
29
+
30
+ function base64EncodeBytes(bytes: Uint8Array): string {
31
+ if (typeof Buffer !== "undefined") {
32
+ return Buffer.from(bytes).toString("base64");
33
+ } else if (typeof btoa !== "undefined") {
34
+ let binary = "";
35
+ for (let i = 0; i < bytes.length; i++) {
36
+ binary += String.fromCharCode(bytes[i]);
37
+ }
38
+ return btoa(binary);
39
+ }
40
+ throw new Error("No base64 encoding method available");
41
+ }
42
+
43
+ function base64Decode(b64: string): string {
44
+ if (typeof Buffer !== "undefined") {
45
+ return Buffer.from(b64, "base64").toString("utf-8");
46
+ } else if (typeof atob !== "undefined") {
47
+ const binary = atob(b64);
48
+ const bytes = new Uint8Array(binary.length);
49
+ for (let i = 0; i < binary.length; i++) {
50
+ bytes[i] = binary.charCodeAt(i);
51
+ }
52
+ return new TextDecoder().decode(bytes);
53
+ }
54
+ throw new Error("No base64 decoding method available");
8
55
  }
9
56
 
10
57
  export type MessageHandler = (message: Message) => void;
11
58
  export type ErrorHandler = (error: Error) => void;
12
59
  export type CloseHandler = () => void;
13
60
 
61
+ /**
62
+ * Simple PubSub client - one WebSocket connection per topic
63
+ * No connection pooling, no reference counting - keep it simple
64
+ */
14
65
  export class PubSubClient {
15
66
  private httpClient: HttpClient;
16
67
  private wsConfig: Partial<WSClientConfig>;
@@ -21,20 +72,30 @@ export class PubSubClient {
21
72
  }
22
73
 
23
74
  /**
24
- * Publish a message to a topic.
75
+ * Publish a message to a topic via HTTP
25
76
  */
26
77
  async publish(topic: string, data: string | Uint8Array): Promise<void> {
27
- const dataBase64 =
28
- typeof data === "string" ? Buffer.from(data).toString("base64") : Buffer.from(data).toString("base64");
78
+ let dataBase64: string;
79
+ if (typeof data === "string") {
80
+ dataBase64 = base64Encode(data);
81
+ } else {
82
+ dataBase64 = base64EncodeBytes(data);
83
+ }
29
84
 
30
- await this.httpClient.post("/v1/pubsub/publish", {
31
- topic,
32
- data_base64: dataBase64,
33
- });
85
+ await this.httpClient.post(
86
+ "/v1/pubsub/publish",
87
+ {
88
+ topic,
89
+ data_base64: dataBase64,
90
+ },
91
+ {
92
+ timeout: 30000,
93
+ }
94
+ );
34
95
  }
35
96
 
36
97
  /**
37
- * List active topics in the current namespace.
98
+ * List active topics in the current namespace
38
99
  */
39
100
  async topics(): Promise<string[]> {
40
101
  const response = await this.httpClient.get<{ topics: string[] }>(
@@ -44,8 +105,8 @@ export class PubSubClient {
44
105
  }
45
106
 
46
107
  /**
47
- * Subscribe to a topic via WebSocket.
48
- * Returns a subscription object with event handlers.
108
+ * Subscribe to a topic via WebSocket
109
+ * Creates one WebSocket connection per topic
49
110
  */
50
111
  async subscribe(
51
112
  topic: string,
@@ -55,16 +116,23 @@ export class PubSubClient {
55
116
  onClose?: CloseHandler;
56
117
  } = {}
57
118
  ): Promise<Subscription> {
58
- const wsUrl = new URL(this.wsConfig.wsURL || "ws://localhost:6001");
119
+ // Build WebSocket URL for this topic
120
+ const wsUrl = new URL(this.wsConfig.wsURL || "ws://127.0.0.1:6001");
59
121
  wsUrl.pathname = "/v1/pubsub/ws";
60
122
  wsUrl.searchParams.set("topic", topic);
61
123
 
124
+ const authToken = this.httpClient.getApiKey() ?? this.httpClient.getToken();
125
+
126
+ // Create WebSocket client
62
127
  const wsClient = new WSClient({
63
128
  ...this.wsConfig,
64
129
  wsURL: wsUrl.toString(),
65
- authToken: this.httpClient.getToken(),
130
+ authToken,
66
131
  });
67
132
 
133
+ await wsClient.connect();
134
+
135
+ // Create subscription wrapper
68
136
  const subscription = new Subscription(wsClient, topic);
69
137
 
70
138
  if (handlers.onMessage) {
@@ -77,66 +145,144 @@ export class PubSubClient {
77
145
  subscription.onClose(handlers.onClose);
78
146
  }
79
147
 
80
- await wsClient.connect();
81
148
  return subscription;
82
149
  }
83
150
  }
84
151
 
152
+ /**
153
+ * Subscription represents an active WebSocket subscription to a topic
154
+ */
85
155
  export class Subscription {
86
156
  private wsClient: WSClient;
87
157
  private topic: string;
88
158
  private messageHandlers: Set<MessageHandler> = new Set();
89
159
  private errorHandlers: Set<ErrorHandler> = new Set();
90
160
  private closeHandlers: Set<CloseHandler> = new Set();
161
+ private isClosed = false;
162
+ private wsMessageHandler: ((data: string) => void) | null = null;
163
+ private wsErrorHandler: ((error: Error) => void) | null = null;
164
+ private wsCloseHandler: (() => void) | null = null;
91
165
 
92
166
  constructor(wsClient: WSClient, topic: string) {
93
167
  this.wsClient = wsClient;
94
168
  this.topic = topic;
95
169
 
96
- this.wsClient.onMessage((data) => {
170
+ // Register message handler
171
+ this.wsMessageHandler = (data) => {
97
172
  try {
173
+ // Parse gateway JSON envelope: {data: base64String, timestamp, topic}
174
+ const envelope: RawEnvelope = JSON.parse(data);
175
+
176
+ // Validate envelope structure
177
+ if (!envelope || typeof envelope !== "object") {
178
+ throw new Error("Invalid envelope: not an object");
179
+ }
180
+ if (!envelope.data || typeof envelope.data !== "string") {
181
+ throw new Error("Invalid envelope: missing or invalid data field");
182
+ }
183
+ if (!envelope.topic || typeof envelope.topic !== "string") {
184
+ throw new Error("Invalid envelope: missing or invalid topic field");
185
+ }
186
+ if (typeof envelope.timestamp !== "number") {
187
+ throw new Error(
188
+ "Invalid envelope: missing or invalid timestamp field"
189
+ );
190
+ }
191
+
192
+ // Decode base64 data
193
+ const messageData = base64Decode(envelope.data);
194
+
98
195
  const message: Message = {
99
- topic: this.topic,
100
- data: data,
101
- timestamp: Date.now(),
196
+ topic: envelope.topic,
197
+ data: messageData,
198
+ timestamp: envelope.timestamp,
102
199
  };
200
+
201
+ console.log("[Subscription] Received message on topic:", this.topic);
103
202
  this.messageHandlers.forEach((handler) => handler(message));
104
203
  } catch (error) {
204
+ console.error("[Subscription] Error processing message:", error);
105
205
  this.errorHandlers.forEach((handler) =>
106
206
  handler(error instanceof Error ? error : new Error(String(error)))
107
207
  );
108
208
  }
109
- });
209
+ };
210
+
211
+ this.wsClient.onMessage(this.wsMessageHandler);
110
212
 
111
- this.wsClient.onError((error) => {
213
+ // Register error handler
214
+ this.wsErrorHandler = (error) => {
112
215
  this.errorHandlers.forEach((handler) => handler(error));
113
- });
216
+ };
217
+ this.wsClient.onError(this.wsErrorHandler);
114
218
 
115
- this.wsClient.onClose(() => {
219
+ // Register close handler
220
+ this.wsCloseHandler = () => {
116
221
  this.closeHandlers.forEach((handler) => handler());
117
- });
222
+ };
223
+ this.wsClient.onClose(this.wsCloseHandler);
118
224
  }
119
225
 
120
- onMessage(handler: MessageHandler) {
226
+ /**
227
+ * Register message handler
228
+ */
229
+ onMessage(handler: MessageHandler): () => void {
121
230
  this.messageHandlers.add(handler);
122
231
  return () => this.messageHandlers.delete(handler);
123
232
  }
124
233
 
125
- onError(handler: ErrorHandler) {
234
+ /**
235
+ * Register error handler
236
+ */
237
+ onError(handler: ErrorHandler): () => void {
126
238
  this.errorHandlers.add(handler);
127
239
  return () => this.errorHandlers.delete(handler);
128
240
  }
129
241
 
130
- onClose(handler: CloseHandler) {
242
+ /**
243
+ * Register close handler
244
+ */
245
+ onClose(handler: CloseHandler): () => void {
131
246
  this.closeHandlers.add(handler);
132
247
  return () => this.closeHandlers.delete(handler);
133
248
  }
134
249
 
135
- close() {
250
+ /**
251
+ * Close subscription and underlying WebSocket
252
+ */
253
+ close(): void {
254
+ if (this.isClosed) {
255
+ return;
256
+ }
257
+ this.isClosed = true;
258
+
259
+ // Remove handlers from WSClient
260
+ if (this.wsMessageHandler) {
261
+ this.wsClient.offMessage(this.wsMessageHandler);
262
+ this.wsMessageHandler = null;
263
+ }
264
+ if (this.wsErrorHandler) {
265
+ this.wsClient.offError(this.wsErrorHandler);
266
+ this.wsErrorHandler = null;
267
+ }
268
+ if (this.wsCloseHandler) {
269
+ this.wsClient.offClose(this.wsCloseHandler);
270
+ this.wsCloseHandler = null;
271
+ }
272
+
273
+ // Clear all local handlers
274
+ this.messageHandlers.clear();
275
+ this.errorHandlers.clear();
276
+ this.closeHandlers.clear();
277
+
278
+ // Close WebSocket connection
136
279
  this.wsClient.close();
137
280
  }
138
281
 
282
+ /**
283
+ * Check if subscription is active
284
+ */
139
285
  isConnected(): boolean {
140
- return this.wsClient.isConnected();
286
+ return !this.isClosed && this.wsClient.isConnected();
141
287
  }
142
288
  }