@scriptdb/client 1.0.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.
Files changed (3) hide show
  1. package/README.md +307 -0
  2. package/dist/index.js +694 -0
  3. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # ScriptDB Client
2
+
3
+ Client module for connecting to and interacting with ScriptDB servers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @scriptdb/client
9
+ # or
10
+ yarn add @scriptdb/client
11
+ # or
12
+ bun add @scriptdb/client
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { ScriptDBClient } from '@scriptdb/client';
19
+
20
+ // Create a client instance
21
+ const client = new ScriptDBClient('scriptdb://localhost:1234/mydatabase', {
22
+ username: 'admin',
23
+ password: 'password'
24
+ });
25
+
26
+ // Connect to the server
27
+ await client.connect();
28
+
29
+ // Execute a command
30
+ const result = await client.execute('getUsers');
31
+ console.log(result);
32
+
33
+ // Close the connection when done
34
+ client.close();
35
+ ```
36
+
37
+ ## API Reference
38
+
39
+ ### Constructor
40
+
41
+ ```typescript
42
+ new ScriptDBClient(uri, options?)
43
+ ```
44
+
45
+ - `uri` (string): Connection URI in format `scriptdb://host:port/database`
46
+ - `options` (ClientOptions, optional): Configuration options
47
+
48
+ #### URI Format
49
+
50
+ The URI follows the pattern: `scriptdb://[username:password@]host:port/database`
51
+
52
+ Examples:
53
+ - `scriptdb://localhost:1234/mydb`
54
+ - `scriptdb://user:pass@localhost:1234/mydb`
55
+ - `scriptdb://example.com:8080/production`
56
+
57
+ ### Options
58
+
59
+ ```typescript
60
+ interface ClientOptions {
61
+ secure?: boolean; // Use TLS (default: true)
62
+ logger?: Logger; // Custom logger
63
+ requestTimeout?: number; // Request timeout in ms (default: 0 = no timeout)
64
+ socketTimeout?: number; // Socket timeout in ms (default: 0 = no timeout)
65
+ retries?: number; // Reconnection retries (default: 3)
66
+ retryDelay?: number; // Initial retry delay in ms (default: 1000)
67
+ tlsOptions?: tls.TlsOptions; // TLS options when secure=true
68
+ frame?: 'ndjson' | 'length-prefix'; // Message framing (default: 'ndjson')
69
+ preferLengthPrefix?: boolean; // Use length-prefixed framing (alias for frame)
70
+ maxPending?: number; // Max concurrent requests (default: 100)
71
+ maxQueue?: number; // Max queued requests (default: 1000)
72
+ maxMessageSize?: number; // Max message size in bytes (default: 5MB)
73
+ signing?: { // Message signing options
74
+ secret: string;
75
+ algorithm?: string; // Default: 'sha256'
76
+ };
77
+ stringify?: (obj: any) => string; // Custom stringify function
78
+ username?: string; // Username (overrides URI)
79
+ password?: string; // Password (overrides URI)
80
+ tokenRefresh?: () => Promise<{ token: string; expiresAt?: number }>; // Token refresh
81
+ }
82
+ ```
83
+
84
+ ### Methods
85
+
86
+ #### connect()
87
+
88
+ Connects to the server and authenticates.
89
+
90
+ ```typescript
91
+ await client.connect(): Promise<ScriptDBClient>
92
+ ```
93
+
94
+ Returns a promise that resolves when authentication is successful.
95
+
96
+ #### execute(command)
97
+
98
+ Executes a command on the server.
99
+
100
+ ```typescript
101
+ await client.execute(command: string): Promise<any>
102
+ ```
103
+
104
+ - `command` (string): The command to execute
105
+
106
+ Returns a promise that resolves with the response data.
107
+
108
+ #### close()
109
+
110
+ Closes the connection to the server.
111
+
112
+ ```typescript
113
+ client.close(): void
114
+ ```
115
+
116
+ #### destroy()
117
+
118
+ Destroys the client and cleans up resources.
119
+
120
+ ```typescript
121
+ client.destroy(): void
122
+ ```
123
+
124
+ ## Examples
125
+
126
+ ### Basic Usage
127
+
128
+ ```typescript
129
+ import { ScriptDBClient } from '@scriptdb/client';
130
+
131
+ const client = new ScriptDBClient('scriptdb://localhost:1234/testdb', {
132
+ username: 'admin',
133
+ password: 'password123'
134
+ });
135
+
136
+ try {
137
+ await client.connect();
138
+
139
+ // Execute database commands
140
+ const users = await client.execute('listUsers');
141
+ console.log('Users:', users);
142
+
143
+ const result = await client.execute('createUser', { name: 'John', email: 'john@example.com' });
144
+ console.log('Created user:', result);
145
+
146
+ } catch (error) {
147
+ console.error('Error:', error.message);
148
+ } finally {
149
+ client.close();
150
+ }
151
+ ```
152
+
153
+ ### Secure Connection with TLS
154
+
155
+ ```typescript
156
+ import { ScriptDBClient } from '@scriptdb/client';
157
+ import { readFileSync } from 'fs';
158
+
159
+ const client = new ScriptDBClient('scriptdb://secure.example.com:443/production', {
160
+ secure: true,
161
+ tlsOptions: {
162
+ ca: readFileSync('./ca-cert.pem'),
163
+ cert: readFileSync('./client-cert.pem'),
164
+ key: readFileSync('./client-key.pem'),
165
+ rejectUnauthorized: true
166
+ },
167
+ username: 'admin',
168
+ password: 'secure-password'
169
+ });
170
+
171
+ await client.connect();
172
+ ```
173
+
174
+ ### Custom Logging
175
+
176
+ ```typescript
177
+ import { ScriptDBClient } from '@scriptdb/client';
178
+
179
+ const client = new ScriptDBClient('scriptdb://localhost:1234/mydb', {
180
+ username: 'user',
181
+ password: 'pass',
182
+ logger: {
183
+ debug: (...args) => console.debug('[DEBUG]', ...args),
184
+ info: (...args) => console.info('[INFO]', ...args),
185
+ warn: (...args) => console.warn('[WARN]', ...args),
186
+ error: (...args) => console.error('[ERROR]', ...args)
187
+ }
188
+ });
189
+ ```
190
+
191
+ ### Message Signing
192
+
193
+ ```typescript
194
+ import { ScriptDBClient } from '@scriptdb/client';
195
+
196
+ const client = new ScriptDBClient('scriptdb://localhost:1234/mydb', {
197
+ username: 'user',
198
+ password: 'pass',
199
+ signing: {
200
+ secret: 'my-secret-key',
201
+ algorithm: 'sha256'
202
+ }
203
+ });
204
+ ```
205
+
206
+ ### Token Refresh
207
+
208
+ ```typescript
209
+ import { ScriptDBClient } from '@scriptdb/client';
210
+
211
+ const client = new ScriptDBClient('scriptdb://localhost:1234/mydb', {
212
+ username: 'user',
213
+ password: 'pass',
214
+ tokenRefresh: async () => {
215
+ // Implement your token refresh logic here
216
+ const response = await fetch('https://api.example.com/refresh-token', {
217
+ method: 'POST',
218
+ headers: { 'Authorization': 'Bearer ' + oldToken }
219
+ });
220
+ const data = await response.json();
221
+ return {
222
+ token: data.token,
223
+ expiresAt: data.expiresAt
224
+ };
225
+ }
226
+ });
227
+ ```
228
+
229
+ ## Error Handling
230
+
231
+ The client uses promises and will reject with errors for various failure cases:
232
+
233
+ ```typescript
234
+ try {
235
+ await client.connect();
236
+ } catch (error) {
237
+ if (error.message.includes('Authentication failed')) {
238
+ console.error('Invalid credentials');
239
+ } else if (error.message.includes('ECONNREFUSED')) {
240
+ console.error('Server not reachable');
241
+ } else {
242
+ console.error('Connection error:', error.message);
243
+ }
244
+ }
245
+ ```
246
+
247
+ ## Advanced Features
248
+
249
+ ### Connection Pooling
250
+
251
+ For high-throughput applications, you can create multiple client instances:
252
+
253
+ ```typescript
254
+ const clients = Array.from({ length: 5 }, () =>
255
+ new ScriptDBClient('scriptdb://localhost:1234/mydb', {
256
+ username: 'user',
257
+ password: 'pass'
258
+ })
259
+ );
260
+
261
+ // Connect all clients
262
+ await Promise.all(clients.map(client => client.connect()));
263
+
264
+ // Use round-robin or other strategy to distribute requests
265
+ let currentClient = 0;
266
+ function getClient() {
267
+ const client = clients[currentClient];
268
+ currentClient = (currentClient + 1) % clients.length;
269
+ return client;
270
+ }
271
+ ```
272
+
273
+ ### Request Queueing
274
+
275
+ The client automatically queues requests when the number of pending requests exceeds `maxPending`:
276
+
277
+ ```typescript
278
+ const client = new ScriptDBClient('scriptdb://localhost:1234/mydb', {
279
+ username: 'user',
280
+ password: 'pass',
281
+ maxPending: 10, // Max concurrent requests
282
+ maxQueue: 1000 // Max queued requests
283
+ });
284
+ ```
285
+
286
+ ## Development
287
+
288
+ ```bash
289
+ # Install dependencies
290
+ npm install
291
+
292
+ # Run tests
293
+ npm test
294
+
295
+ # Build the package
296
+ npm run build
297
+
298
+ # Run type checking
299
+ npm run typecheck
300
+
301
+ # Lint code
302
+ npm run lint
303
+ ```
304
+
305
+ ## License
306
+
307
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,694 @@
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
+ info: () => {},
9
+ warn: () => {},
10
+ error: () => {}
11
+ };
12
+
13
+ class ScriptDBClient {
14
+ options;
15
+ socketTimeout = 0;
16
+ maxMessageSize = 0;
17
+ _mask = () => {};
18
+ _maskArgs = () => [];
19
+ logger = {};
20
+ secure = true;
21
+ requestTimeout = 0;
22
+ retries = 0;
23
+ retryDelay = 0;
24
+ frame = "ndjson";
25
+ uri = "";
26
+ protocolName = "";
27
+ username = null;
28
+ password = null;
29
+ host = "";
30
+ port = 0;
31
+ database = null;
32
+ client = null;
33
+ buffer = Buffer.alloc(0);
34
+ _nextId = 1;
35
+ _pending = new Map;
36
+ _maxPending = 0;
37
+ _maxQueue = 0;
38
+ _pendingQueue = [];
39
+ _connected = false;
40
+ _authenticating = false;
41
+ token = null;
42
+ _currentRetries = 0;
43
+ tokenExpiry = null;
44
+ _destroyed = false;
45
+ _reconnectTimer = null;
46
+ signing = null;
47
+ _stringify = JSON.stringify;
48
+ ready = Promise.resolve();
49
+ _resolveReadyFn = null;
50
+ _rejectReadyFn = null;
51
+ _connecting = null;
52
+ _authPendingId = null;
53
+ constructor(uri, options = {}) {
54
+ if (!uri || typeof uri !== "string")
55
+ throw new Error("uri required");
56
+ this.options = Object.assign({}, options);
57
+ this.socketTimeout = Number.isFinite(this.options.socketTimeout) ? this.options.socketTimeout : 0;
58
+ this.maxMessageSize = Number.isFinite(this.options.maxMessageSize) ? this.options.maxMessageSize : 5 * 1024 * 1024;
59
+ this._mask = (obj) => {
60
+ try {
61
+ if (!obj || typeof obj !== "object")
62
+ return obj;
63
+ const copy = Array.isArray(obj) ? obj.slice() : Object.assign({}, obj);
64
+ if (copy.token)
65
+ copy.token = "****";
66
+ if (copy.password)
67
+ copy.password = "****";
68
+ if (copy.data && copy.data.password)
69
+ copy.data.password = "****";
70
+ return copy;
71
+ } catch (e) {
72
+ return obj;
73
+ }
74
+ };
75
+ const rawLogger = this.options.logger && typeof this.options.logger === "object" ? this.options.logger : noopLogger;
76
+ this._maskArgs = (args) => {
77
+ return args.map((a) => {
78
+ if (!a)
79
+ return a;
80
+ if (typeof a === "string")
81
+ return a;
82
+ return this._mask(a);
83
+ });
84
+ };
85
+ this.logger = {
86
+ debug: (...args) => rawLogger.debug && rawLogger.debug(...this._maskArgs(args)),
87
+ info: (...args) => rawLogger.info && rawLogger.info(...this._maskArgs(args)),
88
+ warn: (...args) => rawLogger.warn && rawLogger.warn(...this._maskArgs(args)),
89
+ error: (...args) => rawLogger.error && rawLogger.error(...this._maskArgs(args))
90
+ };
91
+ this.secure = typeof this.options.secure === "boolean" ? !!this.options.secure : true;
92
+ if (!this.secure)
93
+ this.logger.warn?.("Warning: connecting in insecure mode (secure=false). This is not recommended.");
94
+ this.requestTimeout = Number.isFinite(this.options.requestTimeout) ? this.options.requestTimeout : 0;
95
+ this.retries = Number.isFinite(this.options.retries) ? this.options.retries : 3;
96
+ this.retryDelay = Number.isFinite(this.options.retryDelay) ? this.options.retryDelay : 1000;
97
+ this.frame = this.options.frame === "length-prefix" || this.options.preferLengthPrefix ? "length-prefix" : "ndjson";
98
+ const normalized = uri.replace(/^https?:\/\//i, "scriptdb://");
99
+ let parsed;
100
+ try {
101
+ parsed = new URL(normalized);
102
+ } catch (e) {
103
+ throw new Error("Invalid uri");
104
+ }
105
+ this.uri = normalized;
106
+ this.protocolName = parsed.protocol ? parsed.protocol.replace(":", "") : "scriptdb";
107
+ this.username = (typeof this.options.username === "string" ? this.options.username : parsed.username) || null;
108
+ this.password = (typeof this.options.password === "string" ? this.options.password : parsed.password) || null;
109
+ if (parsed.username && !(typeof this.options.username === "string")) {
110
+ this.logger.warn?.("Credentials found in URI — consider passing credentials via options instead of embedding in URI");
111
+ }
112
+ try {
113
+ parsed.username = "";
114
+ parsed.password = "";
115
+ this.uri = parsed.toString();
116
+ } catch (e) {
117
+ this.uri = normalized;
118
+ }
119
+ this.host = parsed.hostname || "localhost";
120
+ this.port = parsed.port ? parseInt(parsed.port, 10) : 1234;
121
+ this.database = parsed.pathname && parsed.pathname.length > 1 ? parsed.pathname.slice(1) : null;
122
+ this.client = null;
123
+ this.buffer = Buffer.alloc(0);
124
+ this._nextId = 1;
125
+ this._pending = new Map;
126
+ this._maxPending = Number.isFinite(this.options.maxPending) ? this.options.maxPending : 100;
127
+ this._maxQueue = Number.isFinite(this.options.maxQueue) ? this.options.maxQueue : 1000;
128
+ this._pendingQueue = [];
129
+ this._connected = false;
130
+ this._authenticating = false;
131
+ this.token = null;
132
+ this._currentRetries = 0;
133
+ this.tokenExpiry = null;
134
+ this._destroyed = false;
135
+ this._reconnectTimer = null;
136
+ this.signing = this.options.signing && this.options.signing.secret ? this.options.signing : null;
137
+ this._stringify = typeof this.options.stringify === "function" ? this.options.stringify : JSON.stringify;
138
+ this._createReady();
139
+ }
140
+ _createReady() {
141
+ this.ready = new Promise((resolve, reject) => {
142
+ this._resolveReadyFn = resolve;
143
+ this._rejectReadyFn = reject;
144
+ });
145
+ }
146
+ _resolveReady(value) {
147
+ if (this._resolveReadyFn) {
148
+ try {
149
+ this._resolveReadyFn(value);
150
+ } catch (e) {}
151
+ this._resolveReadyFn = null;
152
+ this._rejectReadyFn = null;
153
+ }
154
+ }
155
+ _rejectReady(err) {
156
+ if (this._rejectReadyFn) {
157
+ const rejectFn = this._rejectReadyFn;
158
+ this._resolveReadyFn = null;
159
+ this._rejectReadyFn = null;
160
+ process.nextTick(() => {
161
+ try {
162
+ rejectFn(err);
163
+ } catch (e) {}
164
+ });
165
+ }
166
+ }
167
+ connect() {
168
+ if (this._connecting)
169
+ return this._connecting;
170
+ this._connecting = new Promise((resolve, reject) => {
171
+ const opts = { host: this.host, port: this.port };
172
+ const onConnect = () => {
173
+ console.log("ScriptDBClient: Connected to server at", opts.host, opts.port);
174
+ this.logger?.info?.("Connected to server");
175
+ this._connected = true;
176
+ this._currentRetries = 0;
177
+ console.log("ScriptDBClient: Setting up listeners...");
178
+ this._setupListeners();
179
+ console.log("ScriptDBClient: Authenticating...");
180
+ this.authenticate().then(() => {
181
+ this._processQueue();
182
+ resolve(this);
183
+ }).catch((err) => {
184
+ reject(err);
185
+ });
186
+ };
187
+ try {
188
+ if (this.secure) {
189
+ const connectionOpts = { host: opts.host, port: opts.port };
190
+ const tlsOptions = Object.assign({}, this.options.tlsOptions || {}, connectionOpts);
191
+ if (typeof tlsOptions.rejectUnauthorized === "undefined")
192
+ tlsOptions.rejectUnauthorized = true;
193
+ this.client = tls.connect(tlsOptions, onConnect);
194
+ } else {
195
+ this.client = net.createConnection(opts, onConnect);
196
+ }
197
+ } catch (e) {
198
+ const error = e;
199
+ this.logger?.error?.("Connection failed", error.message);
200
+ this._rejectReady(error);
201
+ this._createReady();
202
+ this._connecting = null;
203
+ return reject(error);
204
+ }
205
+ const onError = (err) => {
206
+ this.logger?.error?.("Client socket error:", err && err.message ? err.message : err);
207
+ this._handleDisconnect(err);
208
+ };
209
+ const onClose = (hadError) => {
210
+ this.logger?.info?.("Server closed connection");
211
+ this._handleDisconnect(null);
212
+ };
213
+ this.client.on("error", onError);
214
+ this.client.on("close", onClose);
215
+ if (this.socketTimeout > 0) {
216
+ this.client.setTimeout(this.socketTimeout);
217
+ this.client.on("timeout", () => {
218
+ this.logger?.warn?.("Socket timeout, destroying connection");
219
+ try {
220
+ this.client?.destroy();
221
+ } catch (e) {}
222
+ });
223
+ }
224
+ }).catch((err) => {
225
+ this._connecting = null;
226
+ throw err;
227
+ });
228
+ return this._connecting;
229
+ }
230
+ _setupListeners() {
231
+ if (!this.client)
232
+ return;
233
+ console.log("ScriptDBClient _setupListeners: called, client exists:", !!this.client);
234
+ this.client.removeAllListeners("data");
235
+ this.buffer = Buffer.alloc(0);
236
+ console.log("ScriptDBClient _setupListeners: frame mode:", this.frame);
237
+ if (this.frame === "length-prefix") {
238
+ this.client.on("data", (chunk) => {
239
+ if (!Buffer.isBuffer(chunk)) {
240
+ try {
241
+ chunk = Buffer.from(chunk);
242
+ } catch (e) {
243
+ return;
244
+ }
245
+ }
246
+ this.buffer = Buffer.concat([this.buffer, chunk]);
247
+ while (this.buffer.length >= 4) {
248
+ const len = this.buffer.readUInt32BE(0);
249
+ if (len > this.maxMessageSize) {
250
+ this.logger?.error?.("Incoming length-prefixed frame exceeds maxMessageSize — closing connection");
251
+ try {
252
+ this.client?.destroy();
253
+ } catch (e) {}
254
+ return;
255
+ }
256
+ if (this.buffer.length < 4 + len)
257
+ break;
258
+ const payload = this.buffer.slice(4, 4 + len);
259
+ this.buffer = this.buffer.slice(4 + len);
260
+ let msg;
261
+ try {
262
+ msg = JSON.parse(payload.toString("utf8"));
263
+ } catch (e) {
264
+ this.logger?.error?.("Invalid JSON frame", e && e.message ? e.message : e);
265
+ continue;
266
+ }
267
+ if (!msg || typeof msg !== "object")
268
+ continue;
269
+ 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);
270
+ if (!validSchema) {
271
+ this.logger?.warn?.("Message failed schema validation — ignoring");
272
+ continue;
273
+ }
274
+ this._handleMessage(msg);
275
+ }
276
+ });
277
+ } else {
278
+ console.log("ScriptDBClient _setupListeners: Setting up NDJSON data listener");
279
+ this.client.on("data", (chunk) => {
280
+ console.log("ScriptDBClient: Received data chunk, length:", chunk.length);
281
+ if (!Buffer.isBuffer(chunk)) {
282
+ try {
283
+ chunk = Buffer.from(chunk);
284
+ } catch (e) {
285
+ return;
286
+ }
287
+ }
288
+ const idxLastNewline = chunk.indexOf(10);
289
+ if (this.buffer.length === 0 && idxLastNewline === chunk.length - 1) {
290
+ let start = 0;
291
+ let idx2;
292
+ while ((idx2 = chunk.indexOf(10, start)) !== -1) {
293
+ const lineBuf = chunk.slice(start, idx2);
294
+ start = idx2 + 1;
295
+ if (lineBuf.length === 0)
296
+ continue;
297
+ let msg;
298
+ try {
299
+ msg = JSON.parse(lineBuf.toString("utf8"));
300
+ } catch (e) {
301
+ this.logger?.error?.("Invalid JSON from server", e && e.message ? e.message : e);
302
+ continue;
303
+ }
304
+ if (!msg || typeof msg !== "object")
305
+ continue;
306
+ 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");
307
+ if (!validSchema)
308
+ continue;
309
+ this._handleMessage(msg);
310
+ }
311
+ return;
312
+ }
313
+ this.buffer = Buffer.concat([this.buffer, chunk]);
314
+ if (this.buffer.length > this.maxMessageSize) {
315
+ this.logger?.error?.("Incoming message exceeds maxMessageSize — closing connection");
316
+ try {
317
+ this.client?.destroy();
318
+ } catch (e) {}
319
+ return;
320
+ }
321
+ let idx;
322
+ while ((idx = this.buffer.indexOf(10)) !== -1) {
323
+ const lineBuf = this.buffer.slice(0, idx);
324
+ this.buffer = this.buffer.slice(idx + 1);
325
+ if (lineBuf.length === 0)
326
+ continue;
327
+ let msg;
328
+ try {
329
+ msg = JSON.parse(lineBuf.toString("utf8"));
330
+ } catch (e) {
331
+ this.logger?.error?.("Invalid JSON from server", e && e.message ? e.message : e);
332
+ continue;
333
+ }
334
+ 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");
335
+ if (!validSchema) {
336
+ this.logger?.warn?.("Message failed schema validation — ignoring");
337
+ continue;
338
+ }
339
+ this._handleMessage(msg);
340
+ }
341
+ });
342
+ }
343
+ }
344
+ _buildFinalBuffer(payloadBase, id) {
345
+ const payloadObj = Object.assign({ id, action: payloadBase.action }, payloadBase.data !== undefined ? { data: payloadBase.data } : {});
346
+ if (this.token)
347
+ payloadObj.token = this.token;
348
+ const payloadStr = this._stringify(payloadObj);
349
+ if (this.signing && this.signing.secret) {
350
+ const hmac = crypto.createHmac(this.signing.algorithm || "sha256", this.signing.secret);
351
+ hmac.update(payloadStr);
352
+ const sig = hmac.digest("hex");
353
+ const envelope = { id, signature: sig, payload: payloadObj };
354
+ const envelopeStr = this._stringify(envelope);
355
+ if (this.frame === "length-prefix") {
356
+ const body = Buffer.from(envelopeStr, "utf8");
357
+ const buf = Buffer.allocUnsafe(4 + body.length);
358
+ buf.writeUInt32BE(body.length, 0);
359
+ body.copy(buf, 4);
360
+ return buf;
361
+ }
362
+ return Buffer.from(envelopeStr + `
363
+ `, "utf8");
364
+ }
365
+ if (this.frame === "length-prefix") {
366
+ const body = Buffer.from(payloadStr, "utf8");
367
+ const buf = Buffer.allocUnsafe(4 + body.length);
368
+ buf.writeUInt32BE(body.length, 0);
369
+ body.copy(buf, 4);
370
+ return buf;
371
+ }
372
+ return Buffer.from(payloadStr + `
373
+ `, "utf8");
374
+ }
375
+ _handleMessage(msg) {
376
+ console.log("ScriptDBClient _handleMessage:", JSON.stringify(msg));
377
+ console.log("ScriptDBClient _handleMessage msg.id:", msg.id, "msg.action:", msg.action, "msg.command:", msg.command, "msg.message:", msg.message);
378
+ if (msg && typeof msg.id !== "undefined") {
379
+ console.log("Handling message with id:", msg.id, "action:", msg.action, "message:", msg.message);
380
+ const pending = this._pending.get(msg.id);
381
+ if (!pending) {
382
+ console.log("No pending request for id", msg.id, "pending map size:", this._pending.size);
383
+ this.logger?.debug?.("No pending request for id", msg.id);
384
+ return;
385
+ }
386
+ const { resolve, reject, timer } = pending;
387
+ if (timer)
388
+ clearTimeout(timer);
389
+ this._pending.delete(msg.id);
390
+ this._processQueue();
391
+ if (msg.action === "login" || msg.command === "login") {
392
+ console.log("Processing login response with id");
393
+ if (msg.message === "AUTH OK") {
394
+ console.log("AUTH OK - setting token and resolving");
395
+ this.token = msg.data && msg.data.token ? msg.data.token : null;
396
+ this._resolveReady(null);
397
+ return resolve(msg.data);
398
+ } else {
399
+ console.log("AUTH FAILED:", msg.data);
400
+ this._rejectReady(new Error("Authentication failed"));
401
+ const errorMsg = msg.data || "Authentication failed";
402
+ try {
403
+ this.client?.end();
404
+ } catch (e) {}
405
+ return reject(new Error(typeof errorMsg === "string" ? errorMsg : "Authentication failed"));
406
+ }
407
+ }
408
+ if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "OK")
409
+ return resolve(msg.data);
410
+ if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "ERROR")
411
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Server returned ERROR"));
412
+ if (msg.action === "create-db" && msg.message === "SUCCESS")
413
+ return resolve(msg.data);
414
+ if (msg.action === "create-db" && msg.message === "ERROR")
415
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Failed to create database"));
416
+ if (msg.message === "OK" || msg.message === "SUCCESS") {
417
+ return resolve(msg.data);
418
+ } else if (msg.message === "ERROR") {
419
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Request failed"));
420
+ }
421
+ return reject(new Error("Invalid response from server"));
422
+ }
423
+ console.log("Unhandled message:", msg);
424
+ this.logger?.debug?.("Unhandled message from server", this._mask(msg));
425
+ }
426
+ authenticate() {
427
+ if (this._authenticating)
428
+ return Promise.reject(new Error("Already authenticating"));
429
+ this._authenticating = true;
430
+ return new Promise((resolve, reject) => {
431
+ const id = this._nextId++;
432
+ const payload = {
433
+ id,
434
+ action: "login",
435
+ data: { username: this.username, password: this.password }
436
+ };
437
+ let timer = null;
438
+ if (this.requestTimeout > 0) {
439
+ timer = setTimeout(() => {
440
+ this._pending.delete(id);
441
+ this._authenticating = false;
442
+ reject(new Error("Auth timeout"));
443
+ this.close();
444
+ }, this.requestTimeout);
445
+ }
446
+ this._pending.set(id, {
447
+ resolve: (data) => {
448
+ if (timer)
449
+ clearTimeout(timer);
450
+ this._authenticating = false;
451
+ if (data && data.token)
452
+ this.token = data.token;
453
+ resolve(data);
454
+ },
455
+ reject: (err) => {
456
+ if (timer)
457
+ clearTimeout(timer);
458
+ this._authenticating = false;
459
+ reject(err);
460
+ },
461
+ timer
462
+ });
463
+ try {
464
+ const buf = Buffer.from(JSON.stringify(payload) + `
465
+ `, "utf8");
466
+ this._write(buf).catch((err) => {
467
+ if (timer)
468
+ clearTimeout(timer);
469
+ this._pending.delete(id);
470
+ this._authenticating = false;
471
+ reject(err);
472
+ });
473
+ } catch (e) {
474
+ clearTimeout(timer);
475
+ this._pending.delete(id);
476
+ this._authenticating = false;
477
+ reject(e);
478
+ }
479
+ }).then((data) => {
480
+ if (data && data.token) {
481
+ this.token = data.token;
482
+ this._resolveReady(null);
483
+ }
484
+ return data;
485
+ }).catch((err) => {
486
+ this._rejectReady(err);
487
+ throw err;
488
+ });
489
+ }
490
+ _write(buf) {
491
+ return new Promise((resolve, reject) => {
492
+ if (!this.client || !this._connected)
493
+ return reject(new Error("Not connected"));
494
+ try {
495
+ const ok = this.client.write(buf, (err) => {
496
+ if (err)
497
+ return reject(err);
498
+ resolve(undefined);
499
+ });
500
+ if (!ok) {
501
+ this.client.once("drain", () => resolve(undefined));
502
+ }
503
+ } catch (e) {
504
+ reject(e);
505
+ }
506
+ });
507
+ }
508
+ async _processQueue() {
509
+ while (this._pending.size < this._maxPending && this._pendingQueue.length > 0) {
510
+ const item = this._pendingQueue.shift();
511
+ const { payloadBase, id, resolve, reject, timer } = item;
512
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
513
+ const refreshed = await this._maybeRefreshToken();
514
+ if (!refreshed) {
515
+ clearTimeout(timer);
516
+ try {
517
+ reject(new Error("Token expired and refresh failed"));
518
+ } catch (e) {}
519
+ continue;
520
+ }
521
+ }
522
+ let buf;
523
+ try {
524
+ buf = this._buildFinalBuffer(payloadBase, id);
525
+ } catch (e) {
526
+ clearTimeout(timer);
527
+ try {
528
+ reject(e);
529
+ } catch (er) {}
530
+ continue;
531
+ }
532
+ this._pending.set(id, { resolve, reject, timer });
533
+ try {
534
+ await this._write(buf);
535
+ } catch (e) {
536
+ clearTimeout(timer);
537
+ this._pending.delete(id);
538
+ try {
539
+ reject(e);
540
+ } catch (er) {}
541
+ }
542
+ }
543
+ }
544
+ async execute(payload) {
545
+ try {
546
+ await this.ready;
547
+ } catch (err) {
548
+ throw new Error("Not authenticated: " + (err && err.message ? err.message : err));
549
+ }
550
+ if (!this.token)
551
+ throw new Error("Not authenticated");
552
+ const id = this._nextId++;
553
+ const payloadBase = { action: payload.action, data: payload.data };
554
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
555
+ const refreshed = await this._maybeRefreshToken();
556
+ if (!refreshed)
557
+ throw new Error("Token expired");
558
+ }
559
+ return new Promise((resolve, reject) => {
560
+ let timer = null;
561
+ if (this.requestTimeout > 0) {
562
+ timer = setTimeout(() => {
563
+ if (this._pending.has(id)) {
564
+ this._pending.delete(id);
565
+ reject(new Error("Request timeout"));
566
+ }
567
+ }, this.requestTimeout);
568
+ }
569
+ if (this._pending.size >= this._maxPending) {
570
+ if (this._pendingQueue.length >= this._maxQueue) {
571
+ if (timer)
572
+ clearTimeout(timer);
573
+ return reject(new Error("Pending queue full"));
574
+ }
575
+ this._pendingQueue.push({ payloadBase, id, resolve, reject, timer });
576
+ this._processQueue().catch(() => {});
577
+ return;
578
+ }
579
+ let finalBuf;
580
+ try {
581
+ finalBuf = this._buildFinalBuffer(payloadBase, id);
582
+ } catch (e) {
583
+ clearTimeout(timer);
584
+ return reject(e);
585
+ }
586
+ this._pending.set(id, { resolve, reject, timer });
587
+ this._write(finalBuf).catch((e) => {
588
+ if (timer)
589
+ clearTimeout(timer);
590
+ this._pending.delete(id);
591
+ reject(e);
592
+ });
593
+ });
594
+ }
595
+ async _maybeRefreshToken() {
596
+ if (!this.options.tokenRefresh || typeof this.options.tokenRefresh !== "function")
597
+ return false;
598
+ try {
599
+ const res = await this.options.tokenRefresh();
600
+ if (res && res.token) {
601
+ this.token = res.token;
602
+ if (res.expiresAt)
603
+ this.tokenExpiry = res.expiresAt;
604
+ return true;
605
+ }
606
+ } catch (e) {
607
+ this.logger?.error?.("Token refresh failed", e && e.message ? e.message : String(e));
608
+ }
609
+ return false;
610
+ }
611
+ destroy() {
612
+ this._destroyed = true;
613
+ if (this._reconnectTimer) {
614
+ clearTimeout(this._reconnectTimer);
615
+ this._reconnectTimer = null;
616
+ }
617
+ this._rejectReady(new Error("Client destroyed"));
618
+ this._pending.forEach((pending, id) => {
619
+ if (pending.timer)
620
+ clearTimeout(pending.timer);
621
+ try {
622
+ pending.reject(new Error("Client destroyed"));
623
+ } catch (e) {}
624
+ this._pending.delete(id);
625
+ });
626
+ this._pendingQueue = [];
627
+ try {
628
+ if (this.client)
629
+ this.client.destroy();
630
+ } catch (e) {}
631
+ this.client = null;
632
+ }
633
+ _handleDisconnect(err) {
634
+ if (!this._connected && !this._authenticating) {
635
+ return;
636
+ }
637
+ const wasAuthenticating = this._authenticating;
638
+ this._pending.forEach((pending, id) => {
639
+ if (pending.timer)
640
+ clearTimeout(pending.timer);
641
+ const errorMsg = wasAuthenticating ? "Authentication failed - credentials may be required" : err && err.message ? err.message : "Disconnected";
642
+ process.nextTick(() => {
643
+ try {
644
+ pending.reject(new Error(errorMsg));
645
+ } catch (e) {}
646
+ });
647
+ this._pending.delete(id);
648
+ });
649
+ this._connected = false;
650
+ this._connecting = null;
651
+ this._authenticating = false;
652
+ if (wasAuthenticating && this.retries === 0) {
653
+ const rejectErr = err || new Error("Authentication failed and no retries configured");
654
+ try {
655
+ this._rejectReady(rejectErr);
656
+ } catch (e) {}
657
+ return;
658
+ }
659
+ if (this._currentRetries < this.retries) {
660
+ this._createReady();
661
+ const base = Math.min(this.retryDelay * Math.pow(2, this._currentRetries), 30000);
662
+ const jitter = Math.floor(Math.random() * Math.min(1000, base));
663
+ const delay = base + jitter;
664
+ this._currentRetries += 1;
665
+ this.logger?.info?.("Attempting reconnect in " + delay + "ms (attempt " + this._currentRetries + ")");
666
+ this.token = null;
667
+ setTimeout(() => {
668
+ this.connect().catch((e) => {
669
+ this.logger?.error?.("Reconnect failed", e && e.message ? e.message : e);
670
+ });
671
+ }, delay);
672
+ } else {
673
+ try {
674
+ this._rejectReady(err || new Error("Disconnected and no retries left"));
675
+ } catch (e) {}
676
+ }
677
+ }
678
+ close() {
679
+ if (!this.client)
680
+ return;
681
+ try {
682
+ this.client.removeAllListeners("data");
683
+ this.client.removeAllListeners("error");
684
+ this.client.removeAllListeners("close");
685
+ this.client.end();
686
+ } catch (e) {}
687
+ this.client = null;
688
+ }
689
+ }
690
+ var src_default = ScriptDBClient;
691
+ export {
692
+ src_default as default,
693
+ ScriptDBClient
694
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@scriptdb/client",
3
+ "version": "1.0.0",
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
+ "node": "^25.2.0"
49
+ }
50
+ }