@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 +21 -0
- package/README.md +28 -0
- package/dist/apps/mod.d.ts +79 -0
- package/dist/apps/mod.js +7 -0
- package/dist/chunk-2D2RT2DW.js +277 -0
- package/dist/chunk-C2ZIFM22.js +272 -0
- package/dist/chunk-G6JDROB4.js +327 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-PMBS2GFA.js +223 -0
- package/dist/chunk-QHDBFVLU.js +87 -0
- package/dist/chunk-XH4OLKBV.js +295 -0
- package/dist/clients/http/mod.d.ts +33 -0
- package/dist/clients/http/mod.js +7 -0
- package/dist/clients/local-storage/mod.d.ts +60 -0
- package/dist/clients/local-storage/mod.js +7 -0
- package/dist/clients/websocket/mod.d.ts +62 -0
- package/dist/clients/websocket/mod.js +7 -0
- package/dist/encrypt/mod.d.ts +1 -0
- package/dist/encrypt/mod.js +31 -0
- package/dist/mod-DHjjiF1o.d.ts +111 -0
- package/dist/src/mod.web.d.ts +7 -0
- package/dist/src/mod.web.js +27 -0
- package/dist/types-Bw0Boe0n.d.ts +219 -0
- package/dist/wallet/mod.d.ts +278 -0
- package/dist/wallet/mod.js +7 -0
- package/package.json +84 -0
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 };
|
package/dist/apps/mod.js
ADDED
|
@@ -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
|
+
};
|