@bandeira-tech/b3nd-web 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 B3nd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ b3nd/sdk provides client/server application protocol with multiple backend support and batteries included
2
+
3
+ b3nd/sdk is intended to support the design of allowing applications to manage their data schemas on their clientside frontends and use a secure and scalable backend solution that is flexible both ways both for the client app and the backend provider
4
+
5
+ b3nd/sdk is used in applications that are nodes to a network that may be local and made of 1 and can run even on your browser using local storage or indexeddb or in memory even, same for a script, or can connect to an http api that runs a simple deno kv or sqlite backend, or connects with multiple other nodes via websocket and http to broadcast and distribute persistence
6
+
7
+ ## Target Module Components Topology
8
+
9
+ b3nd/sdk
10
+ - backends/{memory,http,websocket,localStorage,denokv,postgres,...}
11
+ - client
12
+ - types
13
+
14
+ b3nd/sdk/backends export unified interfaces for different backends, they require initialization with shared standards like backend schema that maps program urls (protocol://toplevel) to validation functions, and also take custom configuration related to the actual backend, i.e. connection string for postgres, url for websocket and http and so on
15
+
16
+ b3nd/sdk/client exports unified interface to route message for multiple backends, it's initialized with a client schema that maps programs urls (protocol://toplevel) to target backend instance programs
17
+
18
+ So while backend schema defines what programs are supported and available in a backend instance, the client schema defines what programs are routed to what backends
19
+
20
+ This way browser apps can communicate with multiple http and websocket backends, as well as have a local instance; also http apis and websocket servers can be setup in meshes to work together for HA or other distributed designs
21
+
22
+ ## Development
23
+
24
+ b3nd/sdk must
25
+
26
+ - ALWAYS have a test for each component to automate validation and simplify troubleshooting
27
+ - NEVER catch/hide/garble errors
28
+ - ALWAYS leave it to the user to decide how to best handle errors for their applications
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @b3nd/sdk/apps
3
+ * Lightweight client for the App Backend installation.
4
+ */
5
+ interface AppsClientConfig {
6
+ appServerUrl: string;
7
+ apiBasePath: string;
8
+ fetch?: typeof fetch;
9
+ authToken?: string;
10
+ }
11
+ interface AppActionDef {
12
+ action: string;
13
+ validation?: {
14
+ stringValue?: {
15
+ format?: "email";
16
+ };
17
+ };
18
+ write: {
19
+ encrypted?: string;
20
+ plain?: string;
21
+ };
22
+ }
23
+ interface AppRegistration {
24
+ appKey: string;
25
+ accountPrivateKeyPem: string;
26
+ encryptionPublicKeyHex?: string;
27
+ encryptionPrivateKeyPem?: string;
28
+ allowedOrigins: string[];
29
+ actions: AppActionDef[];
30
+ }
31
+ declare class AppsClient {
32
+ private base;
33
+ private api;
34
+ private f;
35
+ private authToken?;
36
+ constructor(cfg: AppsClientConfig);
37
+ setAuthToken(token?: string): void;
38
+ health(): Promise<unknown>;
39
+ registerApp(reg: AppRegistration): Promise<{
40
+ success: boolean;
41
+ error?: string;
42
+ }>;
43
+ updateSchema(appKey: string, actions: AppActionDef[]): Promise<{
44
+ success: boolean;
45
+ error?: string;
46
+ }>;
47
+ getSchema(appKey: string): Promise<{
48
+ success: true;
49
+ config: {
50
+ appKey: string;
51
+ allowedOrigins: string[];
52
+ actions: AppActionDef[];
53
+ };
54
+ }>;
55
+ createSession(appKey: string, token: string): Promise<{
56
+ success: true;
57
+ session: string;
58
+ uri: string;
59
+ }>;
60
+ invokeAction(appKey: string, action: string, payload: string, origin?: string): Promise<{
61
+ success: true;
62
+ uri: string;
63
+ record: {
64
+ ts: number;
65
+ data: unknown;
66
+ };
67
+ }>;
68
+ read(appKey: string, uri: string): Promise<{
69
+ success: true;
70
+ uri: string;
71
+ record: {
72
+ ts: number;
73
+ data: unknown;
74
+ };
75
+ raw?: unknown;
76
+ }>;
77
+ }
78
+
79
+ export { type AppActionDef, type AppRegistration, AppsClient, type AppsClientConfig };
@@ -0,0 +1,7 @@
1
+ import {
2
+ AppsClient
3
+ } from "../chunk-QHDBFVLU.js";
4
+ import "../chunk-MLKGABMK.js";
5
+ export {
6
+ AppsClient
7
+ };
@@ -0,0 +1,277 @@
1
+ // wallet/client.ts
2
+ var WalletClient = class {
3
+ walletServerUrl;
4
+ apiBasePath;
5
+ fetchImpl;
6
+ currentSession = null;
7
+ constructor(config) {
8
+ this.walletServerUrl = config.walletServerUrl.replace(/\/$/, "");
9
+ if (!config.apiBasePath || typeof config.apiBasePath !== "string") {
10
+ throw new Error("apiBasePath is required (e.g., '/api/v1')");
11
+ }
12
+ const normalized = (config.apiBasePath.startsWith("/") ? config.apiBasePath : `/${config.apiBasePath}`).replace(/\/$/, "");
13
+ this.apiBasePath = normalized;
14
+ if (config.fetch) {
15
+ this.fetchImpl = config.fetch;
16
+ } else if (typeof window !== "undefined" && typeof window.fetch === "function") {
17
+ this.fetchImpl = window.fetch.bind(window);
18
+ } else {
19
+ this.fetchImpl = fetch;
20
+ }
21
+ }
22
+ /**
23
+ * Get the current authenticated session
24
+ */
25
+ getSession() {
26
+ return this.currentSession;
27
+ }
28
+ /**
29
+ * Set the current session (useful for restoring from storage)
30
+ */
31
+ setSession(session) {
32
+ this.currentSession = session;
33
+ }
34
+ /**
35
+ * Check if user is currently authenticated
36
+ */
37
+ isAuthenticated() {
38
+ return this.currentSession !== null;
39
+ }
40
+ /**
41
+ * Get current username (if authenticated)
42
+ */
43
+ getUsername() {
44
+ var _a;
45
+ return ((_a = this.currentSession) == null ? void 0 : _a.username) || null;
46
+ }
47
+ /**
48
+ * Get current JWT token (if authenticated)
49
+ */
50
+ getToken() {
51
+ var _a;
52
+ return ((_a = this.currentSession) == null ? void 0 : _a.token) || null;
53
+ }
54
+ /**
55
+ * Clear current session (logout)
56
+ */
57
+ logout() {
58
+ this.currentSession = null;
59
+ }
60
+ /**
61
+ * Check wallet server health
62
+ */
63
+ async health() {
64
+ const response = await this.fetchImpl(`${this.walletServerUrl}${this.apiBasePath}/health`);
65
+ if (!response.ok) {
66
+ throw new Error(`Health check failed: ${response.statusText}`);
67
+ }
68
+ return await response.json();
69
+ }
70
+ /**
71
+ * Sign up a new user
72
+ * Returns session data - call setSession() to activate it
73
+ */
74
+ // Tokenless signup is not supported. Use signup(token,...)
75
+ async signup(_credentials) {
76
+ throw new Error("Use signup(token, credentials) \u2014 app token required");
77
+ }
78
+ /**
79
+ * Login existing user
80
+ * Returns session data - call setSession() to activate it
81
+ */
82
+ // Tokenless login is not supported. Use login(token, session, credentials)
83
+ async login(_credentials) {
84
+ throw new Error("Use login(token, session, credentials) \u2014 app token + session required");
85
+ }
86
+ /**
87
+ * Change password for current user
88
+ * Requires active authentication session
89
+ */
90
+ async changePassword(oldPassword, newPassword) {
91
+ if (!this.currentSession) {
92
+ throw new Error("Not authenticated. Please login first.");
93
+ }
94
+ const response = await this.fetchImpl(
95
+ `${this.walletServerUrl}${this.apiBasePath}/auth/change-password`,
96
+ {
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ Authorization: `Bearer ${this.currentSession.token}`
101
+ },
102
+ body: JSON.stringify({
103
+ oldPassword,
104
+ newPassword
105
+ })
106
+ }
107
+ );
108
+ const data = await response.json();
109
+ if (!response.ok || !data.success) {
110
+ throw new Error(data.error || `Change password failed: ${response.statusText}`);
111
+ }
112
+ }
113
+ /**
114
+ * Request a password reset token
115
+ * Does not require authentication
116
+ */
117
+ async requestPasswordReset(_username) {
118
+ throw new Error("Use requestPasswordResetWithToken(token, username)");
119
+ }
120
+ /**
121
+ * Reset password using a reset token
122
+ * Returns session data - call setSession() to activate it
123
+ */
124
+ async resetPassword(_username, _resetToken, _newPassword) {
125
+ throw new Error("Use resetPasswordWithToken(token, username, resetToken, newPassword)");
126
+ }
127
+ /**
128
+ * Sign up with app token (scoped to an app)
129
+ */
130
+ async signupWithToken(token, credentials) {
131
+ if (!token) throw new Error("token is required");
132
+ const response = await this.fetchImpl(`${this.walletServerUrl}${this.apiBasePath}/auth/signup`, {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({ token, username: credentials.username, password: credentials.password })
136
+ });
137
+ const data = await response.json();
138
+ if (!response.ok || !data.success) {
139
+ throw new Error(data.error || `Signup failed: ${response.statusText}`);
140
+ }
141
+ return { username: data.username, token: data.token, expiresIn: data.expiresIn };
142
+ }
143
+ /**
144
+ * Login with app token and session (scoped to an app)
145
+ */
146
+ async loginWithTokenSession(token, session, credentials) {
147
+ if (!token) throw new Error("token is required");
148
+ if (!session) throw new Error("session is required");
149
+ const response = await this.fetchImpl(`${this.walletServerUrl}${this.apiBasePath}/auth/login`, {
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ token, session, username: credentials.username, password: credentials.password })
153
+ });
154
+ const data = await response.json();
155
+ if (!response.ok || !data.success) {
156
+ throw new Error(data.error || `Login failed: ${response.statusText}`);
157
+ }
158
+ return { username: data.username, token: data.token, expiresIn: data.expiresIn };
159
+ }
160
+ /**
161
+ * Request password reset scoped to app token
162
+ */
163
+ async requestPasswordResetWithToken(token, username) {
164
+ if (!token) throw new Error("token is required");
165
+ const response = await this.fetchImpl(`${this.walletServerUrl}${this.apiBasePath}/auth/request-password-reset`, {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({ token, username })
169
+ });
170
+ const data = await response.json();
171
+ if (!response.ok || !data.success) {
172
+ throw new Error(data.error || `Request password reset failed: ${response.statusText}`);
173
+ }
174
+ return { resetToken: data.resetToken, expiresIn: data.expiresIn };
175
+ }
176
+ /**
177
+ * Reset password scoped to app token
178
+ */
179
+ async resetPasswordWithToken(token, username, resetToken, newPassword) {
180
+ if (!token) throw new Error("token is required");
181
+ const response = await this.fetchImpl(`${this.walletServerUrl}${this.apiBasePath}/auth/reset-password`, {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify({ token, username, resetToken, newPassword })
185
+ });
186
+ const data = await response.json();
187
+ if (!response.ok || !data.success) {
188
+ throw new Error(data.error || `Reset password failed: ${response.statusText}`);
189
+ }
190
+ return { username: data.username, token: data.token, expiresIn: data.expiresIn };
191
+ }
192
+ /**
193
+ * Get public keys for the current authenticated user.
194
+ * Requires an active authentication session.
195
+ */
196
+ async getPublicKeys() {
197
+ if (!this.currentSession) {
198
+ throw new Error("Not authenticated. Please login first.");
199
+ }
200
+ const response = await this.fetchImpl(
201
+ `${this.walletServerUrl}${this.apiBasePath}/public-keys`,
202
+ {
203
+ headers: {
204
+ Authorization: `Bearer ${this.currentSession.token}`
205
+ }
206
+ }
207
+ );
208
+ const data = await response.json();
209
+ if (!response.ok || !data.success) {
210
+ throw new Error(data.error || `Get public keys failed: ${response.statusText}`);
211
+ }
212
+ return {
213
+ accountPublicKeyHex: data.accountPublicKeyHex,
214
+ encryptionPublicKeyHex: data.encryptionPublicKeyHex
215
+ };
216
+ }
217
+ /**
218
+ * Proxy a write request through the wallet server
219
+ * The server signs the write with its identity key
220
+ * Requires active authentication session
221
+ */
222
+ async proxyWrite(request) {
223
+ if (!this.currentSession) {
224
+ throw new Error("Not authenticated. Please login first.");
225
+ }
226
+ const response = await this.fetchImpl(`${this.walletServerUrl}${this.apiBasePath}/proxy/write`, {
227
+ method: "POST",
228
+ headers: {
229
+ "Content-Type": "application/json",
230
+ Authorization: `Bearer ${this.currentSession.token}`
231
+ },
232
+ body: JSON.stringify({
233
+ uri: request.uri,
234
+ data: request.data,
235
+ encrypt: request.encrypt
236
+ })
237
+ });
238
+ const data = await response.json();
239
+ if (!response.ok || !data.success) {
240
+ throw new Error(
241
+ data.error || `Proxy write failed: ${response.statusText}`
242
+ );
243
+ }
244
+ return data;
245
+ }
246
+ /**
247
+ * Convenience method: Get current user's public keys
248
+ * Requires active authentication session
249
+ */
250
+ async getMyPublicKeys() {
251
+ return this.getPublicKeys();
252
+ }
253
+ /**
254
+ * Get server's public keys
255
+ *
256
+ * @returns Server's identity and encryption public keys
257
+ * @throws Error if request fails
258
+ */
259
+ async getServerKeys() {
260
+ const response = await this.fetchImpl(`${this.walletServerUrl}${this.apiBasePath}/server-keys`);
261
+ if (!response.ok) {
262
+ throw new Error(`Failed to get server keys: ${response.statusText}`);
263
+ }
264
+ const data = await response.json();
265
+ if (!data.success) {
266
+ throw new Error(data.error || "Failed to get server keys");
267
+ }
268
+ return {
269
+ identityPublicKeyHex: data.identityPublicKeyHex,
270
+ encryptionPublicKeyHex: data.encryptionPublicKeyHex
271
+ };
272
+ }
273
+ };
274
+
275
+ export {
276
+ WalletClient
277
+ };
@@ -0,0 +1,272 @@
1
+ // clients/websocket/mod.ts
2
+ var WebSocketClient = class {
3
+ config;
4
+ ws = null;
5
+ connected = false;
6
+ reconnectAttempts = 0;
7
+ reconnectTimer = null;
8
+ pendingRequests = /* @__PURE__ */ new Map();
9
+ messageHandler = this.handleMessage.bind(this);
10
+ closeHandler = this.handleClose.bind(this);
11
+ errorHandler = this.handleError.bind(this);
12
+ constructor(config) {
13
+ this.config = {
14
+ timeout: 3e4,
15
+ ...config,
16
+ reconnect: {
17
+ enabled: true,
18
+ maxAttempts: 5,
19
+ interval: 1e3,
20
+ backoff: "exponential",
21
+ ...config.reconnect
22
+ }
23
+ };
24
+ }
25
+ /**
26
+ * Ensure WebSocket connection is established
27
+ */
28
+ async ensureConnected() {
29
+ var _a, _b;
30
+ if (this.connected && ((_a = this.ws) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
31
+ return;
32
+ }
33
+ if (((_b = this.ws) == null ? void 0 : _b.readyState) === WebSocket.CONNECTING) {
34
+ return new Promise((resolve, reject) => {
35
+ const timeout = setTimeout(() => reject(new Error("Connection timeout")), this.config.timeout);
36
+ const checkConnection = () => {
37
+ var _a2, _b2;
38
+ if (this.connected) {
39
+ clearTimeout(timeout);
40
+ resolve();
41
+ } else if (((_a2 = this.ws) == null ? void 0 : _a2.readyState) === WebSocket.CLOSED || ((_b2 = this.ws) == null ? void 0 : _b2.readyState) === WebSocket.CLOSING) {
42
+ clearTimeout(timeout);
43
+ reject(new Error("Connection failed"));
44
+ } else {
45
+ setTimeout(checkConnection, 100);
46
+ }
47
+ };
48
+ checkConnection();
49
+ });
50
+ }
51
+ return this.connect();
52
+ }
53
+ /**
54
+ * Establish WebSocket connection
55
+ */
56
+ async connect() {
57
+ return new Promise((resolve, reject) => {
58
+ try {
59
+ const url = new URL(this.config.url);
60
+ if (this.config.auth) {
61
+ switch (this.config.auth.type) {
62
+ case "bearer":
63
+ url.searchParams.set("token", this.config.auth.token || "");
64
+ break;
65
+ case "basic":
66
+ url.username = this.config.auth.username || "";
67
+ url.password = this.config.auth.password || "";
68
+ break;
69
+ }
70
+ }
71
+ this.ws = new WebSocket(url.toString());
72
+ this.ws.addEventListener("open", () => {
73
+ this.connected = true;
74
+ this.reconnectAttempts = 0;
75
+ resolve();
76
+ });
77
+ this.ws.addEventListener("message", this.messageHandler);
78
+ this.ws.addEventListener("close", this.closeHandler);
79
+ this.ws.addEventListener("error", this.errorHandler);
80
+ const timeout = setTimeout(() => {
81
+ var _a;
82
+ (_a = this.ws) == null ? void 0 : _a.close();
83
+ reject(new Error("Connection timeout"));
84
+ }, this.config.timeout);
85
+ this.ws.addEventListener("open", () => clearTimeout(timeout), { once: true });
86
+ } catch (error) {
87
+ reject(error);
88
+ }
89
+ });
90
+ }
91
+ /**
92
+ * Handle WebSocket messages
93
+ */
94
+ handleMessage(event) {
95
+ try {
96
+ const response = JSON.parse(event.data);
97
+ const pending = this.pendingRequests.get(response.id);
98
+ if (pending) {
99
+ clearTimeout(pending.timeout);
100
+ this.pendingRequests.delete(response.id);
101
+ pending.resolve(response);
102
+ }
103
+ } catch (error) {
104
+ console.error("Failed to parse WebSocket message:", error);
105
+ }
106
+ }
107
+ /**
108
+ * Handle WebSocket close
109
+ */
110
+ handleClose() {
111
+ var _a;
112
+ this.connected = false;
113
+ this.cleanupPendingRequests(new Error("WebSocket connection closed"));
114
+ if (((_a = this.config.reconnect) == null ? void 0 : _a.enabled) && this.reconnectAttempts < (this.config.reconnect.maxAttempts || 5)) {
115
+ this.scheduleReconnect();
116
+ }
117
+ }
118
+ /**
119
+ * Handle WebSocket errors
120
+ */
121
+ handleError(_error) {
122
+ this.connected = false;
123
+ this.cleanupPendingRequests(new Error("WebSocket error"));
124
+ }
125
+ /**
126
+ * Schedule reconnection attempt
127
+ */
128
+ scheduleReconnect() {
129
+ var _a, _b;
130
+ if (this.reconnectTimer) {
131
+ clearTimeout(this.reconnectTimer);
132
+ }
133
+ const delay = ((_a = this.config.reconnect) == null ? void 0 : _a.backoff) === "exponential" ? (this.config.reconnect.interval || 1e3) * Math.pow(2, this.reconnectAttempts) : ((_b = this.config.reconnect) == null ? void 0 : _b.interval) || 1e3;
134
+ this.reconnectTimer = setTimeout(() => {
135
+ this.reconnectAttempts++;
136
+ this.connect().catch(() => {
137
+ });
138
+ }, delay);
139
+ }
140
+ /**
141
+ * Cleanup pending requests with error
142
+ */
143
+ cleanupPendingRequests(error) {
144
+ for (const pending of this.pendingRequests.values()) {
145
+ clearTimeout(pending.timeout);
146
+ pending.reject(error);
147
+ }
148
+ this.pendingRequests.clear();
149
+ }
150
+ /**
151
+ * Send request and wait for response
152
+ */
153
+ async sendRequest(type, payload) {
154
+ await this.ensureConnected();
155
+ return new Promise((resolve, reject) => {
156
+ var _a;
157
+ const id = crypto.randomUUID();
158
+ const request = { id, type, payload };
159
+ const timeout = setTimeout(() => {
160
+ this.pendingRequests.delete(id);
161
+ reject(new Error(`Request timeout after ${this.config.timeout}ms`));
162
+ }, this.config.timeout);
163
+ this.pendingRequests.set(id, {
164
+ resolve: (response) => {
165
+ if (response.success) {
166
+ resolve(response.data);
167
+ } else {
168
+ reject(new Error(response.error || "Request failed"));
169
+ }
170
+ },
171
+ reject,
172
+ timeout
173
+ });
174
+ try {
175
+ (_a = this.ws) == null ? void 0 : _a.send(JSON.stringify(request));
176
+ } catch (error) {
177
+ this.pendingRequests.delete(id);
178
+ clearTimeout(timeout);
179
+ reject(error);
180
+ }
181
+ });
182
+ }
183
+ async write(uri, value) {
184
+ try {
185
+ const result = await this.sendRequest("write", { uri, value });
186
+ return result;
187
+ } catch (error) {
188
+ return {
189
+ success: false,
190
+ error: error instanceof Error ? error.message : String(error)
191
+ };
192
+ }
193
+ }
194
+ async read(uri) {
195
+ try {
196
+ const result = await this.sendRequest("read", { uri });
197
+ return result;
198
+ } catch (error) {
199
+ return {
200
+ success: false,
201
+ error: error instanceof Error ? error.message : String(error)
202
+ };
203
+ }
204
+ }
205
+ async list(uri, options) {
206
+ try {
207
+ const result = await this.sendRequest("list", { uri, options });
208
+ return result;
209
+ } catch (error) {
210
+ return {
211
+ success: true,
212
+ data: [],
213
+ pagination: {
214
+ page: (options == null ? void 0 : options.page) || 1,
215
+ limit: (options == null ? void 0 : options.limit) || 50
216
+ }
217
+ };
218
+ }
219
+ }
220
+ async delete(uri) {
221
+ try {
222
+ const result = await this.sendRequest("delete", { uri });
223
+ return result;
224
+ } catch (error) {
225
+ return {
226
+ success: false,
227
+ error: error instanceof Error ? error.message : String(error)
228
+ };
229
+ }
230
+ }
231
+ async health() {
232
+ try {
233
+ const result = await this.sendRequest("health", {});
234
+ return result;
235
+ } catch (error) {
236
+ return {
237
+ status: "unhealthy",
238
+ message: error instanceof Error ? error.message : String(error)
239
+ };
240
+ }
241
+ }
242
+ async getSchema() {
243
+ try {
244
+ const result = await this.sendRequest("getSchema", {});
245
+ return result;
246
+ } catch (error) {
247
+ return [];
248
+ }
249
+ }
250
+ async cleanup() {
251
+ if (this.reconnectTimer) {
252
+ clearTimeout(this.reconnectTimer);
253
+ this.reconnectTimer = null;
254
+ }
255
+ this.cleanupPendingRequests(new Error("Client cleanup"));
256
+ if (this.ws) {
257
+ this.ws.removeEventListener("message", this.messageHandler);
258
+ this.ws.removeEventListener("close", this.closeHandler);
259
+ this.ws.removeEventListener("error", this.errorHandler);
260
+ if (this.ws.readyState === WebSocket.OPEN) {
261
+ this.ws.close();
262
+ }
263
+ this.ws = null;
264
+ }
265
+ this.connected = false;
266
+ this.reconnectAttempts = 0;
267
+ }
268
+ };
269
+
270
+ export {
271
+ WebSocketClient
272
+ };