@scriptdb/browser-client 1.0.7 → 1.0.9
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/README.md +48 -0
- package/dist/index.js +70 -458
- package/package.json +7 -25
package/README.md
CHANGED
|
@@ -193,6 +193,54 @@ const client = new ScriptDBClient('scriptdb://localhost:1234', options);
|
|
|
193
193
|
await client.connect();
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
## CLI Tool
|
|
197
|
+
|
|
198
|
+
ScriptDB also provides a CLI tool for server management:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Install CLI globally
|
|
202
|
+
npm install -g @scriptdb/cli
|
|
203
|
+
|
|
204
|
+
# Start server
|
|
205
|
+
scriptdb start
|
|
206
|
+
|
|
207
|
+
# Start server in background (daemon mode with PM2)
|
|
208
|
+
scriptdb start -d
|
|
209
|
+
|
|
210
|
+
# Check server status
|
|
211
|
+
scriptdb status
|
|
212
|
+
|
|
213
|
+
# View real-time logs
|
|
214
|
+
scriptdb logs
|
|
215
|
+
|
|
216
|
+
# Monitor performance
|
|
217
|
+
scriptdb monit
|
|
218
|
+
|
|
219
|
+
# Stop server
|
|
220
|
+
scriptdb stop
|
|
221
|
+
|
|
222
|
+
# Restart server
|
|
223
|
+
scriptdb restart -d
|
|
224
|
+
|
|
225
|
+
# Start interactive shell
|
|
226
|
+
scriptdb shell
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Changelog
|
|
230
|
+
|
|
231
|
+
### 1.0.9 (2025-01-16)
|
|
232
|
+
|
|
233
|
+
**Added**
|
|
234
|
+
- Native `scriptdb logs` command to view real-time logs
|
|
235
|
+
- Native `scriptdb monit` command to monitor performance
|
|
236
|
+
- Native `scriptdb restart` command to restart the server
|
|
237
|
+
- Native `scriptdb stop` command to stop the server
|
|
238
|
+
- ESLint configuration for TypeScript linting
|
|
239
|
+
|
|
240
|
+
**Fixed**
|
|
241
|
+
- Fixed TypeScript module resolution errors
|
|
242
|
+
- Improved error handling and Windows compatibility
|
|
243
|
+
|
|
196
244
|
## License
|
|
197
245
|
|
|
198
246
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -5,417 +5,131 @@ var noopLogger = {
|
|
|
5
5
|
warn: () => {},
|
|
6
6
|
error: () => {}
|
|
7
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
8
|
|
|
26
9
|
class BrowserClient {
|
|
27
|
-
options;
|
|
28
10
|
logger;
|
|
29
|
-
|
|
11
|
+
host;
|
|
12
|
+
port;
|
|
13
|
+
username;
|
|
14
|
+
password;
|
|
30
15
|
requestTimeout;
|
|
31
|
-
|
|
32
|
-
retryDelay;
|
|
33
|
-
maxMessageSize;
|
|
34
|
-
uri = "";
|
|
35
|
-
protocolName = "";
|
|
36
|
-
username = null;
|
|
37
|
-
password = null;
|
|
38
|
-
host = "";
|
|
39
|
-
port = 0;
|
|
40
|
-
database = null;
|
|
16
|
+
secure;
|
|
41
17
|
ws = null;
|
|
18
|
+
_connected = false;
|
|
42
19
|
_nextId = 1;
|
|
43
20
|
_pending = new Map;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
let parsed;
|
|
81
|
-
try {
|
|
82
|
-
parsed = new URL(uri);
|
|
83
|
-
} catch (e) {
|
|
84
|
-
throw new Error("Invalid uri");
|
|
85
|
-
}
|
|
86
|
-
if (parsed.protocol !== "scriptdb:") {
|
|
87
|
-
throw new Error("URI must use scriptdb:// protocol");
|
|
88
|
-
}
|
|
89
|
-
if (parsed.protocol !== "scriptdb:") {
|
|
90
|
-
throw new Error("URI must use scriptdb:// protocol");
|
|
91
|
-
}
|
|
92
|
-
this.uri = uri;
|
|
93
|
-
this.protocolName = parsed.protocol ? parsed.protocol.replace(":", "") : "scriptdb";
|
|
94
|
-
this.username = (typeof this.options.username === "string" ? this.options.username : decodeURIComponent(parsed.username)) || null;
|
|
95
|
-
this.password = (typeof this.options.password === "string" ? this.options.password : decodeURIComponent(parsed.password)) || null;
|
|
96
|
-
if (parsed.username && typeof this.options.username !== "string") {
|
|
97
|
-
this.logger.warn?.("Credentials found in URI — consider passing credentials via options instead of embedding in URI");
|
|
98
|
-
}
|
|
99
|
-
try {
|
|
100
|
-
parsed.username = "";
|
|
101
|
-
parsed.password = "";
|
|
102
|
-
this.uri = parsed.toString();
|
|
103
|
-
} catch (e) {
|
|
104
|
-
this.uri = uri;
|
|
105
|
-
}
|
|
106
|
-
this.host = parsed.hostname || "localhost";
|
|
107
|
-
this.port = parsed.port ? parseInt(parsed.port, 10) : 1234;
|
|
108
|
-
this.database = parsed.pathname && parsed.pathname.length > 1 ? parsed.pathname.slice(1) : null;
|
|
109
|
-
this._maxPending = Number.isFinite(this.options.maxPending) ? this.options.maxPending : 100;
|
|
110
|
-
this._maxQueue = Number.isFinite(this.options.maxQueue) ? this.options.maxQueue : 1000;
|
|
111
|
-
this.signing = this.options.signing && this.options.signing.secret ? this.options.signing : null;
|
|
112
|
-
this._stringify = typeof this.options.stringify === "function" ? this.options.stringify : JSON.stringify;
|
|
113
|
-
this._createReady();
|
|
114
|
-
}
|
|
115
|
-
_mask(obj) {
|
|
116
|
-
try {
|
|
117
|
-
if (!obj || typeof obj !== "object")
|
|
118
|
-
return obj;
|
|
119
|
-
const copy = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
120
|
-
if (copy.token)
|
|
121
|
-
copy.token = "****";
|
|
122
|
-
if (copy.password)
|
|
123
|
-
copy.password = "****";
|
|
124
|
-
if (copy.data?.password)
|
|
125
|
-
copy.data.password = "****";
|
|
126
|
-
return copy;
|
|
127
|
-
} catch (e) {
|
|
128
|
-
return obj;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
_maskArgs(args) {
|
|
132
|
-
return args.map((a) => {
|
|
133
|
-
if (!a || typeof a === "string")
|
|
134
|
-
return a;
|
|
135
|
-
return this._mask(a);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
_createReady() {
|
|
139
|
-
this.ready = new Promise((resolve, reject) => {
|
|
140
|
-
this._resolveReadyFn = resolve;
|
|
141
|
-
this._rejectReadyFn = reject;
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
_resolveReady(value) {
|
|
145
|
-
if (this._resolveReadyFn) {
|
|
146
|
-
try {
|
|
147
|
-
this._resolveReadyFn(value);
|
|
148
|
-
} catch (e) {}
|
|
149
|
-
this._resolveReadyFn = null;
|
|
150
|
-
this._rejectReadyFn = null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
_rejectReady(err) {
|
|
154
|
-
if (this._rejectReadyFn) {
|
|
155
|
-
const rejectFn = this._rejectReadyFn;
|
|
156
|
-
this._resolveReadyFn = null;
|
|
157
|
-
this._rejectReadyFn = null;
|
|
158
|
-
setTimeout(() => {
|
|
159
|
-
try {
|
|
160
|
-
rejectFn(err);
|
|
161
|
-
} catch (e) {}
|
|
162
|
-
}, 0);
|
|
163
|
-
}
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.logger = options.logger || noopLogger;
|
|
23
|
+
this.host = options.host || "localhost";
|
|
24
|
+
this.port = options.port || 1234;
|
|
25
|
+
this.username = options.username || null;
|
|
26
|
+
this.password = options.password || null;
|
|
27
|
+
this.requestTimeout = Number.isFinite(options.requestTimeout) ? options.requestTimeout : 120000;
|
|
28
|
+
this.secure = options.secure !== undefined ? options.secure : false;
|
|
164
29
|
}
|
|
165
30
|
get connected() {
|
|
166
31
|
return this._connected;
|
|
167
32
|
}
|
|
168
|
-
connect() {
|
|
169
|
-
|
|
170
|
-
return this._connecting;
|
|
171
|
-
this._connecting = new Promise((resolve, reject) => {
|
|
33
|
+
async connect() {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
172
35
|
const wsPort = this.port + 1;
|
|
173
|
-
const
|
|
174
|
-
const wsUrl = `${wsProtocol}://${this.host}:${wsPort}`;
|
|
36
|
+
const wsUrl = `ws://${this.host}:${wsPort}`;
|
|
175
37
|
this.logger.info?.("Connecting to", wsUrl);
|
|
176
38
|
try {
|
|
177
39
|
this.ws = new WebSocket(wsUrl);
|
|
178
40
|
} catch (e) {
|
|
179
41
|
const error = e;
|
|
180
42
|
this.logger.error?.("Connection failed", error.message);
|
|
181
|
-
this._rejectReady(error);
|
|
182
|
-
this._createReady();
|
|
183
|
-
this._connecting = null;
|
|
184
43
|
return reject(error);
|
|
185
44
|
}
|
|
186
|
-
this.ws.onopen = () => {
|
|
187
|
-
|
|
45
|
+
this.ws.onopen = async () => {
|
|
46
|
+
console.log("WebSocket onopen - browserClient.ts");
|
|
47
|
+
this.logger.info?.("WebSocket connected");
|
|
188
48
|
this._connected = true;
|
|
189
|
-
this._currentRetries = 0;
|
|
190
49
|
this._setupListeners();
|
|
191
|
-
|
|
192
|
-
this.
|
|
50
|
+
try {
|
|
51
|
+
console.log("Sending login request with credentials:", { username: this.username || "", hasPassword: !!this.password, secure: this.secure });
|
|
52
|
+
await this.sendRequest("login", {
|
|
53
|
+
username: this.username || "",
|
|
54
|
+
password: this.password || "",
|
|
55
|
+
secure: this.secure
|
|
56
|
+
});
|
|
57
|
+
console.log("Authenticated successfully");
|
|
58
|
+
this.logger.info?.("Authenticated successfully");
|
|
193
59
|
resolve(this);
|
|
194
|
-
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("Login failed:", err);
|
|
195
62
|
reject(err);
|
|
196
|
-
}
|
|
63
|
+
}
|
|
197
64
|
};
|
|
198
|
-
this.ws.onerror = (
|
|
199
|
-
|
|
200
|
-
this.
|
|
65
|
+
this.ws.onerror = (error) => {
|
|
66
|
+
console.error("WebSocket onerror:", error);
|
|
67
|
+
this.logger.error?.("WebSocket error:", error);
|
|
68
|
+
reject(new Error("WebSocket connection failed"));
|
|
201
69
|
};
|
|
202
|
-
this.ws.onclose = (
|
|
203
|
-
|
|
204
|
-
this.
|
|
70
|
+
this.ws.onclose = () => {
|
|
71
|
+
console.log("WebSocket onclose");
|
|
72
|
+
this.logger.info?.("WebSocket disconnected");
|
|
73
|
+
this._connected = false;
|
|
74
|
+
this._cleanup();
|
|
205
75
|
};
|
|
206
|
-
}).catch((err) => {
|
|
207
|
-
this._connecting = null;
|
|
208
|
-
throw err;
|
|
209
76
|
});
|
|
210
|
-
return this._connecting;
|
|
211
77
|
}
|
|
212
78
|
_setupListeners() {
|
|
213
79
|
if (!this.ws)
|
|
214
80
|
return;
|
|
215
81
|
this.ws.onmessage = (event) => {
|
|
216
|
-
|
|
217
|
-
this.logger.error?.("Incoming message exceeds maxMessageSize — ignoring");
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
82
|
+
console.log("WebSocket onmessage fired - browserClient.ts", event.data);
|
|
220
83
|
try {
|
|
221
84
|
const msg = JSON.parse(event.data);
|
|
222
|
-
|
|
223
|
-
return;
|
|
224
|
-
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);
|
|
225
|
-
if (!validSchema) {
|
|
226
|
-
this.logger.warn?.("Message failed schema validation — ignoring");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
85
|
+
console.log("Parsed message:", msg);
|
|
229
86
|
this._handleMessage(msg);
|
|
230
87
|
} catch (error) {
|
|
88
|
+
console.error("Failed to parse message:", error, event.data);
|
|
231
89
|
this.logger.error?.("Failed to parse message:", error);
|
|
232
90
|
}
|
|
233
91
|
};
|
|
234
92
|
}
|
|
235
93
|
_handleMessage(msg) {
|
|
236
|
-
|
|
94
|
+
console.log("_handleMessage called with:", msg);
|
|
95
|
+
if (!msg || typeof msg !== "object") {
|
|
96
|
+
console.log("Invalid message - not an object");
|
|
237
97
|
return;
|
|
98
|
+
}
|
|
238
99
|
this.logger.debug?.("Received message:", msg);
|
|
239
100
|
if (typeof msg.id !== "undefined") {
|
|
101
|
+
console.log("Message has id:", msg.id);
|
|
240
102
|
const pending = this._pending.get(msg.id);
|
|
103
|
+
console.log("Pending request found:", !!pending, "total pending:", this._pending.size);
|
|
241
104
|
if (!pending) {
|
|
242
105
|
this.logger.debug?.("No pending request for id", msg.id);
|
|
106
|
+
console.log("No pending request for id", msg.id);
|
|
243
107
|
return;
|
|
244
108
|
}
|
|
245
109
|
const { resolve, reject, timer } = pending;
|
|
246
110
|
if (timer)
|
|
247
111
|
clearTimeout(timer);
|
|
248
112
|
this._pending.delete(msg.id);
|
|
249
|
-
|
|
250
|
-
if (msg.
|
|
251
|
-
if (msg.message === "AUTH OK") {
|
|
252
|
-
this.token = msg.data?.token || null;
|
|
253
|
-
this._resolveReady(null);
|
|
254
|
-
return resolve(msg.data);
|
|
255
|
-
} else {
|
|
256
|
-
this._rejectReady(new Error("Authentication failed"));
|
|
257
|
-
const errorMsg = msg.data || "Authentication failed";
|
|
258
|
-
try {
|
|
259
|
-
this.ws?.close();
|
|
260
|
-
} catch (e) {}
|
|
261
|
-
return reject(new Error(typeof errorMsg === "string" ? errorMsg : "Authentication failed"));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "OK") {
|
|
265
|
-
return resolve(msg.data);
|
|
266
|
-
}
|
|
267
|
-
if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "ERROR") {
|
|
268
|
-
return reject(new Error(typeof msg.data === "string" ? msg.data : "Server returned ERROR"));
|
|
269
|
-
}
|
|
270
|
-
if (msg.action === "create-db" && msg.message === "SUCCESS") {
|
|
271
|
-
return resolve(msg.data);
|
|
272
|
-
}
|
|
273
|
-
if (msg.action === "create-db" && msg.message === "ERROR") {
|
|
274
|
-
return reject(new Error(typeof msg.data === "string" ? msg.data : "Failed to create database"));
|
|
275
|
-
}
|
|
276
|
-
if (msg.message === "OK" || msg.message === "SUCCESS") {
|
|
277
|
-
return resolve(msg.data);
|
|
278
|
-
} else if (msg.message === "ERROR" || msg.message === "AUTH FAIL" || msg.message === "AUTH FAILED") {
|
|
113
|
+
console.log("Resolving request with message:", msg.message, "data:", msg.data);
|
|
114
|
+
if (msg.message === "ERROR" || msg.message === "AUTH FAILED" || msg.message === "AUTH FAIL") {
|
|
279
115
|
return reject(new Error(typeof msg.data === "string" ? msg.data : "Request failed"));
|
|
280
116
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
async _buildFinalMessage(payloadBase, id) {
|
|
286
|
-
const payloadObj = {
|
|
287
|
-
id,
|
|
288
|
-
action: payloadBase.action,
|
|
289
|
-
...payloadBase.data !== undefined ? { data: payloadBase.data } : {}
|
|
290
|
-
};
|
|
291
|
-
if (this.token)
|
|
292
|
-
payloadObj.token = this.token;
|
|
293
|
-
const payloadStr = this._stringify(payloadObj);
|
|
294
|
-
if (this.signing && this.signing.secret) {
|
|
295
|
-
const sig = await createHmacSignature(payloadStr, this.signing.secret, this.signing.algorithm || "sha256");
|
|
296
|
-
const envelope = { id, signature: sig, payload: payloadObj };
|
|
297
|
-
return this._stringify(envelope);
|
|
298
|
-
}
|
|
299
|
-
return payloadStr;
|
|
300
|
-
}
|
|
301
|
-
authenticate() {
|
|
302
|
-
if (this._authenticating) {
|
|
303
|
-
return Promise.reject(new Error("Already authenticating"));
|
|
304
|
-
}
|
|
305
|
-
this._authenticating = true;
|
|
306
|
-
return new Promise((resolve, reject) => {
|
|
307
|
-
const id = this._nextId++;
|
|
308
|
-
this._authPendingId = id;
|
|
309
|
-
const payload = {
|
|
310
|
-
id,
|
|
311
|
-
action: "login",
|
|
312
|
-
data: { username: this.username, password: this.password }
|
|
313
|
-
};
|
|
314
|
-
let timer = null;
|
|
315
|
-
if (this.requestTimeout > 0) {
|
|
316
|
-
timer = window.setTimeout(() => {
|
|
317
|
-
this._pending.delete(id);
|
|
318
|
-
this._authenticating = false;
|
|
319
|
-
this._authPendingId = null;
|
|
320
|
-
reject(new Error("Auth timeout"));
|
|
321
|
-
this.close();
|
|
322
|
-
}, this.requestTimeout);
|
|
323
|
-
}
|
|
324
|
-
this._pending.set(id, {
|
|
325
|
-
resolve: (data) => {
|
|
326
|
-
if (timer)
|
|
327
|
-
clearTimeout(timer);
|
|
328
|
-
this._authenticating = false;
|
|
329
|
-
this._authPendingId = null;
|
|
330
|
-
if (data?.token)
|
|
331
|
-
this.token = data.token;
|
|
332
|
-
resolve(data);
|
|
333
|
-
},
|
|
334
|
-
reject: (err) => {
|
|
335
|
-
if (timer)
|
|
336
|
-
clearTimeout(timer);
|
|
337
|
-
this._authenticating = false;
|
|
338
|
-
this._authPendingId = null;
|
|
339
|
-
reject(err);
|
|
340
|
-
},
|
|
341
|
-
timer
|
|
342
|
-
});
|
|
343
|
-
try {
|
|
344
|
-
this.ws?.send(JSON.stringify(payload));
|
|
345
|
-
} catch (e) {
|
|
346
|
-
if (timer)
|
|
347
|
-
clearTimeout(timer);
|
|
348
|
-
this._pending.delete(id);
|
|
349
|
-
this._authenticating = false;
|
|
350
|
-
this._authPendingId = null;
|
|
351
|
-
reject(e);
|
|
352
|
-
}
|
|
353
|
-
}).then((data) => {
|
|
354
|
-
if (data?.token) {
|
|
355
|
-
this.token = data.token;
|
|
356
|
-
this._resolveReady(null);
|
|
357
|
-
}
|
|
358
|
-
return data;
|
|
359
|
-
}).catch((err) => {
|
|
360
|
-
this._rejectReady(err);
|
|
361
|
-
throw err;
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
async _processQueue() {
|
|
365
|
-
while (this._pending.size < this._maxPending && this._pendingQueue.length > 0) {
|
|
366
|
-
const item = this._pendingQueue.shift();
|
|
367
|
-
const { payloadBase, id, resolve, reject, timer } = item;
|
|
368
|
-
if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
|
|
369
|
-
const refreshed = await this._maybeRefreshToken();
|
|
370
|
-
if (!refreshed) {
|
|
371
|
-
if (timer)
|
|
372
|
-
clearTimeout(timer);
|
|
373
|
-
try {
|
|
374
|
-
reject(new Error("Token expired and refresh failed"));
|
|
375
|
-
} catch (e) {}
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
let messageStr;
|
|
380
|
-
try {
|
|
381
|
-
messageStr = await this._buildFinalMessage(payloadBase, id);
|
|
382
|
-
} catch (e) {
|
|
383
|
-
if (timer)
|
|
384
|
-
clearTimeout(timer);
|
|
385
|
-
try {
|
|
386
|
-
reject(e);
|
|
387
|
-
} catch (er) {}
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
this._pending.set(id, { resolve, reject, timer });
|
|
391
|
-
try {
|
|
392
|
-
this.ws?.send(messageStr);
|
|
393
|
-
} catch (e) {
|
|
394
|
-
if (timer)
|
|
395
|
-
clearTimeout(timer);
|
|
396
|
-
this._pending.delete(id);
|
|
397
|
-
try {
|
|
398
|
-
reject(e);
|
|
399
|
-
} catch (er) {}
|
|
117
|
+
if (msg.message === "AUTH OK" || msg.message === "OK" || msg.message === "SUCCESS") {
|
|
118
|
+
console.log("Calling resolve with data:", msg.data);
|
|
119
|
+
return resolve(msg.data);
|
|
400
120
|
}
|
|
121
|
+
console.log("Default resolve with data:", msg.data);
|
|
122
|
+
return resolve(msg.data);
|
|
401
123
|
}
|
|
124
|
+
console.log("Message has no id, ignoring");
|
|
402
125
|
}
|
|
403
|
-
async
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
} catch (err) {
|
|
407
|
-
throw new Error("Not authenticated: " + (err?.message || err));
|
|
126
|
+
async sendRequest(action, data = {}) {
|
|
127
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
128
|
+
throw new Error("Not connected");
|
|
408
129
|
}
|
|
409
|
-
if (!this.token)
|
|
410
|
-
throw new Error("Not authenticated");
|
|
411
130
|
const id = this._nextId++;
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
const refreshed = await this._maybeRefreshToken();
|
|
415
|
-
if (!refreshed)
|
|
416
|
-
throw new Error("Token expired");
|
|
417
|
-
}
|
|
418
|
-
return new Promise(async (resolve, reject) => {
|
|
131
|
+
const request = { id, action, data };
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
419
133
|
let timer = null;
|
|
420
134
|
if (this.requestTimeout > 0) {
|
|
421
135
|
timer = window.setTimeout(() => {
|
|
@@ -425,27 +139,9 @@ class BrowserClient {
|
|
|
425
139
|
}
|
|
426
140
|
}, this.requestTimeout);
|
|
427
141
|
}
|
|
428
|
-
if (this._pending.size >= this._maxPending) {
|
|
429
|
-
if (this._pendingQueue.length >= this._maxQueue) {
|
|
430
|
-
if (timer)
|
|
431
|
-
clearTimeout(timer);
|
|
432
|
-
return reject(new Error("Pending queue full"));
|
|
433
|
-
}
|
|
434
|
-
this._pendingQueue.push({ payloadBase, id, resolve, reject, timer });
|
|
435
|
-
this._processQueue().catch(() => {});
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
let messageStr;
|
|
439
|
-
try {
|
|
440
|
-
messageStr = await this._buildFinalMessage(payloadBase, id);
|
|
441
|
-
} catch (e) {
|
|
442
|
-
if (timer)
|
|
443
|
-
clearTimeout(timer);
|
|
444
|
-
return reject(e);
|
|
445
|
-
}
|
|
446
142
|
this._pending.set(id, { resolve, reject, timer });
|
|
447
143
|
try {
|
|
448
|
-
this.ws?.send(
|
|
144
|
+
this.ws?.send(JSON.stringify(request));
|
|
449
145
|
} catch (e) {
|
|
450
146
|
if (timer)
|
|
451
147
|
clearTimeout(timer);
|
|
@@ -454,69 +150,15 @@ class BrowserClient {
|
|
|
454
150
|
}
|
|
455
151
|
});
|
|
456
152
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
async _maybeRefreshToken() {
|
|
461
|
-
if (!this.options.tokenRefresh || typeof this.options.tokenRefresh !== "function") {
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
try {
|
|
465
|
-
const res = await this.options.tokenRefresh();
|
|
466
|
-
if (res?.token) {
|
|
467
|
-
this.token = res.token;
|
|
468
|
-
if (res.expiresAt)
|
|
469
|
-
this.tokenExpiry = res.expiresAt;
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
} catch (e) {
|
|
473
|
-
this.logger.error?.("Token refresh failed", e?.message || String(e));
|
|
474
|
-
}
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
_handleDisconnect(err) {
|
|
478
|
-
if (!this._connected && !this._authenticating)
|
|
479
|
-
return;
|
|
480
|
-
const wasAuthenticating = this._authenticating;
|
|
481
|
-
this._pending.forEach((pending, id) => {
|
|
153
|
+
_cleanup() {
|
|
154
|
+
this._pending.forEach((pending) => {
|
|
482
155
|
if (pending.timer)
|
|
483
156
|
clearTimeout(pending.timer);
|
|
484
|
-
const errorMsg = wasAuthenticating ? "Authentication failed - credentials may be required" : err?.message || "Disconnected";
|
|
485
|
-
setTimeout(() => {
|
|
486
|
-
try {
|
|
487
|
-
pending.reject(new Error(errorMsg));
|
|
488
|
-
} catch (e) {}
|
|
489
|
-
}, 0);
|
|
490
|
-
this._pending.delete(id);
|
|
491
|
-
});
|
|
492
|
-
this._connected = false;
|
|
493
|
-
this._connecting = null;
|
|
494
|
-
this._authenticating = false;
|
|
495
|
-
if (wasAuthenticating && this.retries === 0) {
|
|
496
|
-
const rejectErr = err || new Error("Authentication failed and no retries configured");
|
|
497
|
-
try {
|
|
498
|
-
this._rejectReady(rejectErr);
|
|
499
|
-
} catch (e) {}
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (this._currentRetries < this.retries && !this._destroyed) {
|
|
503
|
-
this._createReady();
|
|
504
|
-
const base = Math.min(this.retryDelay * Math.pow(2, this._currentRetries), 30000);
|
|
505
|
-
const jitter = Math.floor(Math.random() * Math.min(1000, base));
|
|
506
|
-
const delay = base + jitter;
|
|
507
|
-
this._currentRetries += 1;
|
|
508
|
-
this.logger.info?.(`Attempting reconnect in ${delay}ms (attempt ${this._currentRetries})`);
|
|
509
|
-
this.token = null;
|
|
510
|
-
this._reconnectTimer = window.setTimeout(() => {
|
|
511
|
-
this.connect().catch((e) => {
|
|
512
|
-
this.logger.error?.("Reconnect failed", e?.message || e);
|
|
513
|
-
});
|
|
514
|
-
}, delay);
|
|
515
|
-
} else {
|
|
516
157
|
try {
|
|
517
|
-
|
|
158
|
+
pending.reject(new Error("Disconnected"));
|
|
518
159
|
} catch (e) {}
|
|
519
|
-
}
|
|
160
|
+
});
|
|
161
|
+
this._pending.clear();
|
|
520
162
|
}
|
|
521
163
|
close() {
|
|
522
164
|
if (this.ws) {
|
|
@@ -525,37 +167,8 @@ class BrowserClient {
|
|
|
525
167
|
} catch (e) {}
|
|
526
168
|
this.ws = null;
|
|
527
169
|
}
|
|
528
|
-
this._connected = false;
|
|
529
170
|
this._cleanup();
|
|
530
171
|
}
|
|
531
|
-
destroy() {
|
|
532
|
-
this._destroyed = true;
|
|
533
|
-
if (this._reconnectTimer) {
|
|
534
|
-
clearTimeout(this._reconnectTimer);
|
|
535
|
-
this._reconnectTimer = null;
|
|
536
|
-
}
|
|
537
|
-
this._rejectReady(new Error("Client destroyed"));
|
|
538
|
-
this._pending.forEach((pending, id) => {
|
|
539
|
-
if (pending.timer)
|
|
540
|
-
clearTimeout(pending.timer);
|
|
541
|
-
try {
|
|
542
|
-
pending.reject(new Error("Client destroyed"));
|
|
543
|
-
} catch (e) {}
|
|
544
|
-
this._pending.delete(id);
|
|
545
|
-
});
|
|
546
|
-
this._pendingQueue = [];
|
|
547
|
-
this.close();
|
|
548
|
-
}
|
|
549
|
-
_cleanup() {
|
|
550
|
-
this._pending.forEach((pending) => {
|
|
551
|
-
if (pending.timer)
|
|
552
|
-
clearTimeout(pending.timer);
|
|
553
|
-
try {
|
|
554
|
-
pending.reject(new Error("Disconnected"));
|
|
555
|
-
} catch (e) {}
|
|
556
|
-
});
|
|
557
|
-
this._pending.clear();
|
|
558
|
-
}
|
|
559
172
|
async disconnect() {
|
|
560
173
|
this.close();
|
|
561
174
|
}
|
|
@@ -596,6 +209,5 @@ class BrowserClient {
|
|
|
596
209
|
var src_default = BrowserClient;
|
|
597
210
|
export {
|
|
598
211
|
src_default as default,
|
|
599
|
-
BrowserClient as ScriptDBClient,
|
|
600
212
|
BrowserClient
|
|
601
213
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scriptdb/browser-client",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "Browser WebSocket client for ScriptDB",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -17,33 +17,15 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"dev": "bun --watch src/index.ts",
|
|
20
|
-
"build": "bun build src/index.ts --outdir dist --target
|
|
21
|
-
"build:cjs": "bun build src/index.ts --outdir dist --target
|
|
20
|
+
"build": "bun build src/index.ts --outdir dist --target browser --format esm --splitting",
|
|
21
|
+
"build:cjs": "bun build src/index.ts --outdir dist --target browser --format cjs --outfile dist/index.js",
|
|
22
22
|
"build:types": "tsc --emitDeclarationOnly",
|
|
23
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
24
|
"typecheck": "tsc --noEmit",
|
|
29
25
|
"clean": "rm -rf dist"
|
|
30
26
|
},
|
|
31
27
|
"devDependencies": {
|
|
32
|
-
"@types/
|
|
33
|
-
"
|
|
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"
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"typescript": "^5.3.3"
|
|
48
30
|
}
|
|
49
|
-
}
|
|
31
|
+
}
|