@joystick.js/db-canary 0.0.0-canary.2251 → 0.0.0-canary.2253

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 (97) hide show
  1. package/dist/client/database.js +1 -1
  2. package/dist/client/index.js +1 -1
  3. package/dist/server/cluster/master.js +4 -4
  4. package/dist/server/cluster/worker.js +1 -1
  5. package/dist/server/index.js +1 -1
  6. package/dist/server/lib/auto_index_manager.js +1 -1
  7. package/dist/server/lib/backup_manager.js +1 -1
  8. package/dist/server/lib/index_manager.js +1 -1
  9. package/dist/server/lib/operation_dispatcher.js +1 -1
  10. package/dist/server/lib/operations/admin.js +1 -1
  11. package/dist/server/lib/operations/bulk_write.js +1 -1
  12. package/dist/server/lib/operations/create_index.js +1 -1
  13. package/dist/server/lib/operations/delete_many.js +1 -1
  14. package/dist/server/lib/operations/delete_one.js +1 -1
  15. package/dist/server/lib/operations/find.js +1 -1
  16. package/dist/server/lib/operations/find_one.js +1 -1
  17. package/dist/server/lib/operations/insert_one.js +1 -1
  18. package/dist/server/lib/operations/update_one.js +1 -1
  19. package/dist/server/lib/send_response.js +1 -1
  20. package/dist/server/lib/tcp_protocol.js +1 -1
  21. package/package.json +2 -2
  22. package/src/client/database.js +159 -133
  23. package/src/client/index.js +285 -346
  24. package/src/server/cluster/master.js +265 -156
  25. package/src/server/cluster/worker.js +26 -18
  26. package/src/server/index.js +553 -330
  27. package/src/server/lib/auto_index_manager.js +85 -23
  28. package/src/server/lib/backup_manager.js +117 -70
  29. package/src/server/lib/index_manager.js +63 -25
  30. package/src/server/lib/operation_dispatcher.js +339 -168
  31. package/src/server/lib/operations/admin.js +343 -205
  32. package/src/server/lib/operations/bulk_write.js +458 -194
  33. package/src/server/lib/operations/create_index.js +127 -34
  34. package/src/server/lib/operations/delete_many.js +204 -67
  35. package/src/server/lib/operations/delete_one.js +164 -52
  36. package/src/server/lib/operations/find.js +552 -319
  37. package/src/server/lib/operations/find_one.js +530 -304
  38. package/src/server/lib/operations/insert_one.js +147 -52
  39. package/src/server/lib/operations/update_one.js +334 -93
  40. package/src/server/lib/send_response.js +37 -17
  41. package/src/server/lib/tcp_protocol.js +158 -53
  42. package/test_data_api_key_1758233848259_cglfjzhou/data.mdb +0 -0
  43. package/test_data_api_key_1758233848259_cglfjzhou/lock.mdb +0 -0
  44. package/test_data_api_key_1758233848502_urlje2utd/data.mdb +0 -0
  45. package/test_data_api_key_1758233848502_urlje2utd/lock.mdb +0 -0
  46. package/test_data_api_key_1758233848738_mtcpfe5ns/data.mdb +0 -0
  47. package/test_data_api_key_1758233848738_mtcpfe5ns/lock.mdb +0 -0
  48. package/test_data_api_key_1758233848856_9g97p6gag/data.mdb +0 -0
  49. package/test_data_api_key_1758233848856_9g97p6gag/lock.mdb +0 -0
  50. package/test_data_api_key_1758233857008_0tl9zzhj8/data.mdb +0 -0
  51. package/test_data_api_key_1758233857008_0tl9zzhj8/lock.mdb +0 -0
  52. package/test_data_api_key_1758233857120_60c2f2uhu/data.mdb +0 -0
  53. package/test_data_api_key_1758233857120_60c2f2uhu/lock.mdb +0 -0
  54. package/test_data_api_key_1758233857232_aw7fkqgd9/data.mdb +0 -0
  55. package/test_data_api_key_1758233857232_aw7fkqgd9/lock.mdb +0 -0
  56. package/test_data_api_key_1758234881285_4aeflubjb/data.mdb +0 -0
  57. package/test_data_api_key_1758234881285_4aeflubjb/lock.mdb +0 -0
  58. package/test_data_api_key_1758234881520_kb0amvtqb/data.mdb +0 -0
  59. package/test_data_api_key_1758234881520_kb0amvtqb/lock.mdb +0 -0
  60. package/test_data_api_key_1758234881756_k04gfv2va/data.mdb +0 -0
  61. package/test_data_api_key_1758234881756_k04gfv2va/lock.mdb +0 -0
  62. package/test_data_api_key_1758234881876_wn90dpo1z/data.mdb +0 -0
  63. package/test_data_api_key_1758234881876_wn90dpo1z/lock.mdb +0 -0
  64. package/test_data_api_key_1758234889461_26xz3dmbr/data.mdb +0 -0
  65. package/test_data_api_key_1758234889461_26xz3dmbr/lock.mdb +0 -0
  66. package/test_data_api_key_1758234889572_uziz7e0p5/data.mdb +0 -0
  67. package/test_data_api_key_1758234889572_uziz7e0p5/lock.mdb +0 -0
  68. package/test_data_api_key_1758234889684_5f9wmposh/data.mdb +0 -0
  69. package/test_data_api_key_1758234889684_5f9wmposh/lock.mdb +0 -0
  70. package/test_data_api_key_1758235657729_prwgm6mxr/data.mdb +0 -0
  71. package/test_data_api_key_1758235657729_prwgm6mxr/lock.mdb +0 -0
  72. package/test_data_api_key_1758235657961_rc2da0dc2/data.mdb +0 -0
  73. package/test_data_api_key_1758235657961_rc2da0dc2/lock.mdb +0 -0
  74. package/test_data_api_key_1758235658193_oqqxm0sny/data.mdb +0 -0
  75. package/test_data_api_key_1758235658193_oqqxm0sny/lock.mdb +0 -0
  76. package/test_data_api_key_1758235658309_vggac1pj6/data.mdb +0 -0
  77. package/test_data_api_key_1758235658309_vggac1pj6/lock.mdb +0 -0
  78. package/test_data_api_key_1758235665968_61ko07dd1/data.mdb +0 -0
  79. package/test_data_api_key_1758235665968_61ko07dd1/lock.mdb +0 -0
  80. package/test_data_api_key_1758235666082_50lrt6sq8/data.mdb +0 -0
  81. package/test_data_api_key_1758235666082_50lrt6sq8/lock.mdb +0 -0
  82. package/test_data_api_key_1758235666194_ykvauwlzh/data.mdb +0 -0
  83. package/test_data_api_key_1758235666194_ykvauwlzh/lock.mdb +0 -0
  84. package/test_data_api_key_1758236187207_9c4paeh09/data.mdb +0 -0
  85. package/test_data_api_key_1758236187207_9c4paeh09/lock.mdb +0 -0
  86. package/test_data_api_key_1758236187441_4n3o3gkkl/data.mdb +0 -0
  87. package/test_data_api_key_1758236187441_4n3o3gkkl/lock.mdb +0 -0
  88. package/test_data_api_key_1758236187672_jt6b21ye0/data.mdb +0 -0
  89. package/test_data_api_key_1758236187672_jt6b21ye0/lock.mdb +0 -0
  90. package/test_data_api_key_1758236187788_oo84fz9u6/data.mdb +0 -0
  91. package/test_data_api_key_1758236187788_oo84fz9u6/lock.mdb +0 -0
  92. package/test_data_api_key_1758236195507_o9zeznwlm/data.mdb +0 -0
  93. package/test_data_api_key_1758236195507_o9zeznwlm/lock.mdb +0 -0
  94. package/test_data_api_key_1758236195619_qsqd60y41/data.mdb +0 -0
  95. package/test_data_api_key_1758236195619_qsqd60y41/lock.mdb +0 -0
  96. package/test_data_api_key_1758236195731_im13iq284/data.mdb +0 -0
  97. package/test_data_api_key_1758236195731_im13iq284/lock.mdb +0 -0
