@scriptdb/browser-client 1.0.4

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.
Files changed (3) hide show
  1. package/README.md +198 -0
  2. package/dist/index.js +596 -0
  3. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @scriptdb/browser-client
2
+
3
+ Browser-compatible WebSocket client for ScriptDB. Provides the same API as `@scriptdb/client` but uses WebSocket instead of TCP, making it suitable for browser environments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @scriptdb/browser-client
9
+ # or
10
+ bun add @scriptdb/browser-client
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { BrowserClient } from '@scriptdb/browser-client';
17
+
18
+ // Create client with URI (same format as @scriptdb/client)
19
+ const client = new BrowserClient('scriptdb://localhost:1234', {
20
+ username: 'user',
21
+ password: 'pass',
22
+ secure: false, // Use ws:// instead of wss://
23
+ requestTimeout: 30000,
24
+ retries: 3,
25
+ });
26
+
27
+ // Connect and authenticate
28
+ await client.connect();
29
+
30
+ // List databases
31
+ const { databases } = await client.listDatabases();
32
+ console.log('Databases:', databases);
33
+
34
+ // Create a database
35
+ await client.createDatabase('mydb');
36
+
37
+ // Execute code
38
+ const result = await client.run(`
39
+ export const greeting = "Hello, World!";
40
+ `, 'mydb');
41
+ console.log(result);
42
+
43
+ // Close connection
44
+ client.close();
45
+ ```
46
+
47
+ ## Constructor
48
+
49
+ ```typescript
50
+ new BrowserClient(uri: string, options?: ClientOptions)
51
+ ```
52
+
53
+ ### URI Format
54
+
55
+ Supports multiple URI formats:
56
+ - `scriptdb://localhost:1234`
57
+ - `ws://localhost:1235`
58
+ - `http://localhost:1234` (converted to scriptdb://)
59
+
60
+ Credentials can be embedded in the URI:
61
+ ```typescript
62
+ new BrowserClient('scriptdb://user:pass@localhost:1234')
63
+ ```
64
+
65
+ ### Options
66
+
67
+ | Option | Type | Default | Description |
68
+ |--------|------|---------|-------------|
69
+ | `secure` | `boolean` | `true` | Use secure WebSocket (wss://) |
70
+ | `username` | `string` | - | Username for authentication |
71
+ | `password` | `string` | - | Password for authentication |
72
+ | `requestTimeout` | `number` | `120000` | Request timeout in ms (0 = disabled) |
73
+ | `retries` | `number` | `3` | Number of reconnection attempts |
74
+ | `retryDelay` | `number` | `1000` | Initial retry delay in ms |
75
+ | `maxPending` | `number` | `100` | Max concurrent pending requests |
76
+ | `maxQueue` | `number` | `1000` | Max queued requests |
77
+ | `logger` | `Logger` | - | Custom logger with debug/info/warn/error |
78
+ | `tokenRefresh` | `function` | - | Async function to refresh expired tokens |
79
+
80
+ ## API Methods
81
+
82
+ ### Connection
83
+
84
+ ```typescript
85
+ // Connect to server
86
+ await client.connect();
87
+
88
+ // Check connection status
89
+ client.connected; // boolean
90
+
91
+ // Close connection
92
+ client.close();
93
+
94
+ // Disconnect (alias for close)
95
+ await client.disconnect();
96
+
97
+ // Destroy client completely (stops reconnection)
98
+ client.destroy();
99
+ ```
100
+
101
+ ### Database Operations
102
+
103
+ ```typescript
104
+ // List all databases
105
+ const { databases, count } = await client.listDatabases();
106
+
107
+ // Create a database
108
+ await client.createDatabase('mydb');
109
+
110
+ // Remove a database
111
+ await client.removeDatabase('mydb');
112
+
113
+ // Rename a database
114
+ await client.renameDatabase('oldname', 'newname');
115
+
116
+ // Save database to disk
117
+ await client.saveDatabase('mydb');
118
+
119
+ // Update database metadata
120
+ await client.updateDatabase('mydb', { /* metadata */ });
121
+ ```
122
+
123
+ ### Code Execution
124
+
125
+ ```typescript
126
+ // Execute code in a database
127
+ const result = await client.run(`
128
+ export const data = { message: "Hello" };
129
+ console.log("Executed!");
130
+ `, 'mydb');
131
+
132
+ // Result contains:
133
+ // - namespace: exported values
134
+ // - logs: console output
135
+ ```
136
+
137
+ ### Authentication
138
+
139
+ ```typescript
140
+ // Login with credentials
141
+ await client.login('username', 'password');
142
+
143
+ // Logout
144
+ await client.logout();
145
+ ```
146
+
147
+ ### Server Info
148
+
149
+ ```typescript
150
+ const info = await client.getInfo();
151
+ ```
152
+
153
+ ### Low-level Execute
154
+
155
+ ```typescript
156
+ // Execute arbitrary command
157
+ const result = await client.execute({
158
+ action: 'script-code',
159
+ data: { code: '...', databaseName: 'mydb' }
160
+ });
161
+ ```
162
+
163
+ ## Features
164
+
165
+ - **URI Parsing**: Supports scriptdb://, ws://, http:// URIs
166
+ - **Auto-reconnect**: Automatic reconnection with exponential backoff
167
+ - **Token Management**: Automatic token handling and refresh support
168
+ - **Request Queue**: Handles concurrent requests with queue management
169
+ - **Logging**: Configurable logging with automatic credential masking
170
+ - **TypeScript**: Full TypeScript support
171
+
172
+ ## WebSocket Proxy
173
+
174
+ This client connects to a WebSocket proxy server (typically running on TCP port + 1). The proxy handles communication with the ScriptDB TCP server.
175
+
176
+ ```
177
+ Browser -> WebSocket (port 1235) -> Proxy -> TCP (port 1234) -> ScriptDB Server
178
+ ```
179
+
180
+ ## Compatibility with @scriptdb/client
181
+
182
+ The API is designed to be compatible with `@scriptdb/client`:
183
+
184
+ ```typescript
185
+ // Node.js (TCP)
186
+ import { ScriptDBClient } from '@scriptdb/client';
187
+
188
+ // Browser (WebSocket)
189
+ import { ScriptDBClient } from '@scriptdb/browser-client';
190
+
191
+ // Same API
192
+ const client = new ScriptDBClient('scriptdb://localhost:1234', options);
193
+ await client.connect();
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,596 @@
1
+ // src/index.ts
2
+ var noopLogger = {
3
+ debug: () => {},
4
+ info: () => {},
5
+ warn: () => {},
6
+ error: () => {}
7
+ };
8
+ async function createHmacSignature(message, secret, algorithm = "SHA-256") {
9
+ const encoder = new TextEncoder;
10
+ const keyData = encoder.encode(secret);
11
+ const messageData = encoder.encode(message);
12
+ const algoMap = {
13
+ sha256: "SHA-256",
14
+ sha384: "SHA-384",
15
+ sha512: "SHA-512",
16
+ "SHA-256": "SHA-256",
17
+ "SHA-384": "SHA-384",
18
+ "SHA-512": "SHA-512"
19
+ };
20
+ const cryptoAlgo = algoMap[algorithm] || "SHA-256";
21
+ const key = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: cryptoAlgo }, false, ["sign"]);
22
+ const signature = await crypto.subtle.sign("HMAC", key, messageData);
23
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
24
+ }
25
+
26
+ class BrowserClient {
27
+ options;
28
+ logger;
29
+ secure;
30
+ requestTimeout;
31
+ retries;
32
+ retryDelay;
33
+ maxMessageSize;
34
+ uri = "";
35
+ protocolName = "";
36
+ username = null;
37
+ password = null;
38
+ host = "";
39
+ port = 0;
40
+ database = null;
41
+ ws = null;
42
+ _nextId = 1;
43
+ _pending = new Map;
44
+ _maxPending = 100;
45
+ _maxQueue = 1000;
46
+ _pendingQueue = [];
47
+ _connected = false;
48
+ _authenticating = false;
49
+ token = null;
50
+ _currentRetries = 0;
51
+ tokenExpiry = null;
52
+ _destroyed = false;
53
+ _reconnectTimer = null;
54
+ _authPendingId = null;
55
+ signing = null;
56
+ _stringify = JSON.stringify;
57
+ ready = Promise.resolve();
58
+ _resolveReadyFn = null;
59
+ _rejectReadyFn = null;
60
+ _connecting = null;
61
+ constructor(uri, options = {}) {
62
+ if (!uri || typeof uri !== "string")
63
+ throw new Error("uri required");
64
+ this.options = { ...options };
65
+ this.maxMessageSize = Number.isFinite(this.options.maxMessageSize) ? this.options.maxMessageSize : 5 * 1024 * 1024;
66
+ const rawLogger = this.options.logger || noopLogger;
67
+ this.logger = {
68
+ debug: (...args) => rawLogger.debug?.(...this._maskArgs(args)),
69
+ info: (...args) => rawLogger.info?.(...this._maskArgs(args)),
70
+ warn: (...args) => rawLogger.warn?.(...this._maskArgs(args)),
71
+ error: (...args) => rawLogger.error?.(...this._maskArgs(args))
72
+ };
73
+ this.secure = typeof this.options.secure === "boolean" ? this.options.secure : true;
74
+ if (!this.secure) {
75
+ this.logger.warn?.("Warning: connecting in insecure mode (secure=false). This is not recommended.");
76
+ }
77
+ this.requestTimeout = Number.isFinite(this.options.requestTimeout) ? this.options.requestTimeout : 0;
78
+ this.retries = Number.isFinite(this.options.retries) ? this.options.retries : 3;
79
+ this.retryDelay = Number.isFinite(this.options.retryDelay) ? this.options.retryDelay : 1000;
80
+ const normalized = uri.replace(/^https?:\/\//i, "scriptdb://").replace(/^wss?:\/\//i, "scriptdb://");
81
+ let parsed;
82
+ try {
83
+ parsed = new URL(normalized);
84
+ } catch (e) {
85
+ throw new Error("Invalid uri");
86
+ }
87
+ this.uri = normalized;
88
+ this.protocolName = parsed.protocol ? parsed.protocol.replace(":", "") : "scriptdb";
89
+ this.username = (typeof this.options.username === "string" ? this.options.username : decodeURIComponent(parsed.username)) || null;
90
+ this.password = (typeof this.options.password === "string" ? this.options.password : decodeURIComponent(parsed.password)) || null;
91
+ if (parsed.username && typeof this.options.username !== "string") {
92
+ this.logger.warn?.("Credentials found in URI — consider passing credentials via options instead of embedding in URI");
93
+ }
94
+ try {
95
+ parsed.username = "";
96
+ parsed.password = "";
97
+ this.uri = parsed.toString();
98
+ } catch (e) {
99
+ this.uri = normalized;
100
+ }
101
+ this.host = parsed.hostname || "localhost";
102
+ this.port = parsed.port ? parseInt(parsed.port, 10) : 1234;
103
+ this.database = parsed.pathname && parsed.pathname.length > 1 ? parsed.pathname.slice(1) : null;
104
+ this._maxPending = Number.isFinite(this.options.maxPending) ? this.options.maxPending : 100;
105
+ this._maxQueue = Number.isFinite(this.options.maxQueue) ? this.options.maxQueue : 1000;
106
+ this.signing = this.options.signing && this.options.signing.secret ? this.options.signing : null;
107
+ this._stringify = typeof this.options.stringify === "function" ? this.options.stringify : JSON.stringify;
108
+ this._createReady();
109
+ }
110
+ _mask(obj) {
111
+ try {
112
+ if (!obj || typeof obj !== "object")
113
+ return obj;
114
+ const copy = Array.isArray(obj) ? [...obj] : { ...obj };
115
+ if (copy.token)
116
+ copy.token = "****";
117
+ if (copy.password)
118
+ copy.password = "****";
119
+ if (copy.data?.password)
120
+ copy.data.password = "****";
121
+ return copy;
122
+ } catch (e) {
123
+ return obj;
124
+ }
125
+ }
126
+ _maskArgs(args) {
127
+ return args.map((a) => {
128
+ if (!a || typeof a === "string")
129
+ return a;
130
+ return this._mask(a);
131
+ });
132
+ }
133
+ _createReady() {
134
+ this.ready = new Promise((resolve, reject) => {
135
+ this._resolveReadyFn = resolve;
136
+ this._rejectReadyFn = reject;
137
+ });
138
+ }
139
+ _resolveReady(value) {
140
+ if (this._resolveReadyFn) {
141
+ try {
142
+ this._resolveReadyFn(value);
143
+ } catch (e) {}
144
+ this._resolveReadyFn = null;
145
+ this._rejectReadyFn = null;
146
+ }
147
+ }
148
+ _rejectReady(err) {
149
+ if (this._rejectReadyFn) {
150
+ const rejectFn = this._rejectReadyFn;
151
+ this._resolveReadyFn = null;
152
+ this._rejectReadyFn = null;
153
+ setTimeout(() => {
154
+ try {
155
+ rejectFn(err);
156
+ } catch (e) {}
157
+ }, 0);
158
+ }
159
+ }
160
+ get connected() {
161
+ return this._connected;
162
+ }
163
+ connect() {
164
+ if (this._connecting)
165
+ return this._connecting;
166
+ this._connecting = new Promise((resolve, reject) => {
167
+ const wsPort = this.port + 1;
168
+ const wsProtocol = this.secure ? "wss" : "ws";
169
+ const wsUrl = `${wsProtocol}://${this.host}:${wsPort}`;
170
+ this.logger.info?.("Connecting to", wsUrl);
171
+ try {
172
+ this.ws = new WebSocket(wsUrl);
173
+ } catch (e) {
174
+ const error = e;
175
+ this.logger.error?.("Connection failed", error.message);
176
+ this._rejectReady(error);
177
+ this._createReady();
178
+ this._connecting = null;
179
+ return reject(error);
180
+ }
181
+ this.ws.onopen = () => {
182
+ this.logger.info?.("Connected to server");
183
+ this._connected = true;
184
+ this._currentRetries = 0;
185
+ this._setupListeners();
186
+ this.authenticate().then(() => {
187
+ this._processQueue();
188
+ resolve(this);
189
+ }).catch((err) => {
190
+ reject(err);
191
+ });
192
+ };
193
+ this.ws.onerror = (event) => {
194
+ this.logger.error?.("WebSocket error:", event);
195
+ this._handleDisconnect(new Error("WebSocket error"));
196
+ };
197
+ this.ws.onclose = (event) => {
198
+ this.logger.info?.("WebSocket closed", event.code, event.reason);
199
+ this._handleDisconnect(null);
200
+ };
201
+ }).catch((err) => {
202
+ this._connecting = null;
203
+ throw err;
204
+ });
205
+ return this._connecting;
206
+ }
207
+ _setupListeners() {
208
+ if (!this.ws)
209
+ return;
210
+ this.ws.onmessage = (event) => {
211
+ if (typeof event.data === "string" && event.data.length > this.maxMessageSize) {
212
+ this.logger.error?.("Incoming message exceeds maxMessageSize — ignoring");
213
+ return;
214
+ }
215
+ try {
216
+ const msg = JSON.parse(event.data);
217
+ if (!msg || typeof msg !== "object")
218
+ return;
219
+ const validSchema = (typeof msg.id === "undefined" || typeof msg.id === "number") && (typeof msg.action === "string" || typeof msg.action === "undefined") && (typeof msg.command === "string" || typeof msg.command === "undefined") && (typeof msg.message === "string" || typeof msg.message === "undefined") && (typeof msg.data === "object" || typeof msg.data === "undefined" || msg.data === null);
220
+ if (!validSchema) {
221
+ this.logger.warn?.("Message failed schema validation — ignoring");
222
+ return;
223
+ }
224
+ this._handleMessage(msg);
225
+ } catch (error) {
226
+ this.logger.error?.("Failed to parse message:", error);
227
+ }
228
+ };
229
+ }
230
+ _handleMessage(msg) {
231
+ if (!msg || typeof msg !== "object")
232
+ return;
233
+ this.logger.debug?.("Received message:", msg);
234
+ if (typeof msg.id !== "undefined") {
235
+ const pending = this._pending.get(msg.id);
236
+ if (!pending) {
237
+ this.logger.debug?.("No pending request for id", msg.id);
238
+ return;
239
+ }
240
+ const { resolve, reject, timer } = pending;
241
+ if (timer)
242
+ clearTimeout(timer);
243
+ this._pending.delete(msg.id);
244
+ this._processQueue();
245
+ if (msg.action === "login" || msg.command === "login") {
246
+ if (msg.message === "AUTH OK") {
247
+ this.token = msg.data?.token || null;
248
+ this._resolveReady(null);
249
+ return resolve(msg.data);
250
+ } else {
251
+ this._rejectReady(new Error("Authentication failed"));
252
+ const errorMsg = msg.data || "Authentication failed";
253
+ try {
254
+ this.ws?.close();
255
+ } catch (e) {}
256
+ return reject(new Error(typeof errorMsg === "string" ? errorMsg : "Authentication failed"));
257
+ }
258
+ }
259
+ if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "OK") {
260
+ return resolve(msg.data);
261
+ }
262
+ if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "ERROR") {
263
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Server returned ERROR"));
264
+ }
265
+ if (msg.action === "create-db" && msg.message === "SUCCESS") {
266
+ return resolve(msg.data);
267
+ }
268
+ if (msg.action === "create-db" && msg.message === "ERROR") {
269
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Failed to create database"));
270
+ }
271
+ if (msg.message === "OK" || msg.message === "SUCCESS") {
272
+ return resolve(msg.data);
273
+ } else if (msg.message === "ERROR" || msg.message === "AUTH FAIL" || msg.message === "AUTH FAILED") {
274
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Request failed"));
275
+ }
276
+ return reject(new Error("Invalid response from server"));
277
+ }
278
+ this.logger.debug?.("Unhandled message from server", this._mask(msg));
279
+ }
280
+ async _buildFinalMessage(payloadBase, id) {
281
+ const payloadObj = {
282
+ id,
283
+ action: payloadBase.action,
284
+ ...payloadBase.data !== undefined ? { data: payloadBase.data } : {}
285
+ };
286
+ if (this.token)
287
+ payloadObj.token = this.token;
288
+ const payloadStr = this._stringify(payloadObj);
289
+ if (this.signing && this.signing.secret) {
290
+ const sig = await createHmacSignature(payloadStr, this.signing.secret, this.signing.algorithm || "sha256");
291
+ const envelope = { id, signature: sig, payload: payloadObj };
292
+ return this._stringify(envelope);
293
+ }
294
+ return payloadStr;
295
+ }
296
+ authenticate() {
297
+ if (this._authenticating) {
298
+ return Promise.reject(new Error("Already authenticating"));
299
+ }
300
+ this._authenticating = true;
301
+ return new Promise((resolve, reject) => {
302
+ const id = this._nextId++;
303
+ this._authPendingId = id;
304
+ const payload = {
305
+ id,
306
+ action: "login",
307
+ data: { username: this.username, password: this.password }
308
+ };
309
+ let timer = null;
310
+ if (this.requestTimeout > 0) {
311
+ timer = window.setTimeout(() => {
312
+ this._pending.delete(id);
313
+ this._authenticating = false;
314
+ this._authPendingId = null;
315
+ reject(new Error("Auth timeout"));
316
+ this.close();
317
+ }, this.requestTimeout);
318
+ }
319
+ this._pending.set(id, {
320
+ resolve: (data) => {
321
+ if (timer)
322
+ clearTimeout(timer);
323
+ this._authenticating = false;
324
+ this._authPendingId = null;
325
+ if (data?.token)
326
+ this.token = data.token;
327
+ resolve(data);
328
+ },
329
+ reject: (err) => {
330
+ if (timer)
331
+ clearTimeout(timer);
332
+ this._authenticating = false;
333
+ this._authPendingId = null;
334
+ reject(err);
335
+ },
336
+ timer
337
+ });
338
+ try {
339
+ this.ws?.send(JSON.stringify(payload));
340
+ } catch (e) {
341
+ if (timer)
342
+ clearTimeout(timer);
343
+ this._pending.delete(id);
344
+ this._authenticating = false;
345
+ this._authPendingId = null;
346
+ reject(e);
347
+ }
348
+ }).then((data) => {
349
+ if (data?.token) {
350
+ this.token = data.token;
351
+ this._resolveReady(null);
352
+ }
353
+ return data;
354
+ }).catch((err) => {
355
+ this._rejectReady(err);
356
+ throw err;
357
+ });
358
+ }
359
+ async _processQueue() {
360
+ while (this._pending.size < this._maxPending && this._pendingQueue.length > 0) {
361
+ const item = this._pendingQueue.shift();
362
+ const { payloadBase, id, resolve, reject, timer } = item;
363
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
364
+ const refreshed = await this._maybeRefreshToken();
365
+ if (!refreshed) {
366
+ if (timer)
367
+ clearTimeout(timer);
368
+ try {
369
+ reject(new Error("Token expired and refresh failed"));
370
+ } catch (e) {}
371
+ continue;
372
+ }
373
+ }
374
+ let messageStr;
375
+ try {
376
+ messageStr = await this._buildFinalMessage(payloadBase, id);
377
+ } catch (e) {
378
+ if (timer)
379
+ clearTimeout(timer);
380
+ try {
381
+ reject(e);
382
+ } catch (er) {}
383
+ continue;
384
+ }
385
+ this._pending.set(id, { resolve, reject, timer });
386
+ try {
387
+ this.ws?.send(messageStr);
388
+ } catch (e) {
389
+ if (timer)
390
+ clearTimeout(timer);
391
+ this._pending.delete(id);
392
+ try {
393
+ reject(e);
394
+ } catch (er) {}
395
+ }
396
+ }
397
+ }
398
+ async execute(payload) {
399
+ try {
400
+ await this.ready;
401
+ } catch (err) {
402
+ throw new Error("Not authenticated: " + (err?.message || err));
403
+ }
404
+ if (!this.token)
405
+ throw new Error("Not authenticated");
406
+ const id = this._nextId++;
407
+ const payloadBase = { action: payload.action, data: payload.data };
408
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
409
+ const refreshed = await this._maybeRefreshToken();
410
+ if (!refreshed)
411
+ throw new Error("Token expired");
412
+ }
413
+ return new Promise(async (resolve, reject) => {
414
+ let timer = null;
415
+ if (this.requestTimeout > 0) {
416
+ timer = window.setTimeout(() => {
417
+ if (this._pending.has(id)) {
418
+ this._pending.delete(id);
419
+ reject(new Error("Request timeout"));
420
+ }
421
+ }, this.requestTimeout);
422
+ }
423
+ if (this._pending.size >= this._maxPending) {
424
+ if (this._pendingQueue.length >= this._maxQueue) {
425
+ if (timer)
426
+ clearTimeout(timer);
427
+ return reject(new Error("Pending queue full"));
428
+ }
429
+ this._pendingQueue.push({ payloadBase, id, resolve, reject, timer });
430
+ this._processQueue().catch(() => {});
431
+ return;
432
+ }
433
+ let messageStr;
434
+ try {
435
+ messageStr = await this._buildFinalMessage(payloadBase, id);
436
+ } catch (e) {
437
+ if (timer)
438
+ clearTimeout(timer);
439
+ return reject(e);
440
+ }
441
+ this._pending.set(id, { resolve, reject, timer });
442
+ try {
443
+ this.ws?.send(messageStr);
444
+ } catch (e) {
445
+ if (timer)
446
+ clearTimeout(timer);
447
+ this._pending.delete(id);
448
+ reject(e);
449
+ }
450
+ });
451
+ }
452
+ async sendRequest(action, data = {}) {
453
+ return this.execute({ action, data });
454
+ }
455
+ async _maybeRefreshToken() {
456
+ if (!this.options.tokenRefresh || typeof this.options.tokenRefresh !== "function") {
457
+ return false;
458
+ }
459
+ try {
460
+ const res = await this.options.tokenRefresh();
461
+ if (res?.token) {
462
+ this.token = res.token;
463
+ if (res.expiresAt)
464
+ this.tokenExpiry = res.expiresAt;
465
+ return true;
466
+ }
467
+ } catch (e) {
468
+ this.logger.error?.("Token refresh failed", e?.message || String(e));
469
+ }
470
+ return false;
471
+ }
472
+ _handleDisconnect(err) {
473
+ if (!this._connected && !this._authenticating)
474
+ return;
475
+ const wasAuthenticating = this._authenticating;
476
+ this._pending.forEach((pending, id) => {
477
+ if (pending.timer)
478
+ clearTimeout(pending.timer);
479
+ const errorMsg = wasAuthenticating ? "Authentication failed - credentials may be required" : err?.message || "Disconnected";
480
+ setTimeout(() => {
481
+ try {
482
+ pending.reject(new Error(errorMsg));
483
+ } catch (e) {}
484
+ }, 0);
485
+ this._pending.delete(id);
486
+ });
487
+ this._connected = false;
488
+ this._connecting = null;
489
+ this._authenticating = false;
490
+ if (wasAuthenticating && this.retries === 0) {
491
+ const rejectErr = err || new Error("Authentication failed and no retries configured");
492
+ try {
493
+ this._rejectReady(rejectErr);
494
+ } catch (e) {}
495
+ return;
496
+ }
497
+ if (this._currentRetries < this.retries && !this._destroyed) {
498
+ this._createReady();
499
+ const base = Math.min(this.retryDelay * Math.pow(2, this._currentRetries), 30000);
500
+ const jitter = Math.floor(Math.random() * Math.min(1000, base));
501
+ const delay = base + jitter;
502
+ this._currentRetries += 1;
503
+ this.logger.info?.(`Attempting reconnect in ${delay}ms (attempt ${this._currentRetries})`);
504
+ this.token = null;
505
+ this._reconnectTimer = window.setTimeout(() => {
506
+ this.connect().catch((e) => {
507
+ this.logger.error?.("Reconnect failed", e?.message || e);
508
+ });
509
+ }, delay);
510
+ } else {
511
+ try {
512
+ this._rejectReady(err || new Error("Disconnected and no retries left"));
513
+ } catch (e) {}
514
+ }
515
+ }
516
+ close() {
517
+ if (this.ws) {
518
+ try {
519
+ this.ws.close();
520
+ } catch (e) {}
521
+ this.ws = null;
522
+ }
523
+ this._connected = false;
524
+ this._cleanup();
525
+ }
526
+ destroy() {
527
+ this._destroyed = true;
528
+ if (this._reconnectTimer) {
529
+ clearTimeout(this._reconnectTimer);
530
+ this._reconnectTimer = null;
531
+ }
532
+ this._rejectReady(new Error("Client destroyed"));
533
+ this._pending.forEach((pending, id) => {
534
+ if (pending.timer)
535
+ clearTimeout(pending.timer);
536
+ try {
537
+ pending.reject(new Error("Client destroyed"));
538
+ } catch (e) {}
539
+ this._pending.delete(id);
540
+ });
541
+ this._pendingQueue = [];
542
+ this.close();
543
+ }
544
+ _cleanup() {
545
+ this._pending.forEach((pending) => {
546
+ if (pending.timer)
547
+ clearTimeout(pending.timer);
548
+ try {
549
+ pending.reject(new Error("Disconnected"));
550
+ } catch (e) {}
551
+ });
552
+ this._pending.clear();
553
+ }
554
+ async disconnect() {
555
+ this.close();
556
+ }
557
+ async login(username, password) {
558
+ return this.sendRequest("login", { username, password });
559
+ }
560
+ async logout() {
561
+ return this.sendRequest("logout", {});
562
+ }
563
+ async listDatabases() {
564
+ return this.sendRequest("list-dbs", {});
565
+ }
566
+ async createDatabase(name) {
567
+ return this.sendRequest("create-db", { databaseName: name });
568
+ }
569
+ async removeDatabase(name) {
570
+ return this.sendRequest("remove-db", { databaseName: name });
571
+ }
572
+ async renameDatabase(oldName, newName) {
573
+ return this.sendRequest("rename-db", { databaseName: oldName, newName });
574
+ }
575
+ async run(code, databaseName) {
576
+ return this.sendRequest("script-code", { code, databaseName });
577
+ }
578
+ async saveDatabase(databaseName) {
579
+ return this.sendRequest("save-db", { databaseName });
580
+ }
581
+ async updateDatabase(databaseName, data) {
582
+ return this.sendRequest("update-db", { databaseName, ...data });
583
+ }
584
+ async getInfo() {
585
+ return this.sendRequest("get-info", {});
586
+ }
587
+ async executeShell(command) {
588
+ return this.sendRequest("shell-command", { command });
589
+ }
590
+ }
591
+ var src_default = BrowserClient;
592
+ export {
593
+ src_default as default,
594
+ BrowserClient as ScriptDBClient,
595
+ BrowserClient
596
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@scriptdb/browser-client",
3
+ "version": "1.0.4",
4
+ "description": "Client module resolver for script database",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "dev": "bun --watch src/index.ts",
20
+ "build": "bun build src/index.ts --outdir dist --target node --format esm --splitting --external bcryptjs --external bottleneck --external jsonwebtoken --external lru-cache",
21
+ "build:cjs": "bun build src/index.ts --outdir dist --target node --format cjs --outfile dist/index.js --external bcryptjs --external bottleneck --external jsonwebtoken --external lru-cache",
22
+ "build:types": "tsc --emitDeclarationOnly",
23
+ "build:all": "bun run build && bun run build:cjs && bun run build:types",
24
+ "test": "bun test",
25
+ "lint": "bun run lint:src",
26
+ "lint:src": "eslint src --ext .ts,.tsx",
27
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
28
+ "typecheck": "tsc --noEmit",
29
+ "clean": "rm -rf dist"
30
+ },
31
+ "devDependencies": {
32
+ "@types/bcryptjs": "^3.0.0",
33
+ "@types/bun": "^1.3.2",
34
+ "@types/jsonwebtoken": "^9.0.10",
35
+ "@types/lru-cache": "^7.10.10",
36
+ "@types/node": "^24.10.1",
37
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
38
+ "@typescript-eslint/parser": "^6.0.0",
39
+ "bun-types": "latest",
40
+ "eslint": "^8.0.0",
41
+ "typescript": "^5.0.0"
42
+ },
43
+ "dependencies": {
44
+ "bcryptjs": "^3.0.3",
45
+ "bottleneck": "^2.19.5",
46
+ "jsonwebtoken": "^9.0.2",
47
+ "lru-cache": "^11.2.2"
48
+ }
49
+ }