@scriptdb/client 1.1.0 → 1.1.2
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 +1 -1
- package/dist/index.d.mts +192 -0
- package/dist/index.d.ts +192 -0
- package/dist/index.js +317 -163
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +863 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +2 -5
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as net from "net";
|
|
3
|
+
import * as tls from "tls";
|
|
4
|
+
import { URL } from "url";
|
|
5
|
+
import * as crypto from "crypto";
|
|
6
|
+
var noopLogger = {
|
|
7
|
+
debug: () => {
|
|
8
|
+
},
|
|
9
|
+
info: () => {
|
|
10
|
+
},
|
|
11
|
+
warn: () => {
|
|
12
|
+
},
|
|
13
|
+
error: () => {
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var ScriptDBClient = class {
|
|
17
|
+
/**
|
|
18
|
+
* Create a new client. Do NOT auto-connect in constructor — call connect()
|
|
19
|
+
* @param uri - Connection URI
|
|
20
|
+
* @param options - Client options
|
|
21
|
+
* @param options.secure - use TLS
|
|
22
|
+
* @param options.logger - { debug, info, warn, error }
|
|
23
|
+
* @param options.requestTimeout - Request timeout in ms
|
|
24
|
+
* @param options.retries - Reconnection retries
|
|
25
|
+
* @param options.retryDelay - Initial retry delay in ms
|
|
26
|
+
* @param options.tlsOptions - Passed to tls.connect when secure
|
|
27
|
+
*/
|
|
28
|
+
constructor(uri, options = {}) {
|
|
29
|
+
this.socketTimeout = 0;
|
|
30
|
+
this.maxMessageSize = 0;
|
|
31
|
+
this._mask = () => {
|
|
32
|
+
};
|
|
33
|
+
this._maskArgs = () => [];
|
|
34
|
+
this.logger = {};
|
|
35
|
+
this.secure = true;
|
|
36
|
+
this.requestTimeout = 0;
|
|
37
|
+
this.retries = 0;
|
|
38
|
+
this.retryDelay = 0;
|
|
39
|
+
this.frame = "ndjson";
|
|
40
|
+
this.uri = "";
|
|
41
|
+
this.protocolName = "";
|
|
42
|
+
this.username = null;
|
|
43
|
+
this.password = null;
|
|
44
|
+
this.host = "";
|
|
45
|
+
this.port = 0;
|
|
46
|
+
this.database = null;
|
|
47
|
+
this.client = null;
|
|
48
|
+
this.buffer = Buffer.alloc(0);
|
|
49
|
+
this._nextId = 1;
|
|
50
|
+
this._pending = /* @__PURE__ */ new Map();
|
|
51
|
+
this._maxPending = 0;
|
|
52
|
+
this._maxQueue = 0;
|
|
53
|
+
this._pendingQueue = [];
|
|
54
|
+
this._connected = false;
|
|
55
|
+
this._authenticating = false;
|
|
56
|
+
this.token = null;
|
|
57
|
+
this._currentRetries = 0;
|
|
58
|
+
this.tokenExpiry = null;
|
|
59
|
+
this._destroyed = false;
|
|
60
|
+
this._reconnectTimer = null;
|
|
61
|
+
this.signing = null;
|
|
62
|
+
this._stringify = JSON.stringify;
|
|
63
|
+
this.ready = Promise.resolve();
|
|
64
|
+
this._resolveReadyFn = null;
|
|
65
|
+
this._rejectReadyFn = null;
|
|
66
|
+
this._connecting = null;
|
|
67
|
+
this._authPendingId = null;
|
|
68
|
+
if (!uri || typeof uri !== "string") throw new Error("uri required");
|
|
69
|
+
this.options = Object.assign({}, options);
|
|
70
|
+
this.socketTimeout = Number.isFinite(this.options.socketTimeout) ? this.options.socketTimeout : 0;
|
|
71
|
+
this.maxMessageSize = Number.isFinite(this.options.maxMessageSize) ? this.options.maxMessageSize : 5 * 1024 * 1024;
|
|
72
|
+
this._mask = (obj) => {
|
|
73
|
+
try {
|
|
74
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
75
|
+
const copy = Array.isArray(obj) ? obj.slice() : Object.assign({}, obj);
|
|
76
|
+
if (copy.token) copy.token = "****";
|
|
77
|
+
if (copy.password) copy.password = "****";
|
|
78
|
+
if (copy.data && copy.data.password) copy.data.password = "****";
|
|
79
|
+
return copy;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return obj;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const rawLogger = this.options.logger && typeof this.options.logger === "object" ? this.options.logger : noopLogger;
|
|
85
|
+
this._maskArgs = (args) => {
|
|
86
|
+
return args.map((a) => {
|
|
87
|
+
if (!a) return a;
|
|
88
|
+
if (typeof a === "string") return a;
|
|
89
|
+
return this._mask(a);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
this.logger = {
|
|
93
|
+
debug: (...args) => rawLogger.debug && rawLogger.debug(...this._maskArgs(args)),
|
|
94
|
+
info: (...args) => rawLogger.info && rawLogger.info(...this._maskArgs(args)),
|
|
95
|
+
warn: (...args) => rawLogger.warn && rawLogger.warn(...this._maskArgs(args)),
|
|
96
|
+
error: (...args) => rawLogger.error && rawLogger.error(...this._maskArgs(args))
|
|
97
|
+
};
|
|
98
|
+
this.secure = typeof this.options.secure === "boolean" ? !!this.options.secure : true;
|
|
99
|
+
if (!this.secure)
|
|
100
|
+
this.logger.warn?.(
|
|
101
|
+
"Warning: connecting in insecure mode (secure=false). This is not recommended."
|
|
102
|
+
);
|
|
103
|
+
this.requestTimeout = Number.isFinite(this.options.requestTimeout) ? this.options.requestTimeout : 0;
|
|
104
|
+
this.retries = Number.isFinite(this.options.retries) ? this.options.retries : 3;
|
|
105
|
+
this.retryDelay = Number.isFinite(this.options.retryDelay) ? this.options.retryDelay : 1e3;
|
|
106
|
+
this.frame = this.options.frame === "length-prefix" || this.options.preferLengthPrefix ? "length-prefix" : "ndjson";
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = new URL(uri);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
throw new Error("Invalid uri");
|
|
112
|
+
}
|
|
113
|
+
if (parsed.protocol !== "scriptdb:") {
|
|
114
|
+
throw new Error("URI must use scriptdb:// protocol");
|
|
115
|
+
}
|
|
116
|
+
this.uri = uri;
|
|
117
|
+
this.protocolName = parsed.protocol ? parsed.protocol.replace(":", "") : "scriptdb";
|
|
118
|
+
this.username = (typeof this.options.username === "string" ? this.options.username : parsed.username) || null;
|
|
119
|
+
this.password = (typeof this.options.password === "string" ? this.options.password : parsed.password) || null;
|
|
120
|
+
if (parsed.username && !(typeof this.options.username === "string")) {
|
|
121
|
+
this.logger.warn?.(
|
|
122
|
+
"Credentials found in URI \u2014 consider passing credentials via options instead of embedding in URI"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
parsed.username = "";
|
|
127
|
+
parsed.password = "";
|
|
128
|
+
this.uri = parsed.toString();
|
|
129
|
+
} catch (e) {
|
|
130
|
+
this.uri = uri;
|
|
131
|
+
}
|
|
132
|
+
this.host = parsed.hostname || "localhost";
|
|
133
|
+
this.port = parsed.port ? parseInt(parsed.port, 10) : 1234;
|
|
134
|
+
this.database = parsed.pathname && parsed.pathname.length > 1 ? parsed.pathname.slice(1) : null;
|
|
135
|
+
this.client = null;
|
|
136
|
+
this.buffer = Buffer.alloc(0);
|
|
137
|
+
this._nextId = 1;
|
|
138
|
+
this._pending = /* @__PURE__ */ new Map();
|
|
139
|
+
this._maxPending = Number.isFinite(this.options.maxPending) ? this.options.maxPending : 100;
|
|
140
|
+
this._maxQueue = Number.isFinite(this.options.maxQueue) ? this.options.maxQueue : 1e3;
|
|
141
|
+
this._pendingQueue = [];
|
|
142
|
+
this._connected = false;
|
|
143
|
+
this._authenticating = false;
|
|
144
|
+
this.token = null;
|
|
145
|
+
this._currentRetries = 0;
|
|
146
|
+
this.tokenExpiry = null;
|
|
147
|
+
this._destroyed = false;
|
|
148
|
+
this._reconnectTimer = null;
|
|
149
|
+
this.signing = this.options.signing && this.options.signing.secret ? this.options.signing : null;
|
|
150
|
+
this._stringify = typeof this.options.stringify === "function" ? this.options.stringify : JSON.stringify;
|
|
151
|
+
this._createReady();
|
|
152
|
+
}
|
|
153
|
+
_createReady() {
|
|
154
|
+
this.ready = new Promise((resolve, reject) => {
|
|
155
|
+
this._resolveReadyFn = resolve;
|
|
156
|
+
this._rejectReadyFn = reject;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
_resolveReady(value) {
|
|
160
|
+
if (this._resolveReadyFn) {
|
|
161
|
+
try {
|
|
162
|
+
this._resolveReadyFn(value);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
}
|
|
165
|
+
this._resolveReadyFn = null;
|
|
166
|
+
this._rejectReadyFn = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
_rejectReady(err) {
|
|
170
|
+
if (this._rejectReadyFn) {
|
|
171
|
+
const rejectFn = this._rejectReadyFn;
|
|
172
|
+
this._resolveReadyFn = null;
|
|
173
|
+
this._rejectReadyFn = null;
|
|
174
|
+
process.nextTick(() => {
|
|
175
|
+
try {
|
|
176
|
+
rejectFn(err);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check if connected
|
|
184
|
+
*/
|
|
185
|
+
get connected() {
|
|
186
|
+
return this._connected;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Connect to server and authenticate. Returns a Promise that resolves when authenticated.
|
|
190
|
+
*/
|
|
191
|
+
connect() {
|
|
192
|
+
if (this._connecting) return this._connecting;
|
|
193
|
+
this._connecting = new Promise((resolve, reject) => {
|
|
194
|
+
const opts = { host: this.host, port: this.port };
|
|
195
|
+
const onConnect = () => {
|
|
196
|
+
console.log("ScriptDBClient: Connected to server at", opts.host, opts.port);
|
|
197
|
+
this.logger?.info?.("Connected to server");
|
|
198
|
+
this._connected = true;
|
|
199
|
+
this._currentRetries = 0;
|
|
200
|
+
console.log("ScriptDBClient: Setting up listeners...");
|
|
201
|
+
this._setupListeners();
|
|
202
|
+
console.log("ScriptDBClient: Authenticating...");
|
|
203
|
+
this.authenticate().then(() => {
|
|
204
|
+
this._processQueue();
|
|
205
|
+
resolve(this);
|
|
206
|
+
}).catch((err) => {
|
|
207
|
+
reject(err);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
if (this.secure) {
|
|
212
|
+
const connectionOpts = { host: opts.host, port: opts.port };
|
|
213
|
+
const tlsOptions = Object.assign(
|
|
214
|
+
{},
|
|
215
|
+
this.options.tlsOptions || {},
|
|
216
|
+
connectionOpts
|
|
217
|
+
);
|
|
218
|
+
if (typeof tlsOptions.rejectUnauthorized === "undefined")
|
|
219
|
+
tlsOptions.rejectUnauthorized = true;
|
|
220
|
+
this.client = tls.connect(tlsOptions, onConnect);
|
|
221
|
+
} else {
|
|
222
|
+
this.client = net.createConnection(opts, onConnect);
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
const error = e;
|
|
226
|
+
this.logger?.error?.("Connection failed", error.message);
|
|
227
|
+
this._rejectReady(error);
|
|
228
|
+
this._createReady();
|
|
229
|
+
this._connecting = null;
|
|
230
|
+
return reject(error);
|
|
231
|
+
}
|
|
232
|
+
if (!this.client) {
|
|
233
|
+
const error = new Error("Failed to create client socket");
|
|
234
|
+
this.logger?.error?.("Connection failed", error.message);
|
|
235
|
+
this._connecting = null;
|
|
236
|
+
return reject(error);
|
|
237
|
+
}
|
|
238
|
+
const onError = (err) => {
|
|
239
|
+
this.logger?.error?.(
|
|
240
|
+
"Client socket error:",
|
|
241
|
+
err && err.message ? err.message : err
|
|
242
|
+
);
|
|
243
|
+
this._handleDisconnect(err);
|
|
244
|
+
};
|
|
245
|
+
const onClose = (hadError) => {
|
|
246
|
+
this.logger?.info?.("Server closed connection");
|
|
247
|
+
this._handleDisconnect(null);
|
|
248
|
+
};
|
|
249
|
+
this.client.on("error", onError);
|
|
250
|
+
this.client.on("close", onClose);
|
|
251
|
+
if (this.socketTimeout > 0 && this.client) {
|
|
252
|
+
this.client.setTimeout(this.socketTimeout);
|
|
253
|
+
this.client.on("timeout", () => {
|
|
254
|
+
this.logger?.warn?.("Socket timeout, destroying connection");
|
|
255
|
+
try {
|
|
256
|
+
this.client?.destroy();
|
|
257
|
+
} catch (e) {
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}).catch((err) => {
|
|
262
|
+
this._connecting = null;
|
|
263
|
+
throw err;
|
|
264
|
+
});
|
|
265
|
+
return this._connecting;
|
|
266
|
+
}
|
|
267
|
+
_setupListeners() {
|
|
268
|
+
if (!this.client) return;
|
|
269
|
+
console.log("ScriptDBClient _setupListeners: called, client exists:", !!this.client);
|
|
270
|
+
this.client.removeAllListeners("data");
|
|
271
|
+
this.buffer = Buffer.alloc(0);
|
|
272
|
+
console.log("ScriptDBClient _setupListeners: frame mode:", this.frame);
|
|
273
|
+
if (this.frame === "length-prefix") {
|
|
274
|
+
this.client.on("data", (chunk) => {
|
|
275
|
+
if (!Buffer.isBuffer(chunk)) {
|
|
276
|
+
try {
|
|
277
|
+
chunk = Buffer.from(chunk);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
283
|
+
while (this.buffer.length >= 4) {
|
|
284
|
+
const len = this.buffer.readUInt32BE(0);
|
|
285
|
+
if (len > this.maxMessageSize) {
|
|
286
|
+
this.logger?.error?.(
|
|
287
|
+
"Incoming length-prefixed frame exceeds maxMessageSize \u2014 closing connection"
|
|
288
|
+
);
|
|
289
|
+
try {
|
|
290
|
+
this.client?.destroy();
|
|
291
|
+
} catch (e) {
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (this.buffer.length < 4 + len) break;
|
|
296
|
+
const payload = this.buffer.slice(4, 4 + len);
|
|
297
|
+
this.buffer = this.buffer.slice(4 + len);
|
|
298
|
+
let msg;
|
|
299
|
+
try {
|
|
300
|
+
msg = JSON.parse(payload.toString("utf8"));
|
|
301
|
+
} catch (e) {
|
|
302
|
+
this.logger?.error?.(
|
|
303
|
+
"Invalid JSON frame",
|
|
304
|
+
e && e.message ? e.message : e
|
|
305
|
+
);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (!msg || typeof msg !== "object") continue;
|
|
309
|
+
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);
|
|
310
|
+
if (!validSchema) {
|
|
311
|
+
this.logger?.warn?.("Message failed schema validation \u2014 ignoring");
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
this._handleMessage(msg);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
console.log("ScriptDBClient _setupListeners: Setting up NDJSON data listener");
|
|
319
|
+
this.client.on("data", (chunk) => {
|
|
320
|
+
console.log("ScriptDBClient: Received data chunk, length:", chunk.length);
|
|
321
|
+
if (!Buffer.isBuffer(chunk)) {
|
|
322
|
+
try {
|
|
323
|
+
chunk = Buffer.from(chunk);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const idxLastNewline = chunk.indexOf(10);
|
|
329
|
+
if (this.buffer.length === 0 && idxLastNewline === chunk.length - 1) {
|
|
330
|
+
let start = 0;
|
|
331
|
+
let idx2;
|
|
332
|
+
while ((idx2 = chunk.indexOf(10, start)) !== -1) {
|
|
333
|
+
const lineBuf = chunk.slice(start, idx2);
|
|
334
|
+
start = idx2 + 1;
|
|
335
|
+
if (lineBuf.length === 0) continue;
|
|
336
|
+
let msg;
|
|
337
|
+
try {
|
|
338
|
+
msg = JSON.parse(lineBuf.toString("utf8"));
|
|
339
|
+
} catch (e) {
|
|
340
|
+
this.logger?.error?.(
|
|
341
|
+
"Invalid JSON from server",
|
|
342
|
+
e && e.message ? e.message : e
|
|
343
|
+
);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (!msg || typeof msg !== "object") continue;
|
|
347
|
+
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");
|
|
348
|
+
if (!validSchema) continue;
|
|
349
|
+
this._handleMessage(msg);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
354
|
+
if (this.buffer.length > this.maxMessageSize) {
|
|
355
|
+
this.logger?.error?.(
|
|
356
|
+
"Incoming message exceeds maxMessageSize \u2014 closing connection"
|
|
357
|
+
);
|
|
358
|
+
try {
|
|
359
|
+
this.client?.destroy();
|
|
360
|
+
} catch (e) {
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
let idx;
|
|
365
|
+
while ((idx = this.buffer.indexOf(10)) !== -1) {
|
|
366
|
+
const lineBuf = this.buffer.slice(0, idx);
|
|
367
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
368
|
+
if (lineBuf.length === 0) continue;
|
|
369
|
+
let msg;
|
|
370
|
+
try {
|
|
371
|
+
msg = JSON.parse(lineBuf.toString("utf8"));
|
|
372
|
+
} catch (e) {
|
|
373
|
+
this.logger?.error?.(
|
|
374
|
+
"Invalid JSON from server",
|
|
375
|
+
e && e.message ? e.message : e
|
|
376
|
+
);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
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");
|
|
380
|
+
if (!validSchema) {
|
|
381
|
+
this.logger?.warn?.("Message failed schema validation \u2014 ignoring");
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
this._handleMessage(msg);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// build the final buffer for sending using current token and signing settings
|
|
390
|
+
_buildFinalBuffer(payloadBase, id) {
|
|
391
|
+
const payloadObj = Object.assign(
|
|
392
|
+
{ id, action: payloadBase.action },
|
|
393
|
+
payloadBase.data !== void 0 ? { data: payloadBase.data } : {}
|
|
394
|
+
);
|
|
395
|
+
if (this.token) payloadObj.token = this.token;
|
|
396
|
+
const payloadStr = this._stringify(payloadObj);
|
|
397
|
+
if (this.signing && this.signing.secret) {
|
|
398
|
+
const hmac = crypto.createHmac(
|
|
399
|
+
this.signing.algorithm || "sha256",
|
|
400
|
+
this.signing.secret
|
|
401
|
+
);
|
|
402
|
+
hmac.update(payloadStr);
|
|
403
|
+
const sig = hmac.digest("hex");
|
|
404
|
+
const envelope = { id, signature: sig, payload: payloadObj };
|
|
405
|
+
const envelopeStr = this._stringify(envelope);
|
|
406
|
+
if (this.frame === "length-prefix") {
|
|
407
|
+
const body = Buffer.from(envelopeStr, "utf8");
|
|
408
|
+
const buf = Buffer.allocUnsafe(4 + body.length);
|
|
409
|
+
buf.writeUInt32BE(body.length, 0);
|
|
410
|
+
body.copy(buf, 4);
|
|
411
|
+
return buf;
|
|
412
|
+
}
|
|
413
|
+
return Buffer.from(envelopeStr + "\n", "utf8");
|
|
414
|
+
}
|
|
415
|
+
if (this.frame === "length-prefix") {
|
|
416
|
+
const body = Buffer.from(payloadStr, "utf8");
|
|
417
|
+
const buf = Buffer.allocUnsafe(4 + body.length);
|
|
418
|
+
buf.writeUInt32BE(body.length, 0);
|
|
419
|
+
body.copy(buf, 4);
|
|
420
|
+
return buf;
|
|
421
|
+
}
|
|
422
|
+
return Buffer.from(payloadStr + "\n", "utf8");
|
|
423
|
+
}
|
|
424
|
+
_handleMessage(msg) {
|
|
425
|
+
console.log("ScriptDBClient _handleMessage:", JSON.stringify(msg));
|
|
426
|
+
console.log("ScriptDBClient _handleMessage msg.id:", msg.id, "msg.action:", msg.action, "msg.command:", msg.command, "msg.message:", msg.message);
|
|
427
|
+
if (msg && typeof msg.id !== "undefined") {
|
|
428
|
+
console.log("Handling message with id:", msg.id, "action:", msg.action, "message:", msg.message);
|
|
429
|
+
const pending = this._pending.get(msg.id);
|
|
430
|
+
if (!pending) {
|
|
431
|
+
console.log("No pending request for id", msg.id, "pending map size:", this._pending.size);
|
|
432
|
+
this.logger?.debug?.("No pending request for id", msg.id);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const { resolve, reject, timer } = pending;
|
|
436
|
+
if (timer) clearTimeout(timer);
|
|
437
|
+
this._pending.delete(msg.id);
|
|
438
|
+
this._processQueue();
|
|
439
|
+
if (msg.action === "login" || msg.command === "login") {
|
|
440
|
+
console.log("Processing login response with id");
|
|
441
|
+
if (msg.message === "AUTH OK") {
|
|
442
|
+
console.log("AUTH OK - setting token and resolving");
|
|
443
|
+
this.token = msg.data && msg.data.token ? msg.data.token : null;
|
|
444
|
+
this._resolveReady(null);
|
|
445
|
+
return resolve(msg.data);
|
|
446
|
+
} else {
|
|
447
|
+
console.log("AUTH FAILED:", msg.data);
|
|
448
|
+
this._rejectReady(new Error("Authentication failed"));
|
|
449
|
+
const errorMsg = msg.data || "Authentication failed";
|
|
450
|
+
try {
|
|
451
|
+
this.client?.end();
|
|
452
|
+
} catch (e) {
|
|
453
|
+
}
|
|
454
|
+
return reject(new Error(typeof errorMsg === "string" ? errorMsg : "Authentication failed"));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "OK")
|
|
458
|
+
return resolve(msg.data);
|
|
459
|
+
if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "ERROR")
|
|
460
|
+
return reject(new Error(typeof msg.data === "string" ? msg.data : "Server returned ERROR"));
|
|
461
|
+
if (msg.action === "create-db" && msg.message === "SUCCESS")
|
|
462
|
+
return resolve(msg.data);
|
|
463
|
+
if (msg.action === "create-db" && msg.message === "ERROR")
|
|
464
|
+
return reject(new Error(typeof msg.data === "string" ? msg.data : "Failed to create database"));
|
|
465
|
+
if (msg.message === "OK" || msg.message === "SUCCESS") {
|
|
466
|
+
return resolve(msg.data);
|
|
467
|
+
} else if (msg.message === "ERROR") {
|
|
468
|
+
return reject(new Error(typeof msg.data === "string" ? msg.data : "Request failed"));
|
|
469
|
+
}
|
|
470
|
+
return reject(new Error("Invalid response from server"));
|
|
471
|
+
}
|
|
472
|
+
console.log("Unhandled message:", msg);
|
|
473
|
+
this.logger?.debug?.("Unhandled message from server", this._mask(msg));
|
|
474
|
+
}
|
|
475
|
+
authenticate() {
|
|
476
|
+
if (this._authenticating)
|
|
477
|
+
return Promise.reject(new Error("Already authenticating"));
|
|
478
|
+
this._authenticating = true;
|
|
479
|
+
return new Promise((resolve, reject) => {
|
|
480
|
+
const id = this._nextId++;
|
|
481
|
+
const payload = {
|
|
482
|
+
id,
|
|
483
|
+
action: "login",
|
|
484
|
+
data: { username: this.username, password: this.password }
|
|
485
|
+
};
|
|
486
|
+
let timer = null;
|
|
487
|
+
if (this.requestTimeout > 0) {
|
|
488
|
+
timer = setTimeout(() => {
|
|
489
|
+
this._pending.delete(id);
|
|
490
|
+
this._authenticating = false;
|
|
491
|
+
reject(new Error("Auth timeout"));
|
|
492
|
+
this.close();
|
|
493
|
+
}, this.requestTimeout);
|
|
494
|
+
}
|
|
495
|
+
this._pending.set(id, {
|
|
496
|
+
resolve: (data) => {
|
|
497
|
+
if (timer) clearTimeout(timer);
|
|
498
|
+
this._authenticating = false;
|
|
499
|
+
if (data && data.token) this.token = data.token;
|
|
500
|
+
resolve(data);
|
|
501
|
+
},
|
|
502
|
+
reject: (err) => {
|
|
503
|
+
if (timer) clearTimeout(timer);
|
|
504
|
+
this._authenticating = false;
|
|
505
|
+
reject(err);
|
|
506
|
+
},
|
|
507
|
+
timer
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
const buf = Buffer.from(JSON.stringify(payload) + "\n", "utf8");
|
|
511
|
+
this._write(buf).catch((err) => {
|
|
512
|
+
if (timer) clearTimeout(timer);
|
|
513
|
+
this._pending.delete(id);
|
|
514
|
+
this._authenticating = false;
|
|
515
|
+
reject(err);
|
|
516
|
+
});
|
|
517
|
+
} catch (e) {
|
|
518
|
+
clearTimeout(timer);
|
|
519
|
+
this._pending.delete(id);
|
|
520
|
+
this._authenticating = false;
|
|
521
|
+
reject(e);
|
|
522
|
+
}
|
|
523
|
+
}).then((data) => {
|
|
524
|
+
if (data && data.token) {
|
|
525
|
+
this.token = data.token;
|
|
526
|
+
this._resolveReady(null);
|
|
527
|
+
}
|
|
528
|
+
return data;
|
|
529
|
+
}).catch((err) => {
|
|
530
|
+
this._rejectReady(err);
|
|
531
|
+
throw err;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
// internal write helper that respects backpressure
|
|
535
|
+
_write(buf) {
|
|
536
|
+
return new Promise((resolve, reject) => {
|
|
537
|
+
if (!this.client || !this._connected)
|
|
538
|
+
return reject(new Error("Not connected"));
|
|
539
|
+
try {
|
|
540
|
+
const ok = this.client.write(buf, (err) => {
|
|
541
|
+
if (err) return reject(err);
|
|
542
|
+
resolve(void 0);
|
|
543
|
+
});
|
|
544
|
+
if (!ok) {
|
|
545
|
+
this.client.once("drain", () => resolve(void 0));
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
reject(e);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
async _processQueue() {
|
|
553
|
+
while (this._pending.size < this._maxPending && this._pendingQueue.length > 0) {
|
|
554
|
+
const item = this._pendingQueue.shift();
|
|
555
|
+
const { payloadBase, id, resolve, reject, timer } = item;
|
|
556
|
+
if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
|
|
557
|
+
const refreshed = await this._maybeRefreshToken();
|
|
558
|
+
if (!refreshed) {
|
|
559
|
+
clearTimeout(timer);
|
|
560
|
+
try {
|
|
561
|
+
reject(new Error("Token expired and refresh failed"));
|
|
562
|
+
} catch (e) {
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
let buf;
|
|
568
|
+
try {
|
|
569
|
+
buf = this._buildFinalBuffer(payloadBase, id);
|
|
570
|
+
} catch (e) {
|
|
571
|
+
clearTimeout(timer);
|
|
572
|
+
try {
|
|
573
|
+
reject(e);
|
|
574
|
+
} catch (er) {
|
|
575
|
+
}
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
579
|
+
try {
|
|
580
|
+
await this._write(buf);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
clearTimeout(timer);
|
|
583
|
+
this._pending.delete(id);
|
|
584
|
+
try {
|
|
585
|
+
reject(e);
|
|
586
|
+
} catch (er) {
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Execute a command. Supports concurrent requests via request id mapping.
|
|
593
|
+
* Returns a Promise resolved with response.data or rejected on ERROR/timeout.
|
|
594
|
+
*/
|
|
595
|
+
async execute(payload) {
|
|
596
|
+
try {
|
|
597
|
+
await this.ready;
|
|
598
|
+
} catch (err) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
"Not authenticated: " + (err && err.message ? err.message : err)
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (!this.token) throw new Error("Not authenticated");
|
|
604
|
+
const id = this._nextId++;
|
|
605
|
+
const payloadBase = { action: payload.action, data: payload.data };
|
|
606
|
+
if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
|
|
607
|
+
const refreshed = await this._maybeRefreshToken();
|
|
608
|
+
if (!refreshed) throw new Error("Token expired");
|
|
609
|
+
}
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
let timer = null;
|
|
612
|
+
if (this.requestTimeout > 0) {
|
|
613
|
+
timer = setTimeout(() => {
|
|
614
|
+
if (this._pending.has(id)) {
|
|
615
|
+
this._pending.delete(id);
|
|
616
|
+
reject(new Error("Request timeout"));
|
|
617
|
+
}
|
|
618
|
+
}, this.requestTimeout);
|
|
619
|
+
}
|
|
620
|
+
if (this._pending.size >= this._maxPending) {
|
|
621
|
+
if (this._pendingQueue.length >= this._maxQueue) {
|
|
622
|
+
if (timer) clearTimeout(timer);
|
|
623
|
+
return reject(new Error("Pending queue full"));
|
|
624
|
+
}
|
|
625
|
+
this._pendingQueue.push({ payloadBase, id, resolve, reject, timer });
|
|
626
|
+
this._processQueue().catch(() => {
|
|
627
|
+
});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
let finalBuf;
|
|
631
|
+
try {
|
|
632
|
+
finalBuf = this._buildFinalBuffer(payloadBase, id);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
return reject(e);
|
|
636
|
+
}
|
|
637
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
638
|
+
this._write(finalBuf).catch((e) => {
|
|
639
|
+
if (timer) clearTimeout(timer);
|
|
640
|
+
this._pending.delete(id);
|
|
641
|
+
reject(e);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Attempt to refresh token using provided tokenRefresh option if token expired.
|
|
647
|
+
* options.tokenRefresh should be async function that returns { token, expiresAt }
|
|
648
|
+
*/
|
|
649
|
+
async _maybeRefreshToken() {
|
|
650
|
+
if (!this.options.tokenRefresh || typeof this.options.tokenRefresh !== "function")
|
|
651
|
+
return false;
|
|
652
|
+
try {
|
|
653
|
+
const res = await this.options.tokenRefresh();
|
|
654
|
+
if (res && res.token) {
|
|
655
|
+
this.token = res.token;
|
|
656
|
+
if (res.expiresAt) this.tokenExpiry = res.expiresAt;
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
} catch (e) {
|
|
660
|
+
this.logger?.error?.(
|
|
661
|
+
"Token refresh failed",
|
|
662
|
+
e && e.message ? e.message : String(e)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
destroy() {
|
|
668
|
+
this._destroyed = true;
|
|
669
|
+
if (this._reconnectTimer) {
|
|
670
|
+
clearTimeout(this._reconnectTimer);
|
|
671
|
+
this._reconnectTimer = null;
|
|
672
|
+
}
|
|
673
|
+
this._rejectReady(new Error("Client destroyed"));
|
|
674
|
+
this._pending.forEach((pending, id) => {
|
|
675
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
676
|
+
try {
|
|
677
|
+
pending.reject(new Error("Client destroyed"));
|
|
678
|
+
} catch (e) {
|
|
679
|
+
}
|
|
680
|
+
this._pending.delete(id);
|
|
681
|
+
});
|
|
682
|
+
this._pendingQueue = [];
|
|
683
|
+
try {
|
|
684
|
+
if (this.client) this.client.destroy();
|
|
685
|
+
} catch (e) {
|
|
686
|
+
}
|
|
687
|
+
this.client = null;
|
|
688
|
+
}
|
|
689
|
+
_handleDisconnect(err) {
|
|
690
|
+
if (!this._connected && !this._authenticating) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const wasAuthenticating = this._authenticating;
|
|
694
|
+
this._pending.forEach((pending, id) => {
|
|
695
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
696
|
+
const errorMsg = wasAuthenticating ? "Authentication failed - credentials may be required" : err && err.message ? err.message : "Disconnected";
|
|
697
|
+
process.nextTick(() => {
|
|
698
|
+
try {
|
|
699
|
+
pending.reject(new Error(errorMsg));
|
|
700
|
+
} catch (e) {
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
this._pending.delete(id);
|
|
704
|
+
});
|
|
705
|
+
this._connected = false;
|
|
706
|
+
this._connecting = null;
|
|
707
|
+
this._authenticating = false;
|
|
708
|
+
if (wasAuthenticating && this.retries === 0) {
|
|
709
|
+
const rejectErr = err || new Error("Authentication failed and no retries configured");
|
|
710
|
+
try {
|
|
711
|
+
this._rejectReady(rejectErr);
|
|
712
|
+
} catch (e) {
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (this._currentRetries < this.retries) {
|
|
717
|
+
this._createReady();
|
|
718
|
+
const base = Math.min(
|
|
719
|
+
this.retryDelay * Math.pow(2, this._currentRetries),
|
|
720
|
+
3e4
|
|
721
|
+
);
|
|
722
|
+
const jitter = Math.floor(Math.random() * Math.min(1e3, base));
|
|
723
|
+
const delay = base + jitter;
|
|
724
|
+
this._currentRetries += 1;
|
|
725
|
+
this.logger?.info?.(
|
|
726
|
+
"Attempting reconnect in " + delay + "ms (attempt " + this._currentRetries + ")"
|
|
727
|
+
);
|
|
728
|
+
this.token = null;
|
|
729
|
+
setTimeout(() => {
|
|
730
|
+
this.connect().catch((e) => {
|
|
731
|
+
this.logger?.error?.("Reconnect failed", e && e.message ? e.message : e);
|
|
732
|
+
});
|
|
733
|
+
}, delay);
|
|
734
|
+
} else {
|
|
735
|
+
try {
|
|
736
|
+
this._rejectReady(err || new Error("Disconnected and no retries left"));
|
|
737
|
+
} catch (e) {
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
close() {
|
|
742
|
+
if (!this.client) return;
|
|
743
|
+
try {
|
|
744
|
+
this.client.removeAllListeners("data");
|
|
745
|
+
this.client.removeAllListeners("error");
|
|
746
|
+
this.client.removeAllListeners("close");
|
|
747
|
+
this.client.end();
|
|
748
|
+
} catch (e) {
|
|
749
|
+
}
|
|
750
|
+
this.client = null;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Disconnect (alias for close)
|
|
754
|
+
*/
|
|
755
|
+
disconnect() {
|
|
756
|
+
this.close();
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Send request helper (for public API methods)
|
|
760
|
+
*/
|
|
761
|
+
async sendRequest(action, data = {}) {
|
|
762
|
+
return this.execute({ action, data });
|
|
763
|
+
}
|
|
764
|
+
// ========== Public API Methods ==========
|
|
765
|
+
/**
|
|
766
|
+
* Login with username and password
|
|
767
|
+
*/
|
|
768
|
+
async login(username, password) {
|
|
769
|
+
return this.sendRequest("login", { username, password });
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Logout from server
|
|
773
|
+
*/
|
|
774
|
+
async logout() {
|
|
775
|
+
return this.sendRequest("logout", {});
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* List all databases
|
|
779
|
+
*/
|
|
780
|
+
async listDatabases() {
|
|
781
|
+
return this.sendRequest("list-dbs", {});
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Create a new database
|
|
785
|
+
*/
|
|
786
|
+
async createDatabase(name) {
|
|
787
|
+
return this.sendRequest("create-db", { databaseName: name });
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Remove a database
|
|
791
|
+
*/
|
|
792
|
+
async removeDatabase(name) {
|
|
793
|
+
return this.sendRequest("remove-db", { databaseName: name });
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Rename a database
|
|
797
|
+
*/
|
|
798
|
+
async renameDatabase(oldName, newName) {
|
|
799
|
+
return this.sendRequest("rename-db", { databaseName: oldName, newName });
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Execute code in a database
|
|
803
|
+
*/
|
|
804
|
+
async run(code, databaseName) {
|
|
805
|
+
let stringCode;
|
|
806
|
+
if (typeof code === "function") {
|
|
807
|
+
const funcStr = code.toString();
|
|
808
|
+
const arrowMatch = funcStr.match(/^[\s]*\(?\s*\)?\s*=>\s*{?/);
|
|
809
|
+
const functionMatch = funcStr.match(/^[\s]*function\s*\(?[\w\s]*\)?\s*{/);
|
|
810
|
+
const match = arrowMatch || functionMatch;
|
|
811
|
+
const start = match ? match[0].length : 0;
|
|
812
|
+
const end = funcStr.lastIndexOf("}");
|
|
813
|
+
stringCode = funcStr.substring(start, end);
|
|
814
|
+
stringCode = stringCode.replace(/^[\s\r\n]+/, "").replace(/[\s\r\n]+$/, "");
|
|
815
|
+
stringCode = stringCode.replace(
|
|
816
|
+
/import\s*\(\s*([^)]+?)\s*\)\s*\.from\s*\(\s*(['"])([^'"]+)\2\s*\)/g,
|
|
817
|
+
(_, importArg, quote, modulePath) => {
|
|
818
|
+
const trimmed = importArg.trim();
|
|
819
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
820
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
821
|
+
return `import { ${inner} } from ${quote}${modulePath}${quote}`;
|
|
822
|
+
} else {
|
|
823
|
+
return `import ${trimmed} from ${quote}${modulePath}${quote}`;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
stringCode = stringCode.split("\n").map((line) => line.trim()).join("\n").trim();
|
|
828
|
+
} else {
|
|
829
|
+
stringCode = code;
|
|
830
|
+
}
|
|
831
|
+
return this.sendRequest("script-code", { code: stringCode, databaseName });
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Save a database to disk
|
|
835
|
+
*/
|
|
836
|
+
async saveDatabase(databaseName) {
|
|
837
|
+
return this.sendRequest("save-db", { databaseName });
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Update database metadata
|
|
841
|
+
*/
|
|
842
|
+
async updateDatabase(databaseName, data) {
|
|
843
|
+
return this.sendRequest("update-db", { databaseName, ...data });
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get server information
|
|
847
|
+
*/
|
|
848
|
+
async getInfo() {
|
|
849
|
+
return this.sendRequest("get-info", {});
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Execute shell command on server
|
|
853
|
+
*/
|
|
854
|
+
async executeShell(command) {
|
|
855
|
+
return this.sendRequest("shell-command", { command });
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
var index_default = ScriptDBClient;
|
|
859
|
+
export {
|
|
860
|
+
ScriptDBClient,
|
|
861
|
+
index_default as default
|
|
862
|
+
};
|
|
863
|
+
//# sourceMappingURL=index.mjs.map
|