@@ -3,23 +3,61 @@ import { EventEmitter } from 'events';
3
3
  import { encode as encode_messagepack, decode as decode_messagepack } from 'msgpackr';
4
4
  import Database from './database.js';
5
5
 
6
+ /**
7
+ * Creates MessagePack encoding options for consistent serialization.
8
+ * @returns {Object} MessagePack encoding options
9
+ */
10
+ const create_messagepack_options = () => ({
11
+ useFloat32: false,
12
+ int64AsType: 'number',
13
+ mapsAsObjects: true
14
+ });
15
+
6
16
  /**
7
17
  * Encodes a message with MessagePack and prepends a 4-byte length header.
8
18
  * @param {any} data - The data to encode
9
19
  * @returns {Buffer} The encoded message with length header
10
20
  */
11
21
  const encode_message = (data) => {
12
- // NOTE: Use compatible MessagePack options to avoid parsing issues.
13
- const messagepack_data = encode_messagepack(data, {
14
- useFloat32: false,
15
- int64AsType: 'number',
16
- mapsAsObjects: true
17
- });
22
+ const messagepack_data = encode_messagepack(data, create_messagepack_options());
18
23
  const length_buffer = Buffer.allocUnsafe(4);
19
24
  length_buffer.writeUInt32BE(messagepack_data.length, 0);
20
25
  return Buffer.concat([length_buffer, messagepack_data]);
21
26
  };
22
27
 
28
+ /**
29
+ * Extracts complete message from buffer when enough data is available.
30
+ * @param {Buffer} buffer - Current buffer
31
+ * @param {number} expected_length - Expected message length
32
+ * @returns {Object} Result with message data and remaining buffer
33
+ */
34
+ const extract_complete_message = (buffer, expected_length) => {
35
+ const message_data = buffer.slice(0, expected_length);
36
+ const remaining_buffer = buffer.slice(expected_length);
37
+
38
+ try {
39
+ const decoded_message = decode_messagepack(message_data, create_messagepack_options());
40
+ return { message: decoded_message, buffer: remaining_buffer };
41
+ } catch (error) {
42
+ throw new Error(`Invalid message format: ${error.message}`);
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Reads length header from buffer if available.
48
+ * @param {Buffer} buffer - Current buffer
49
+ * @returns {Object} Result with expected length and remaining buffer
50
+ */
51
+ const read_length_header = (buffer) => {
52
+ if (buffer.length < 4) {
53
+ return { expected_length: null, buffer };
54
+ }
55
+
56
+ const expected_length = buffer.readUInt32BE(0);
57
+ const remaining_buffer = buffer.slice(4);
58
+ return { expected_length, buffer: remaining_buffer };
59
+ };
60
+
23
61
  /**
24
62
  * Creates a message parser for handling TCP stream data with length-prefixed MessagePack messages.
25
63
  * @returns {Object} Parser object with parse_messages and reset methods
@@ -28,64 +66,143 @@ const create_message_parser = () => {
28
66
  let buffer = Buffer.alloc(0);
29
67
  let expected_length = null;
30
68
 
31
- /**
32
- * Parses incoming data and extracts complete messages.
33
- * @param {Buffer} data - Raw TCP data
34
- * @returns {Array} Array of decoded messages
35
- * @throws {Error} When message format is invalid
36
- */
37
69
  const parse_messages = (data) => {
38
70
  buffer = Buffer.concat([buffer, data]);
39
71
  const messages = [];
40
72
 
41
73
  while (buffer.length > 0) {
42
74
  if (expected_length === null) {
43
- if (buffer.length < 4) {
75
+ const header_result = read_length_header(buffer);
76
+ expected_length = header_result.expected_length;
77
+ buffer = header_result.buffer;
78
+
79
+ if (expected_length === null) {
44
80
  break;
45
81
  }
46
-
47
- expected_length = buffer.readUInt32BE(0);
48
- buffer = buffer.slice(4);
49
82
  }
50
83
 
51
84
  if (buffer.length < expected_length) {
52
85
  break;
53
86
  }
54
87
 
55
- const message_data = buffer.slice(0, expected_length);
56
- buffer = buffer.slice(expected_length);
88
+ const message_result = extract_complete_message(buffer, expected_length);
89
+ messages.push(message_result.message);
90
+ buffer = message_result.buffer;
57
91
  expected_length = null;
58
-
59
- try {
60
- // NOTE: Use compatible MessagePack options to avoid parsing issues.
61
- const decoded_message = decode_messagepack(message_data, {
62
- useFloat32: false,
63
- int64AsType: 'number',
64
- mapsAsObjects: true
65
- });
66
- messages.push(decoded_message);
67
- } catch (error) {
68
- throw new Error(`Invalid message format: ${error.message}`);
69
- }
70
92
  }
71
93
 
72
94
  return messages;
73
95
  };
74
96
 
75
- /**
76
- * Resets the parser state, clearing buffers and expected length.
77
- */
78
97
  const reset = () => {
79
98
  buffer = Buffer.alloc(0);
80
99
  expected_length = null;
81
100
  };
82
101
 
83
- return {
84
- parse_messages,
85
- reset
86
- };
102
+ return { parse_messages, reset };
87
103
  };
88
104
 
105
+ /**
106
+ * Calculates exponential backoff delay with maximum limit.
107
+ * @param {number} attempt_number - Current attempt number
108
+ * @param {number} base_delay - Base delay in milliseconds
109
+ * @returns {number} Calculated delay in milliseconds
110
+ */
111
+ const calculate_reconnect_delay = (attempt_number, base_delay) => {
112
+ return Math.min(base_delay * Math.pow(2, attempt_number - 1), 30000);
113
+ };
114
+
115
+ /**
116
+ * Creates default client options with fallback values.
117
+ * @param {Object} options - User provided options
118
+ * @returns {Object} Complete options object with defaults
119
+ */
120
+ const create_client_options = (options = {}) => ({
121
+ host: options.host || 'localhost',
122
+ port: options.port || 1983,
123
+ password: options.password || null,
124
+ timeout: options.timeout || 5000,
125
+ reconnect: options.reconnect !== false,
126
+ max_reconnect_attempts: options.max_reconnect_attempts || 10,
127
+ reconnect_delay: options.reconnect_delay || 1000,
128
+ auto_connect: options.auto_connect !== false
129
+ });
130
+
131
+ /**
132
+ * Creates a connection timeout handler.
133
+ * @param {net.Socket} socket - Socket connection
134
+ * @param {Function} error_handler - Error handling function
135
+ * @param {number} timeout_ms - Timeout duration in milliseconds
136
+ * @returns {NodeJS.Timeout} Timeout reference
137
+ */
138
+ const create_connection_timeout = (socket, error_handler, timeout_ms) => {
139
+ return setTimeout(() => {
140
+ if (socket && !socket.destroyed) {
141
+ socket.destroy();
142
+ error_handler(new Error('Connection timeout'));
143
+ }
144
+ }, timeout_ms);
145
+ };
146
+
147
+ /**
148
+ * Creates a request timeout handler.
149
+ * @param {Map} pending_requests - Map of pending requests
150
+ * @param {number} request_id - Request identifier
151
+ * @param {number} timeout_ms - Timeout duration in milliseconds
152
+ * @returns {NodeJS.Timeout} Timeout reference
153
+ */
154
+ const create_request_timeout = (pending_requests, request_id, timeout_ms) => {
155
+ return setTimeout(() => {
156
+ const request = pending_requests.get(request_id);
157
+ if (request) {
158
+ pending_requests.delete(request_id);
159
+ request.reject(new Error('Request timeout'));
160
+ }
161
+ }, timeout_ms);
162
+ };
163
+
164
+ /**
165
+ * Clears all pending requests with error.
166
+ * @param {Map} pending_requests - Map of pending requests
167
+ * @param {string} error_message - Error message to send
168
+ */
169
+ const clear_pending_requests = (pending_requests, error_message) => {
170
+ for (const [request_id, { reject, timeout }] of pending_requests) {
171
+ clearTimeout(timeout);
172
+ reject(new Error(error_message));
173
+ }
174
+ pending_requests.clear();
175
+ };
176
+
177
+ /**
178
+ * Determines if response indicates success.
179
+ * @param {Object} message - Server response message
180
+ * @returns {boolean} True if response indicates success
181
+ */
182
+ const is_successful_response = (message) => {
183
+ return message.ok === 1 || message.ok === true;
184
+ };
185
+
186
+ /**
187
+ * Determines if response indicates failure.
188
+ * @param {Object} message - Server response message
189
+ * @returns {boolean} True if response indicates failure
190
+ */
191
+ const is_error_response = (message) => {
192
+ return message.ok === 0 || message.ok === false;
193
+ };
194
+
195
+ /**
196
+ * Extracts error message from server response.
197
+ * @param {Object} message - Server response message
198
+ * @returns {string} Error message
199
+ */
200
+ const extract_error_message = (message) => {
201
+ if (typeof message.error === 'string') {
202
+ return message.error;
203
+ }
204
+ return JSON.stringify(message.error) || 'Operation failed';
205
+ };
89
206
 
90
207
  /**
91
208
  * @typedef {Object} ClientOptions
@@ -110,20 +227,17 @@ const create_message_parser = () => {
110
227
  * @fires JoystickDBClient#response
111
228
  */
112
229
  class JoystickDBClient extends EventEmitter {
113
- /**
114
- * Creates a new JoystickDB client instance.
115
- * @param {ClientOptions} [options={}] - Client configuration options
116
- */
117
230
  constructor(options = {}) {
118
231
  super();
119
232
 
120
- this.host = options.host || 'localhost';
121
- this.port = options.port || 1983;
122
- this.password = options.password || null;
123
- this.timeout = options.timeout || 5000;
124
- this.reconnect = options.reconnect !== false;
125
- this.max_reconnect_attempts = options.max_reconnect_attempts || 10;
126
- this.reconnect_delay = options.reconnect_delay || 1000;
233
+ const client_options = create_client_options(options);
234
+ this.host = client_options.host;
235
+ this.port = client_options.port;
236
+ this.password = client_options.password;
237
+ this.timeout = client_options.timeout;
238
+ this.reconnect = client_options.reconnect;
239
+ this.max_reconnect_attempts = client_options.max_reconnect_attempts;
240
+ this.reconnect_delay = client_options.reconnect_delay;
127
241
 
128
242
  this.socket = null;
129
243
  this.message_parser = null;
@@ -137,14 +251,11 @@ class JoystickDBClient extends EventEmitter {
137
251
  this.request_id_counter = 0;
138
252
  this.request_queue = [];
139
253
 
140
- if (options.auto_connect !== false) {
254
+ if (client_options.auto_connect) {
141
255
  this.connect();
142
256
  }
143
257
  }
144
258
 
145
- /**
146
- * Establishes connection to the JoystickDB server.
147
- */
148
259
  connect() {
149
260
  if (this.is_connecting || this.is_connected) {
150
261
  return;
@@ -154,41 +265,25 @@ class JoystickDBClient extends EventEmitter {
154
265
  this.socket = new net.Socket();
155
266
  this.message_parser = create_message_parser();
156
267
 
157
- const connection_timeout = setTimeout(() => {
158
- if (this.socket && !this.is_connected) {
159
- this.socket.destroy();
160
- this.handle_connection_error(new Error('Connection timeout'));
161
- }
162
- }, this.timeout);
268
+ const connection_timeout = create_connection_timeout(
269
+ this.socket,
270
+ this.handle_connection_error.bind(this),
271
+ this.timeout
272
+ );
163
273
 
274
+ this.setup_socket_handlers(connection_timeout);
164
275
  this.socket.connect(this.port, this.host, () => {
165
- clearTimeout(connection_timeout);
166
- this.is_connected = true;
167
- this.is_connecting = false;
168
- this.reconnect_attempts = 0;
169
-
170
- this.emit('connect');
171
-
172
- if (this.password) {
173
- this.authenticate();
174
- } else {
175
- // NOTE: If no password provided, assume development mode and skip authentication.
176
- this.is_authenticated = true;
177
- this.emit('authenticated');
178
- this.process_request_queue();
179
- }
276
+ this.handle_successful_connection(connection_timeout);
180
277
  });
181
-
278
+ }
279
+
280
+ /**
281
+ * Sets up socket event handlers.
282
+ * @param {NodeJS.Timeout} connection_timeout - Connection timeout reference
283
+ */
284
+ setup_socket_handlers(connection_timeout) {
182
285
  this.socket.on('data', (data) => {
183
- try {
184
- const messages = this.message_parser.parse_messages(data);
185
-
186
- for (const message of messages) {
187
- this.handle_message(message);
188
- }
189
- } catch (error) {
190
- this.emit('error', new Error(`Message parsing failed: ${error.message}`));
191
- }
286
+ this.handle_incoming_data(data);
192
287
  });
193
288
 
194
289
  this.socket.on('error', (error) => {
@@ -203,8 +298,49 @@ class JoystickDBClient extends EventEmitter {
203
298
  }
204
299
 
205
300
  /**
206
- * Authenticates with the server using provided credentials.
301
+ * Handles successful socket connection.
302
+ * @param {NodeJS.Timeout} connection_timeout - Connection timeout reference
303
+ */
304
+ handle_successful_connection(connection_timeout) {
305
+ clearTimeout(connection_timeout);
306
+ this.is_connected = true;
307
+ this.is_connecting = false;
308
+ this.reconnect_attempts = 0;
309
+
310
+ this.emit('connect');
311
+
312
+ if (this.password) {
313
+ this.authenticate();
314
+ } else {
315
+ this.handle_authentication_complete();
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Handles completion of authentication process.
321
+ */
322
+ handle_authentication_complete() {
323
+ this.is_authenticated = true;
324
+ this.emit('authenticated');
325
+ this.process_request_queue();
326
+ }
327
+
328
+ /**
329
+ * Handles incoming data from socket.
330
+ * @param {Buffer} data - Raw TCP data
207
331
  */
332
+ handle_incoming_data(data) {
333
+ try {
334
+ const messages = this.message_parser.parse_messages(data);
335
+
336
+ for (const message of messages) {
337
+ this.handle_message(message);
338
+ }
339
+ } catch (error) {
340
+ this.emit('error', new Error(`Message parsing failed: ${error.message}`));
341
+ }
342
+ }
343
+
208
344
  async authenticate() {
209
345
  if (!this.password) {
210
346
  this.emit('error', new Error('Password required for authentication. Provide password in client options: joystickdb.client({ password: "your_password" })'));
@@ -218,9 +354,7 @@ class JoystickDBClient extends EventEmitter {
218
354
  });
219
355
 
220
356
  if (result.ok === 1) {
221
- this.is_authenticated = true;
222
- this.emit('authenticated');
223
- this.process_request_queue();
357
+ this.handle_authentication_complete();
224
358
  } else {
225
359
  throw new Error('Authentication failed');
226
360
  }
@@ -230,58 +364,51 @@ class JoystickDBClient extends EventEmitter {
230
364
  }
231
365
  }
232
366
 
233
- /**
234
- * Handles incoming messages from the server.
235
- * @param {Object} message - Decoded message from server
236
- */
237
367
  handle_message(message) {
238
368
  if (this.pending_requests.size > 0) {
239
- const [request_id, { resolve, reject, timeout }] = this.pending_requests.entries().next().value;
240
- clearTimeout(timeout);
241
- this.pending_requests.delete(request_id);
242
-
243
- if (message.ok === 1 || message.ok === true) {
244
- resolve(message);
245
- } else if (message.ok === 0 || message.ok === false) {
246
- const error_message = typeof message.error === 'string' ? message.error : JSON.stringify(message.error) || 'Operation failed';
247
- reject(new Error(error_message));
248
- } else {
249
- resolve(message);
250
- }
369
+ this.handle_pending_request_response(message);
251
370
  } else {
252
371
  this.emit('response', message);
253
372
  }
254
373
  }
255
374
 
256
375
  /**
257
- * Handles connection errors and manages reconnection logic.
258
- * @param {Error} error - The connection error
376
+ * Handles response for pending request.
377
+ * @param {Object} message - Server response message
259
378
  */
260
- handle_connection_error(error) {
261
- this.is_connecting = false;
262
- this.is_connected = false;
263
- this.is_authenticated = false;
379
+ handle_pending_request_response(message) {
380
+ const [request_id, { resolve, reject, timeout }] = this.pending_requests.entries().next().value;
381
+ clearTimeout(timeout);
382
+ this.pending_requests.delete(request_id);
264
383
 
265
- if (this.socket) {
266
- this.socket.removeAllListeners();
267
- this.socket.destroy();
268
- this.socket = null;
384
+ if (is_successful_response(message)) {
385
+ resolve(message);
386
+ } else if (is_error_response(message)) {
387
+ const error_message = extract_error_message(message);
388
+ reject(new Error(error_message));
389
+ } else {
390
+ resolve(message);
269
391
  }
392
+ }
393
+
394
+ handle_connection_error(error) {
395
+ this.reset_connection_state();
396
+ clear_pending_requests(this.pending_requests, 'Connection lost');
270
397
 
271
- if (this.message_parser) {
272
- this.message_parser.reset();
273
- }
398
+ this.emit('error', error);
274
399
 
275
- // NOTE: Reject all pending requests.
276
- for (const [request_id, { reject, timeout }] of this.pending_requests) {
277
- clearTimeout(timeout);
278
- reject(new Error('Connection lost'));
400
+ if (this.should_attempt_reconnect()) {
401
+ this.schedule_reconnect();
402
+ } else {
403
+ this.emit('disconnect');
279
404
  }
280
- this.pending_requests.clear();
281
-
282
- this.emit('error', error);
405
+ }
406
+
407
+ handle_disconnect() {
408
+ this.reset_connection_state();
409
+ clear_pending_requests(this.pending_requests, 'Connection closed');
283
410
 
284
- if (this.reconnect && this.reconnect_attempts < this.max_reconnect_attempts) {
411
+ if (this.should_attempt_reconnect()) {
285
412
  this.schedule_reconnect();
286
413
  } else {
287
414
  this.emit('disconnect');
@@ -289,45 +416,35 @@ class JoystickDBClient extends EventEmitter {
289
416
  }
290
417
 
291
418
  /**
292
- * Handles disconnection events and manages reconnection logic.
419
+ * Resets connection state variables.
293
420
  */
294
- handle_disconnect() {
421
+ reset_connection_state() {
422
+ this.is_connecting = false;
295
423
  this.is_connected = false;
296
424
  this.is_authenticated = false;
297
- this.is_connecting = false;
298
425
 
299
426
  if (this.socket) {
300
427
  this.socket.removeAllListeners();
428
+ this.socket.destroy();
301
429
  this.socket = null;
302
430
  }
303
431
 
304
432
  if (this.message_parser) {
305
433
  this.message_parser.reset();
306
434
  }
307
-
308
- // NOTE: Reject all pending requests.
309
- for (const [request_id, { reject, timeout }] of this.pending_requests) {
310
- clearTimeout(timeout);
311
- reject(new Error('Connection closed'));
312
- }
313
- this.pending_requests.clear();
314
-
315
- if (this.reconnect && this.reconnect_attempts < this.max_reconnect_attempts) {
316
- this.schedule_reconnect();
317
- } else {
318
- this.emit('disconnect');
319
- }
320
435
  }
321
436
 
322
437
  /**
323
- * Schedules a reconnection attempt with exponential backoff.
438
+ * Determines if reconnection should be attempted.
439
+ * @returns {boolean} True if reconnection should be attempted
324
440
  */
441
+ should_attempt_reconnect() {
442
+ return this.reconnect && this.reconnect_attempts < this.max_reconnect_attempts;
443
+ }
444
+
325
445
  schedule_reconnect() {
326
446
  this.reconnect_attempts++;
327
- const delay = Math.min(
328
- this.reconnect_delay * Math.pow(2, this.reconnect_attempts - 1),
329
- 30000
330
- );
447
+ const delay = calculate_reconnect_delay(this.reconnect_attempts, this.reconnect_delay);
331
448
 
332
449
  this.emit('reconnecting', { attempt: this.reconnect_attempts, delay });
333
450
 
@@ -336,36 +453,16 @@ class JoystickDBClient extends EventEmitter {
336
453
  }, delay);
337
454
  }
338
455
 
339
- /**
340
- * Sends a request to the server.
341
- * @param {string} op - Operation name
342
- * @param {Object} [data={}] - Request data
343
- * @param {boolean} [use_queue=true] - Whether to queue request if not connected
344
- * @returns {Promise<Object>} Server response
345
- */
346
456
  send_request(op, data = {}, use_queue = true) {
347
457
  return new Promise((resolve, reject) => {
348
458
  const request_id = ++this.request_id_counter;
349
- const message = {
350
- op,
351
- data
352
- };
459
+ const message = { op, data };
353
460
 
354
- const request = {
355
- message,
356
- resolve,
357
- reject,
358
- request_id
359
- };
461
+ const request = { message, resolve, reject, request_id };
360
462
 
361
- if (!this.is_connected || (op !== 'authentication' && op !== 'setup' && op !== 'ping' && !this.is_authenticated)) {
362
- if (use_queue) {
363
- this.request_queue.push(request);
364
- return;
365
- } else {
366
- reject(new Error('Not connected or authenticated'));
367
- return;
368
- }
463
+ if (this.should_queue_request(op, use_queue)) {
464
+ this.request_queue.push(request);
465
+ return;
369
466
  }
370
467
 
371
468
  this.send_request_now(request);
@@ -373,17 +470,22 @@ class JoystickDBClient extends EventEmitter {
373
470
  }
374
471
 
375
472
  /**
376
- * Immediately sends a request to the server.
377
- * @param {Object} request - Request object with message, resolve, reject, and request_id
473
+ * Determines if request should be queued.
474
+ * @param {string} op - Operation type
475
+ * @param {boolean} use_queue - Whether to use queue
476
+ * @returns {boolean} True if request should be queued
378
477
  */
478
+ should_queue_request(op, use_queue) {
479
+ const bypass_auth_ops = ['authentication', 'setup', 'ping'];
480
+ const needs_auth = !bypass_auth_ops.includes(op);
481
+
482
+ return (!this.is_connected || (needs_auth && !this.is_authenticated)) && use_queue;
483
+ }
484
+
379
485
  send_request_now(request) {
380
486
  const { message, resolve, reject, request_id } = request;
381
487
 
382
- const timeout = setTimeout(() => {
383
- this.pending_requests.delete(request_id);
384
- reject(new Error('Request timeout'));
385
- }, this.timeout);
386
-
488
+ const timeout = create_request_timeout(this.pending_requests, request_id, this.timeout);
387
489
  this.pending_requests.set(request_id, { resolve, reject, timeout });
388
490
 
389
491
  try {
@@ -396,9 +498,6 @@ class JoystickDBClient extends EventEmitter {
396
498
  }
397
499
  }
398
500
 
399
- /**
400
- * Processes queued requests after connection and authentication.
401
- */
402
501
  process_request_queue() {
403
502
  while (this.request_queue.length > 0 && this.is_connected && this.is_authenticated) {
404
503
  const request = this.request_queue.shift();
@@ -406,9 +505,6 @@ class JoystickDBClient extends EventEmitter {
406
505
  }
407
506
  }
408
507
 
409
- /**
410
- * Disconnects from the server and disables reconnection.
411
- */
412
508
  disconnect() {
413
509
  this.reconnect = false;
414
510
 
@@ -422,30 +518,15 @@ class JoystickDBClient extends EventEmitter {
422
518
  }
423
519
  }
424
520
 
425
-
426
-
427
521
  // NOTE: Backup Operations.
428
- /**
429
- * Triggers an immediate backup.
430
- * @returns {Promise<Object>} Backup result
431
- */
432
522
  async backup_now() {
433
523
  return this.send_request('admin', { admin_action: 'backup_now' });
434
524
  }
435
525
 
436
- /**
437
- * Lists all available backups.
438
- * @returns {Promise<Object>} Backups list
439
- */
440
526
  async list_backups() {
441
527
  return this.send_request('admin', { admin_action: 'list_backups' });
442
528
  }
443
529
 
444
- /**
445
- * Restores from a specific backup.
446
- * @param {string} backup_name - Name of backup to restore
447
- * @returns {Promise<Object>} Restore result
448
- */
449
530
  async restore_backup(backup_name) {
450
531
  return this.send_request('admin', {
451
532
  admin_action: 'restore_backup',
@@ -454,96 +535,48 @@ class JoystickDBClient extends EventEmitter {
454
535
  }
455
536
 
456
537
  // NOTE: Replication Operations.
457
- /**
458
- * Gets replication status and statistics.
459
- * @returns {Promise<Object>} Replication status
460
- */
461
538
  async get_replication_status() {
462
539
  return this.send_request('admin', { admin_action: 'get_replication_status' });
463
540
  }
464
541
 
465
- /**
466
- * Adds a secondary node to replication.
467
- * @param {Object} secondary - Secondary node configuration
468
- * @param {string} secondary.id - Secondary node ID
469
- * @param {string} secondary.ip - Secondary node IP address
470
- * @param {number} secondary.port - Secondary node port
471
- * @param {string} secondary.private_key - Base64 encoded private key
472
- * @returns {Promise<Object>} Add secondary result
473
- */
474
542
  async add_secondary(secondary) {
475
543
  return this.send_request('admin', { admin_action: 'add_secondary', ...secondary });
476
544
  }
477
545
 
478
- /**
479
- * Removes a secondary node from replication.
480
- * @param {string} secondary_id - Secondary node ID to remove
481
- * @returns {Promise<Object>} Remove secondary result
482
- */
483
546
  async remove_secondary(secondary_id) {
484
547
  return this.send_request('admin', { admin_action: 'remove_secondary', secondary_id });
485
548
  }
486
549
 
487
- /**
488
- * Forces synchronization with all secondary nodes.
489
- * @returns {Promise<Object>} Sync result
490
- */
491
550
  async sync_secondaries() {
492
551
  return this.send_request('admin', { admin_action: 'sync_secondaries' });
493
552
  }
494
553
 
495
- /**
496
- * Gets health status of secondary nodes.
497
- * @returns {Promise<Object>} Secondary health status
498
- */
499
554
  async get_secondary_health() {
500
555
  return this.send_request('admin', { admin_action: 'get_secondary_health' });
501
556
  }
502
557
 
503
- /**
504
- * Gets write forwarder status (for secondary nodes).
505
- * @returns {Promise<Object>} Write forwarder status
506
- */
507
558
  async get_forwarder_status() {
508
559
  return this.send_request('admin', { admin_action: 'get_forwarder_status' });
509
560
  }
510
561
 
511
562
  // NOTE: Health and Utility Operations.
512
- /**
513
- * Pings the server to check connectivity.
514
- * @returns {Promise<Object>} Ping result
515
- */
516
563
  async ping() {
517
564
  return this.send_request('ping', {}, false);
518
565
  }
519
566
 
520
- /**
521
- * Reloads server configuration.
522
- * @returns {Promise<Object>} Reload result
523
- */
524
567
  async reload() {
525
568
  return this.send_request('reload');
526
569
  }
527
570
 
528
571
  // NOTE: Auto-Indexing Operations.
529
- /**
530
- * Gets automatic indexing statistics.
531
- * @returns {Promise<Object>} Auto-indexing statistics
532
- */
533
572
  async get_auto_index_stats() {
534
573
  return this.send_request('admin', { admin_action: 'get_auto_index_stats' });
535
574
  }
536
575
 
537
-
538
576
  // NOTE: Setup Operation.
539
- /**
540
- * Performs initial server setup.
541
- * @returns {Promise<Object>} Setup result
542
- */
543
577
  async setup() {
544
578
  const result = await this.send_request('setup', {}, false);
545
579
 
546
- // NOTE: Display setup instructions to user.
547
580
  if (result.data && result.data.instructions) {
548
581
  console.log(result.data.instructions);
549
582
  }
@@ -552,14 +585,6 @@ class JoystickDBClient extends EventEmitter {
552
585
  }
553
586
 
554
587
  // NOTE: Database Operations.
555
- /**
556
- * Deletes multiple documents from a collection.
557
- * @param {string} collection - Collection name
558
- * @param {Object} [filter={}] - Query filter to match documents
559
- * @param {Object} [options={}] - Delete options
560
- * @param {number} [options.limit] - Maximum number of documents to delete
561
- * @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
562
- */
563
588
  async delete_many(collection, filter = {}, options = {}) {
564
589
  return this.send_request('delete_many', {
565
590
  collection,
@@ -569,24 +594,19 @@ class JoystickDBClient extends EventEmitter {
569
594
  }
570
595
 
571
596
  // NOTE: Database Interface.
572
- /**
573
- * Returns a database interface for method chaining operations.
574
- * @param {string} database_name - Database name
575
- * @returns {Database} Database interface instance
576
- */
577
597
  db(database_name) {
578
598
  return new Database(this, database_name);
579
599
  }
580
600
 
581
-
582
601
  // NOTE: Multi-Database Admin Operations.
583
- /**
584
- * Lists all databases on the server.
585
- * @returns {Promise<Object>} Databases list
586
- */
587
602
  async list_databases() {
588
603
  return this.send_request('admin', { admin_action: 'list_databases' });
589
604
  }
605
+
606
+ // NOTE: Statistics Operations.
607
+ async get_stats() {
608
+ return this.send_request('admin', { admin_action: 'stats' });
609
+ }
590
610
  }
591
611
 
592
612
  /**
@@ -594,24 +614,12 @@ class JoystickDBClient extends EventEmitter {
594
614
  * Provides a fluent API for database operations on a specific collection.
595
615
  */
596
616
  class Collection {
597
- /**
598
- * Creates a new Collection instance.
599
- * @param {JoystickDBClient} client - The client instance
600
- * @param {string} database_name - Name of the database
601
- * @param {string} collection_name - Name of the collection
602
- */
603
617
  constructor(client, database_name, collection_name) {
604
618
  this.client = client;
605
619
  this.database_name = database_name;
606
620
  this.collection_name = collection_name;
607
621
  }
608
622
 
609
- /**
610
- * Inserts a single document into the collection.
611
- * @param {Object} document - Document to insert
612
- * @param {Object} [options={}] - Insert options
613
- * @returns {Promise<Object>} Insert result
614
- */
615
623
  async insert_one(document, options = {}) {
616
624
  return this.client.send_request('insert_one', {
617
625
  database: this.database_name,
@@ -621,12 +629,6 @@ class Collection {
621
629
  });
622
630
  }
623
631
 
624
- /**
625
- * Finds a single document in the collection.
626
- * @param {Object} [filter={}] - Query filter
627
- * @param {Object} [options={}] - Find options
628
- * @returns {Promise<Object|null>} Found document or null
629
- */
630
632
  async find_one(filter = {}, options = {}) {
631
633
  const result = await this.client.send_request('find_one', {
632
634
  database: this.database_name,
@@ -637,12 +639,6 @@ class Collection {
637
639
  return result.document;
638
640
  }
639
641
 
640
- /**
641
- * Finds multiple documents in the collection.
642
- * @param {Object} [filter={}] - Query filter
643
- * @param {Object} [options={}] - Find options
644
- * @returns {Promise<Array>} Array of found documents
645
- */
646
642
  async find(filter = {}, options = {}) {
647
643
  const result = await this.client.send_request('find', {
648
644
  database: this.database_name,
@@ -650,16 +646,9 @@ class Collection {
650
646
  filter,
651
647
  options
652
648
  });
653
- return result.documents || [];
649
+ return { documents: result.documents || [] };
654
650
  }
655
651
 
656
- /**
657
- * Updates a single document in the collection.
658
- * @param {Object} filter - Query filter to match document
659
- * @param {Object} update - Update operations
660
- * @param {Object} [options={}] - Update options
661
- * @returns {Promise<Object>} Update result
662
- */
663
652
  async update_one(filter, update, options = {}) {
664
653
  return this.client.send_request('update_one', {
665
654
  database: this.database_name,
@@ -670,12 +659,6 @@ class Collection {
670
659
  });
671
660
  }
672
661
 
673
- /**
674
- * Deletes a single document from the collection.
675
- * @param {Object} filter - Query filter to match document
676
- * @param {Object} [options={}] - Delete options
677
- * @returns {Promise<Object>} Delete result
678
- */
679
662
  async delete_one(filter, options = {}) {
680
663
  return this.client.send_request('delete_one', {
681
664
  database: this.database_name,
@@ -685,13 +668,6 @@ class Collection {
685
668
  });
686
669
  }
687
670
 
688
- /**
689
- * Deletes multiple documents from the collection.
690
- * @param {Object} [filter={}] - Query filter to match documents
691
- * @param {Object} [options={}] - Delete options
692
- * @param {number} [options.limit] - Maximum number of documents to delete
693
- * @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
694
- */
695
671
  async delete_many(filter = {}, options = {}) {
696
672
  return this.client.send_request('delete_many', {
697
673
  database: this.database_name,
@@ -701,12 +677,6 @@ class Collection {
701
677
  });
702
678
  }
703
679
 
704
- /**
705
- * Performs multiple write operations in a single request.
706
- * @param {Array<Object>} operations - Array of write operations
707
- * @param {Object} [options={}] - Bulk write options
708
- * @returns {Promise<Object>} Bulk write result
709
- */
710
680
  async bulk_write(operations, options = {}) {
711
681
  return this.client.send_request('bulk_write', {
712
682
  database: this.database_name,
@@ -716,12 +686,6 @@ class Collection {
716
686
  });
717
687
  }
718
688
 
719
- /**
720
- * Creates an index on a collection field.
721
- * @param {string} field - Field name to index
722
- * @param {Object} [options={}] - Index options
723
- * @returns {Promise<Object>} Index creation result
724
- */
725
689
  async create_index(field, options = {}) {
726
690
  return this.client.send_request('create_index', {
727
691
  database: this.database_name,
@@ -731,12 +695,6 @@ class Collection {
731
695
  });
732
696
  }
733
697
 
734
- /**
735
- * Creates or updates an index on a collection field (upsert operation).
736
- * @param {string} field - Field name to index
737
- * @param {Object} [options={}] - Index options
738
- * @returns {Promise<Object>} Index upsert result
739
- */
740
698
  async upsert_index(field, options = {}) {
741
699
  return this.client.send_request('create_index', {
742
700
  database: this.database_name,
@@ -746,11 +704,6 @@ class Collection {
746
704
  });
747
705
  }
748
706
 
749
- /**
750
- * Drops an index from a collection field.
751
- * @param {string} field - Field name of index to drop
752
- * @returns {Promise<Object>} Index drop result
753
- */
754
707
  async drop_index(field) {
755
708
  return this.client.send_request('drop_index', {
756
709
  database: this.database_name,
@@ -759,10 +712,6 @@ class Collection {
759
712
  });
760
713
  }
761
714
 
762
- /**
763
- * Gets all indexes for the collection.
764
- * @returns {Promise<Object>} Indexes information
765
- */
766
715
  async get_indexes() {
767
716
  return this.client.send_request('get_indexes', {
768
717
  database: this.database_name,
@@ -771,19 +720,9 @@ class Collection {
771
720
  }
772
721
  }
773
722
 
774
- // NOTE: Add Collection class as static property for Database class access.
775
723
  JoystickDBClient.Collection = Collection;
776
724
 
777
- /**
778
- * JoystickDB client factory and utilities.
779
- * @namespace
780
- */
781
725
  const joystickdb = {
782
- /**
783
- * Creates a new JoystickDB client instance.
784
- * @param {ClientOptions} options - Client configuration options
785
- * @returns {JoystickDBClient} New client instance
786
- */
787
726
  client: (options) => new JoystickDBClient(options)
788
727
  };
789
728