@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
@@ -56,25 +56,70 @@ import {
56
56
  initialize_recovery_manager,
57
57
  reset_recovery_state
58
58
  } from './lib/recovery_manager.js';
59
+ import { has_settings } from './lib/load_settings.js';
59
60
 
60
- /** @type {Set<string>} Set of authenticated client socket IDs */
61
61
  const authenticated_clients = new Set();
62
-
63
- /** @type {Object|null} Connection manager instance */
64
62
  let connection_manager = null;
65
63
 
64
+ /**
65
+ * Validates authentication request data.
66
+ * @param {Object} data - Authentication data
67
+ * @returns {boolean} True if data is valid
68
+ */
69
+ const validate_authentication_data = (data) => {
70
+ return data && data.password;
71
+ };
72
+
73
+ /**
74
+ * Creates authentication error response.
75
+ * @param {string} error_message - Error message
76
+ * @returns {Object} Error response object
77
+ */
78
+ const create_authentication_error_response = (error_message) => ({
79
+ ok: 0,
80
+ error: error_message
81
+ });
82
+
83
+ /**
84
+ * Creates authentication success response.
85
+ * @returns {Object} Success response object
86
+ */
87
+ const create_authentication_success_response = () => ({
88
+ ok: 1,
89
+ version: "1.0.0",
90
+ message: 'Authentication successful'
91
+ });
92
+
93
+ /**
94
+ * Sends response and closes socket connection.
95
+ * @param {net.Socket} socket - Client socket
96
+ * @param {Object} response - Response object to send
97
+ */
98
+ const send_response_and_close_socket = (socket, response) => {
99
+ const encoded_response = encode_message(response);
100
+ socket.write(encoded_response);
101
+ socket.end();
102
+ };
103
+
104
+ /**
105
+ * Sends response to socket without closing.
106
+ * @param {net.Socket} socket - Client socket
107
+ * @param {Object} response - Response object to send
108
+ */
109
+ const send_response_to_socket = (socket, response) => {
110
+ const encoded_response = encode_message(response);
111
+ socket.write(encoded_response);
112
+ };
113
+
66
114
  /**
67
115
  * Handles client authentication requests.
68
116
  * @param {net.Socket} socket - Client socket connection
69
- * @param {Object} [data={}] - Authentication data containing password
70
- * @param {string} data.password - Client password for authentication
117
+ * @param {Object} data - Authentication data containing password
71
118
  */
72
119
  export const authentication = async (socket, data = {}) => {
73
- if (!data?.password) {
74
- const response = { ok: 0, error: 'Authentication operation requires password to be set in data.' };
75
- const encoded_response = encode_message(response);
76
- socket.write(encoded_response);
77
- socket.end();
120
+ if (!validate_authentication_data(data)) {
121
+ const response = create_authentication_error_response('Authentication operation requires password to be set in data.');
122
+ send_response_and_close_socket(socket, response);
78
123
  return;
79
124
  }
80
125
 
@@ -82,66 +127,68 @@ export const authentication = async (socket, data = {}) => {
82
127
  const client_ip = get_client_ip(socket);
83
128
 
84
129
  if (is_rate_limited(client_ip)) {
85
- const response = { ok: 0, error: 'Too many failed attempts. Please try again later.' };
86
- const encoded_response = encode_message(response);
87
- socket.write(encoded_response);
88
- socket.end();
130
+ const response = create_authentication_error_response('Too many failed attempts. Please try again later.');
131
+ send_response_and_close_socket(socket, response);
89
132
  return;
90
133
  }
91
134
 
92
135
  const is_valid = await verify_password(data.password, client_ip);
93
136
 
94
137
  if (!is_valid) {
95
- const response = { ok: 0, error: 'Authentication failed' };
96
- const encoded_response = encode_message(response);
97
- socket.write(encoded_response);
98
- socket.end();
138
+ const response = create_authentication_error_response('Authentication failed');
139
+ send_response_and_close_socket(socket, response);
99
140
  return;
100
141
  }
101
142
 
102
143
  authenticated_clients.add(socket.id);
103
- const auth_response = {
104
- ok: 1,
105
- version: "1.0.0",
106
- message: 'Authentication successful'
107
- };
108
- const encoded_auth_response = encode_message(auth_response);
109
- socket.write(encoded_auth_response);
144
+ const auth_response = create_authentication_success_response();
145
+ send_response_to_socket(socket, auth_response);
110
146
  } catch (error) {
111
- const response = { ok: 0, error: `Authentication error: ${error.message}` };
112
- const encoded_response = encode_message(response);
113
- socket.write(encoded_response);
114
- socket.end();
147
+ const response = create_authentication_error_response(`Authentication error: ${error.message}`);
148
+ send_response_and_close_socket(socket, response);
115
149
  }
116
150
  };
117
151
 
152
+ /**
153
+ * Creates setup success response.
154
+ * @param {string} password - Generated password
155
+ * @returns {Object} Setup response object
156
+ */
157
+ const create_setup_success_response = (password) => ({
158
+ ok: 1,
159
+ password,
160
+ message: 'Authentication setup completed successfully. Save this password - it will not be shown again.'
161
+ });
162
+
163
+ /**
164
+ * Creates setup error response.
165
+ * @param {string} error_message - Error message
166
+ * @returns {Object} Error response object
167
+ */
168
+ const create_setup_error_response = (error_message) => ({
169
+ ok: 0,
170
+ error: `Setup error: ${error_message}`
171
+ });
172
+
118
173
  /**
119
174
  * Handles initial server setup, generating authentication password.
120
175
  * @param {net.Socket} socket - Client socket connection
121
- * @param {Object} [data={}] - Setup data (currently unused)
176
+ * @param {Object} data - Setup data (currently unused)
122
177
  */
123
178
  export const setup = async (socket, data = {}) => {
124
179
  try {
125
180
  const password = setup_authentication();
126
-
127
- // NOTE: Return response format expected by tests.
128
- const response = {
129
- ok: 1,
130
- password,
131
- message: 'Authentication setup completed successfully. Save this password - it will not be shown again.'
132
- };
133
- const encoded_response = encode_message(response);
134
- socket.write(encoded_response);
181
+ const response = create_setup_success_response(password);
182
+ send_response_to_socket(socket, response);
135
183
  } catch (error) {
136
- const response = { ok: 0, error: `Setup error: ${error.message}` };
137
- const encoded_response = encode_message(response);
138
- socket.write(encoded_response);
184
+ const response = create_setup_error_response(error.message);
185
+ send_response_to_socket(socket, response);
139
186
  }
140
187
  };
141
188
 
142
189
  /**
143
190
  * Validates if an operation type is supported.
144
- * @param {string} [op_type=''] - Operation type to validate
191
+ * @param {string} op_type - Operation type to validate
145
192
  * @returns {boolean} True if operation type is valid
146
193
  * @throws {Error} When no operation type is provided
147
194
  */
@@ -153,6 +200,32 @@ export const check_op_type = (op_type = '') => {
153
200
  return op_types.includes(op_type);
154
201
  };
155
202
 
203
+ /**
204
+ * Parses string data as JSON.
205
+ * @param {string} string_data - String data to parse
206
+ * @returns {Object|null} Parsed JSON object or null
207
+ */
208
+ const parse_string_data = (string_data) => {
209
+ return safe_json_parse(string_data);
210
+ };
211
+
212
+ /**
213
+ * Parses Buffer data as MessagePack then JSON.
214
+ * @param {Buffer} buffer_data - Buffer data to parse
215
+ * @returns {Object|null} Parsed object or null
216
+ */
217
+ const parse_buffer_data = (buffer_data) => {
218
+ try {
219
+ const decoded_messagepack = decode_messagepack(buffer_data);
220
+ if (typeof decoded_messagepack === 'string') {
221
+ return safe_json_parse(decoded_messagepack);
222
+ }
223
+ return decoded_messagepack;
224
+ } catch (error) {
225
+ return null;
226
+ }
227
+ };
228
+
156
229
  /**
157
230
  * Parses raw data from various formats (string, Buffer, MessagePack) into JavaScript objects.
158
231
  * @param {string|Buffer|Object} raw_data - Raw data to parse
@@ -161,17 +234,10 @@ export const check_op_type = (op_type = '') => {
161
234
  export const parse_data = (raw_data) => {
162
235
  try {
163
236
  if (typeof raw_data === 'string') {
164
- const data_as_json = safe_json_parse(raw_data);
165
- return data_as_json;
237
+ return parse_string_data(raw_data);
166
238
  } else if (Buffer.isBuffer(raw_data)) {
167
- // NOTE: Decode MessagePack buffer and then parse as JSON.
168
- const decoded_messagepack = decode_messagepack(raw_data);
169
- if (typeof decoded_messagepack === 'string') {
170
- return safe_json_parse(decoded_messagepack);
171
- }
172
- return decoded_messagepack;
239
+ return parse_buffer_data(raw_data);
173
240
  } else {
174
- // NOTE: raw_data is already decoded from MessagePack by the message parser.
175
241
  return raw_data;
176
242
  }
177
243
  } catch (error) {
@@ -186,7 +252,6 @@ export const parse_data = (raw_data) => {
186
252
  * @returns {boolean} True if socket is authenticated or in development mode
187
253
  */
188
254
  const check_authentication = (socket) => {
189
- // NOTE: Bypass authentication in development mode for TCP connections.
190
255
  if (is_development_mode()) {
191
256
  return true;
192
257
  }
@@ -194,363 +259,509 @@ const check_authentication = (socket) => {
194
259
  return authenticated_clients.has(socket.id);
195
260
  };
196
261
 
197
-
198
262
  /**
199
- * @typedef {Object} ServerOptions
200
- * @property {number} [worker_count] - Number of worker processes
201
- * @property {number} [port=1983] - Server port
202
- * @property {string} [environment] - Environment name for settings file (defaults to NODE_ENV)
263
+ * Attempts to restore from backup if configured.
264
+ * @param {Object} settings - Server settings
265
+ * @param {Function} log - Logger function
203
266
  */
267
+ const attempt_startup_restore = async (settings, log) => {
268
+ if (!settings?.restore_from) {
269
+ return;
270
+ }
271
+
272
+ try {
273
+ log.info('Startup restore requested', { backup_filename: settings.restore_from });
274
+
275
+ const restore_result = await restore_backup(settings.restore_from);
276
+
277
+ log.info('Startup restore completed', {
278
+ backup_filename: settings.restore_from,
279
+ duration_ms: restore_result.duration_ms
280
+ });
281
+
282
+ const updated_settings = { ...settings };
283
+ delete updated_settings.restore_from;
284
+
285
+ process.env.JOYSTICK_DB_SETTINGS = JSON.stringify(updated_settings);
286
+
287
+ load_settings();
288
+
289
+ log.info('Removed restore_from from settings after successful restore');
290
+ } catch (restore_error) {
291
+ log.error('Startup restore failed', {
292
+ backup_filename: settings.restore_from,
293
+ error: restore_error.message
294
+ });
295
+
296
+ log.info('Continuing with fresh database after restore failure');
297
+ }
298
+ };
204
299
 
205
300
  /**
206
- * Creates and configures the JoystickDB TCP server.
207
- * @returns {Promise<net.Server>} Configured TCP server instance with cleanup method
301
+ * Loads server settings with fallback for missing files.
302
+ * @returns {Object|null} Settings object or null if not available
208
303
  */
209
- export const create_server = async () => {
210
- const { create_context_logger } = create_logger('server');
211
- const log = create_context_logger();
212
-
213
- /*
214
- NOTE:
215
- Try to load settings, but don't fail if file doesn't exist (for tests).
216
- */
217
- let settings = null;
304
+ const load_server_settings = () => {
218
305
  try {
219
306
  load_settings();
220
- settings = get_settings();
307
+ return get_settings();
221
308
  } catch (error) {
222
- /*
223
- NOTE:
224
- Settings file doesn't exist - this is OK for tests.
225
- We'll handle missing settings in individual operations.
226
- */
227
- }
228
-
229
- // NOTE: Handle startup restore if configured.
230
- if (settings?.restore_from) {
231
- try {
232
- log.info('Startup restore requested', { backup_filename: settings.restore_from });
233
-
234
- const restore_result = await restore_backup(settings.restore_from);
235
-
236
- log.info('Startup restore completed', {
237
- backup_filename: settings.restore_from,
238
- duration_ms: restore_result.duration_ms
239
- });
240
-
241
- // NOTE: Remove restore_from from settings after successful restore.
242
- const updated_settings = { ...settings };
243
- delete updated_settings.restore_from;
244
-
245
- // NOTE: Update the environment variable with the new settings.
246
- process.env.JOYSTICK_DB_SETTINGS = JSON.stringify(updated_settings);
247
-
248
- // NOTE: Reload settings to reflect the change.
249
- load_settings();
250
- settings = get_settings();
251
-
252
- log.info('Removed restore_from from settings after successful restore');
253
- } catch (restore_error) {
254
- log.error('Startup restore failed', {
255
- backup_filename: settings.restore_from,
256
- error: restore_error.message
257
- });
258
-
259
- // NOTE: Continue with fresh database if restore fails.
260
- log.info('Continuing with fresh database after restore failure');
261
- }
262
- }
263
-
264
- // NOTE: Initialize database with data_path from settings if available.
265
- let database_path = './data'; // Default path
266
- if (settings?.data_path) {
267
- database_path = settings.data_path;
309
+ return null;
268
310
  }
311
+ };
312
+
313
+ /**
314
+ * Initializes core server components.
315
+ * @param {Object} settings - Server settings
316
+ */
317
+ const initialize_server_components = async (settings) => {
318
+ const database_path = settings?.data_path || './data';
269
319
 
270
320
  initialize_database(database_path);
271
321
  initialize_auth_manager();
272
322
  await initialize_api_key_manager();
273
323
  initialize_recovery_manager();
274
-
275
- // NOTE: Initialize replication manager for primary nodes.
324
+ };
325
+
326
+ /**
327
+ * Initializes replication manager with error handling.
328
+ * @param {Function} log - Logger function
329
+ */
330
+ const initialize_replication_with_logging = (log) => {
276
331
  try {
277
332
  initialize_replication_manager();
278
333
  log.info('Replication manager initialized');
279
334
  } catch (replication_error) {
280
335
  log.warn('Failed to initialize replication manager', { error: replication_error.message });
281
336
  }
282
-
283
- // NOTE: Initialize write forwarder for secondary nodes.
337
+ };
338
+
339
+ /**
340
+ * Initializes write forwarder with error handling.
341
+ * @param {Function} log - Logger function
342
+ */
343
+ const initialize_write_forwarder_with_logging = (log) => {
284
344
  try {
285
345
  initialize_write_forwarder();
286
346
  log.info('Write forwarder initialized');
287
347
  } catch (forwarder_error) {
288
348
  log.warn('Failed to initialize write forwarder', { error: forwarder_error.message });
289
349
  }
290
-
291
- // NOTE: Start backup scheduling if S3 is configured.
292
- if (settings?.s3) {
293
- try {
294
- start_backup_schedule();
295
- log.info('Backup scheduling started');
296
- } catch (backup_schedule_error) {
297
- log.warn('Failed to start backup scheduling', { error: backup_schedule_error.message });
298
- }
350
+ };
351
+
352
+ /**
353
+ * Starts backup scheduling if S3 is configured.
354
+ * @param {Object} settings - Server settings
355
+ * @param {Function} log - Logger function
356
+ */
357
+ const start_backup_scheduling = (settings, log) => {
358
+ if (!settings?.s3) {
359
+ return;
299
360
  }
300
-
301
- connection_manager = create_connection_manager({
302
- max_connections: 1000,
303
- idle_timeout: 10 * 60 * 1000,
304
- request_timeout: 5 * 1000
305
- });
306
-
307
- // NOTE: Start HTTP server for setup and recovery operations.
308
- let http_server_instance = null;
361
+
309
362
  try {
310
- const { http_port } = get_port_configuration();
311
- http_server_instance = await start_http_server(http_port);
363
+ start_backup_schedule();
364
+ log.info('Backup scheduling started');
365
+ } catch (backup_schedule_error) {
366
+ log.warn('Failed to start backup scheduling', { error: backup_schedule_error.message });
367
+ }
368
+ };
369
+
370
+ /**
371
+ * Starts HTTP server with error handling.
372
+ * @param {number} http_port - HTTP port number
373
+ * @param {Function} log - Logger function
374
+ * @returns {Object|null} HTTP server instance or null
375
+ */
376
+ const start_http_server_with_logging = async (http_port, log) => {
377
+ try {
378
+ const http_server_instance = await start_http_server(http_port);
312
379
  if (http_server_instance) {
313
380
  log.info('HTTP server started', { http_port });
314
381
  }
382
+ return http_server_instance;
315
383
  } catch (http_error) {
316
384
  log.warn('Failed to start HTTP server', { error: http_error.message });
385
+ return null;
317
386
  }
318
-
319
- // NOTE: Display development mode startup message if in development.
387
+ };
388
+
389
+ /**
390
+ * Displays appropriate startup messages based on environment.
391
+ */
392
+ const display_startup_messages = () => {
320
393
  if (is_development_mode()) {
321
394
  const { tcp_port, http_port } = get_port_configuration();
322
395
  display_development_startup_message(tcp_port, http_port);
323
396
  } else {
324
397
  warn_undefined_node_env();
325
398
  }
399
+ };
400
+
401
+ /**
402
+ * Creates connection manager instance.
403
+ * @returns {Object} Connection manager instance
404
+ */
405
+ const create_server_connection_manager = () => {
406
+ return create_connection_manager({
407
+ max_connections: 1000,
408
+ idle_timeout: 10 * 60 * 1000,
409
+ request_timeout: 5 * 1000
410
+ });
411
+ };
412
+
413
+ /**
414
+ * Handles socket data processing.
415
+ * @param {net.Socket} socket - Client socket
416
+ * @param {Buffer} raw_data - Raw socket data
417
+ * @param {Object} message_parser - Message parser instance
418
+ * @param {Function} log - Logger function
419
+ */
420
+ const handle_socket_data = async (socket, raw_data, message_parser, log) => {
421
+ connection_manager.update_activity(socket.id);
326
422
 
327
- const server = net.createServer((socket = {}) => {
328
- if (!connection_manager.add_connection(socket)) {
329
- return;
330
- }
423
+ try {
424
+ const messages = message_parser.parse_messages(raw_data);
331
425
 
332
- const message_parser = create_message_parser();
426
+ for (const message of messages) {
427
+ await process_single_message(socket, message, raw_data.length, log);
428
+ }
429
+ } catch (error) {
430
+ log.error('Message parsing failed', {
431
+ client_id: socket.id,
432
+ error: error.message
433
+ });
333
434
 
334
- socket.on('data', async (raw_data) => {
335
- connection_manager.update_activity(socket.id);
435
+ send_error(socket, { message: 'Invalid message format' });
436
+ socket.end();
437
+ }
438
+ };
439
+
440
+ /**
441
+ * Processes a single parsed message.
442
+ * @param {net.Socket} socket - Client socket
443
+ * @param {Object} message - Parsed message
444
+ * @param {number} raw_data_size - Size of raw data
445
+ * @param {Function} log - Logger function
446
+ */
447
+ const process_single_message = async (socket, message, raw_data_size, log) => {
448
+ const parsed_data = message;
449
+ const op_type = parsed_data?.op || null;
450
+
451
+ if (!op_type) {
452
+ send_error(socket, { message: 'Missing operation type' });
453
+ return;
454
+ }
455
+
456
+ const is_valid_op_type = check_op_type(op_type);
457
+
458
+ if (!is_valid_op_type) {
459
+ send_error(socket, { message: 'Invalid operation type' });
460
+ return;
461
+ }
462
+
463
+ const request_timeout = connection_manager.create_request_timeout(socket.id, op_type);
464
+
465
+ try {
466
+ await route_operation_to_handler(socket, op_type, parsed_data, raw_data_size);
467
+ } finally {
468
+ clearTimeout(request_timeout);
469
+ }
470
+ };
471
+
472
+ /**
473
+ * Routes operation to appropriate handler.
474
+ * @param {net.Socket} socket - Client socket
475
+ * @param {string} op_type - Operation type
476
+ * @param {Object} parsed_data - Parsed message data
477
+ * @param {number} raw_data_size - Size of raw data
478
+ */
479
+ const route_operation_to_handler = async (socket, op_type, parsed_data, raw_data_size) => {
480
+ const operation_data = parsed_data?.data || {};
481
+
482
+ switch(op_type) {
483
+ case 'authentication':
484
+ await authentication(socket, operation_data);
485
+ break;
336
486
 
337
- try {
338
- const messages = message_parser.parse_messages(raw_data);
339
-
340
- for (const message of messages) {
341
- // NOTE: Message is already decoded from MessagePack by the message parser.
342
- const parsed_data = message;
343
- const op_type = parsed_data?.op || null;
344
-
345
- if (!op_type) {
346
- send_error(socket, { message: 'Missing operation type' });
347
- continue;
348
- }
349
-
350
- const is_valid_op_type = check_op_type(op_type);
351
-
352
- if (!is_valid_op_type) {
353
- send_error(socket, { message: 'Invalid operation type' });
354
- continue;
355
- }
356
-
357
- const request_timeout = connection_manager.create_request_timeout(socket.id, op_type);
358
-
359
- try {
360
- switch(op_type) {
361
- case 'authentication':
362
- await authentication(socket, parsed_data?.data || {});
363
- break;
364
-
365
- case 'setup':
366
- await setup(socket, parsed_data?.data || {});
367
- break;
368
-
369
- case 'insert_one':
370
- case 'update_one':
371
- case 'delete_one':
372
- case 'delete_many':
373
- case 'bulk_write':
374
- case 'find_one':
375
- case 'find':
376
- case 'create_index':
377
- case 'drop_index':
378
- case 'get_indexes':
379
- // NOTE: Use shared operation dispatcher for database operations.
380
- await handle_database_operation(socket, op_type, parsed_data?.data || {}, check_authentication, raw_data.length, connection_manager, authenticated_clients);
381
- break;
382
-
383
- case 'ping':
384
- handle_ping_operation(socket);
385
- break;
386
-
387
- case 'admin':
388
- // NOTE: Use shared operation dispatcher for admin operations.
389
- await handle_admin_operation(socket, parsed_data?.data || {}, check_authentication, connection_manager, authenticated_clients);
390
- break;
391
-
392
- case 'reload':
393
- if (!check_authentication(socket)) {
394
- send_error(socket, { message: 'Authentication required' });
395
- break;
396
- }
397
-
398
- try {
399
- let old_settings = null;
400
- try {
401
- old_settings = get_settings();
402
- } catch (error) {
403
- // NOTE: Settings not loaded yet, that's OK.
404
- }
405
-
406
- let new_settings = null;
407
- try {
408
- await load_settings();
409
- new_settings = get_settings();
410
- } catch (error) {
411
- // NOTE: Settings file doesn't exist, create default settings for tests.
412
- new_settings = {
413
- port: 1983,
414
- authentication: {}
415
- };
416
- }
417
-
418
- const reload_info = {
419
- ok: 1,
420
- status: 'success',
421
- message: 'Configuration reloaded successfully',
422
- changes: {
423
- port_changed: old_settings ? old_settings.port !== new_settings.port : false,
424
- authentication_changed: old_settings ? old_settings.authentication?.password_hash !== new_settings.authentication?.password_hash : false
425
- },
426
- timestamp: new Date().toISOString()
427
- };
428
-
429
- const encoded_response = encode_message(reload_info);
430
- socket.write(encoded_response);
431
- } catch (error) {
432
- const error_response = { ok: 0, error: `Reload operation failed: ${error.message}` };
433
- const encoded_error_response = encode_message(error_response);
434
- socket.write(encoded_error_response);
435
- }
436
- break;
437
-
438
- default:
439
- send_error(socket, { message: `Operation ${op_type} not implemented` });
440
- }
441
- } finally {
442
- clearTimeout(request_timeout);
443
- }
444
- }
445
- } catch (error) {
446
- log.error('Message parsing failed', {
447
- client_id: socket.id,
448
- error: error.message
449
- });
450
-
451
- send_error(socket, { message: 'Invalid message format' });
452
- socket.end();
453
- }
454
- });
487
+ case 'setup':
488
+ await setup(socket, operation_data);
489
+ break;
490
+
491
+ case 'insert_one':
492
+ case 'update_one':
493
+ case 'delete_one':
494
+ case 'delete_many':
495
+ case 'bulk_write':
496
+ case 'find_one':
497
+ case 'find':
498
+ case 'create_index':
499
+ case 'drop_index':
500
+ case 'get_indexes':
501
+ await handle_database_operation(socket, op_type, operation_data, check_authentication, raw_data_size, connection_manager, authenticated_clients);
502
+ break;
503
+
504
+ case 'ping':
505
+ handle_ping_operation(socket);
506
+ break;
507
+
508
+ case 'admin':
509
+ await handle_admin_operation(socket, operation_data, check_authentication, connection_manager, authenticated_clients);
510
+ break;
511
+
512
+ case 'reload':
513
+ await handle_reload_operation(socket);
514
+ break;
515
+
516
+ default:
517
+ send_error(socket, { message: `Operation ${op_type} not implemented` });
518
+ }
519
+ };
455
520
 
456
- socket.on('end', () => {
457
- log.info('Client disconnected', { socket_id: socket.id });
458
- authenticated_clients.delete(socket.id);
459
- connection_manager.remove_connection(socket.id);
460
- });
521
+ /**
522
+ * Handles reload operation.
523
+ * @param {net.Socket} socket - Client socket
524
+ */
525
+ const handle_reload_operation = async (socket) => {
526
+ if (!check_authentication(socket)) {
527
+ send_error(socket, { message: 'Authentication required' });
528
+ return;
529
+ }
530
+
531
+ try {
532
+ const old_settings = get_current_settings();
533
+ const new_settings = await reload_server_settings();
461
534
 
462
- socket.on('error', (error) => {
463
- log.error('Socket error', {
464
- socket_id: socket.id,
465
- error: error.message
466
- });
467
-
468
- authenticated_clients.delete(socket.id);
469
- connection_manager.remove_connection(socket.id);
470
- });
535
+ const reload_info = create_reload_response(old_settings, new_settings);
536
+ send_response_to_socket(socket, reload_info);
537
+ } catch (error) {
538
+ const error_response = { ok: 0, error: `Reload operation failed: ${error.message}` };
539
+ send_response_to_socket(socket, error_response);
540
+ }
541
+ };
542
+
543
+ /**
544
+ * Gets current settings with fallback.
545
+ * @returns {Object|null} Current settings or null
546
+ */
547
+ const get_current_settings = () => {
548
+ try {
549
+ return get_settings();
550
+ } catch (error) {
551
+ return null;
552
+ }
553
+ };
554
+
555
+ /**
556
+ * Reloads server settings.
557
+ * @returns {Object} New settings object
558
+ */
559
+ const reload_server_settings = async () => {
560
+ try {
561
+ await load_settings();
562
+ return get_settings();
563
+ } catch (error) {
564
+ return {
565
+ port: 1983,
566
+ authentication: {}
567
+ };
568
+ }
569
+ };
570
+
571
+ /**
572
+ * Creates reload operation response.
573
+ * @param {Object} old_settings - Previous settings
574
+ * @param {Object} new_settings - New settings
575
+ * @returns {Object} Reload response object
576
+ */
577
+ const create_reload_response = (old_settings, new_settings) => ({
578
+ ok: 1,
579
+ status: 'success',
580
+ message: 'Configuration reloaded successfully',
581
+ changes: {
582
+ port_changed: old_settings ? old_settings.port !== new_settings.port : false,
583
+ authentication_changed: old_settings ? old_settings.authentication?.password_hash !== new_settings.authentication?.password_hash : false
584
+ },
585
+ timestamp: new Date().toISOString()
586
+ });
587
+
588
+ /**
589
+ * Handles socket disconnection.
590
+ * @param {net.Socket} socket - Client socket
591
+ * @param {Function} log - Logger function
592
+ */
593
+ const handle_socket_disconnection = (socket, log) => {
594
+ log.info('Client disconnected', { socket_id: socket.id });
595
+ authenticated_clients.delete(socket.id);
596
+ connection_manager.remove_connection(socket.id);
597
+ };
598
+
599
+ /**
600
+ * Handles socket errors.
601
+ * @param {net.Socket} socket - Client socket
602
+ * @param {Error} error - Socket error
603
+ * @param {Function} log - Logger function
604
+ */
605
+ const handle_socket_error = (socket, error, log) => {
606
+ log.error('Socket error', {
607
+ socket_id: socket.id,
608
+ error: error.message
609
+ });
610
+
611
+ authenticated_clients.delete(socket.id);
612
+ connection_manager.remove_connection(socket.id);
613
+ };
614
+
615
+ /**
616
+ * Sets up socket event handlers.
617
+ * @param {net.Socket} socket - Client socket
618
+ * @param {Object} message_parser - Message parser instance
619
+ * @param {Function} log - Logger function
620
+ */
621
+ const setup_socket_event_handlers = (socket, message_parser, log) => {
622
+ socket.on('data', async (raw_data) => {
623
+ await handle_socket_data(socket, raw_data, message_parser, log);
471
624
  });
625
+
626
+ socket.on('end', () => {
627
+ handle_socket_disconnection(socket, log);
628
+ });
629
+
630
+ socket.on('error', (error) => {
631
+ handle_socket_error(socket, error, log);
632
+ });
633
+ };
634
+
635
+ /**
636
+ * Handles new socket connections.
637
+ * @param {net.Socket} socket - New client socket
638
+ * @param {Function} log - Logger function
639
+ */
640
+ const handle_new_socket_connection = (socket, log) => {
641
+ if (!connection_manager.add_connection(socket)) {
642
+ return;
643
+ }
472
644
 
473
- // NOTE: Add cleanup method for tests.
474
- server.cleanup = async () => {
645
+ const message_parser = create_message_parser();
646
+ setup_socket_event_handlers(socket, message_parser, log);
647
+ };
648
+
649
+ /**
650
+ * Creates cleanup function for server shutdown.
651
+ * @returns {Function} Cleanup function
652
+ */
653
+ const create_server_cleanup_function = () => {
654
+ return async () => {
475
655
  try {
476
- // NOTE: Stop HTTP server.
477
656
  await stop_http_server();
478
-
479
- // NOTE: Stop backup scheduling.
480
657
  stop_backup_schedule();
481
-
482
- // NOTE: Shutdown replication manager.
483
658
  await shutdown_replication_manager();
484
-
485
- // NOTE: Shutdown write forwarder.
486
659
  await shutdown_write_forwarder();
487
660
 
488
- // NOTE: Shutdown connection manager.
489
661
  if (connection_manager) {
490
662
  connection_manager.shutdown();
491
663
  }
492
664
 
493
- // NOTE: Clear authenticated clients.
494
665
  authenticated_clients.clear();
495
-
496
- // NOTE: Shutdown write queue.
497
666
  await shutdown_write_queue();
498
667
 
499
- // NOTE: Wait a bit to ensure all operations are complete.
500
668
  await new Promise(resolve => setTimeout(resolve, 100));
501
-
502
- // NOTE: Cleanup database (after everything else is shut down).
503
669
  await cleanup_database();
504
670
 
505
- // NOTE: Reset auth state.
506
671
  reset_auth_state();
507
-
508
- // NOTE: Reset recovery state.
509
672
  reset_recovery_state();
510
673
  } catch (error) {
511
674
  // NOTE: Ignore cleanup errors in tests.
512
675
  }
513
676
  };
514
-
515
- return server;
516
677
  };
517
678
 
518
- if (import.meta.url === `file://${process.argv[1]}`) {
519
- const { create_context_logger } = create_logger('main');
679
+ /**
680
+ * @typedef {Object} ServerOptions
681
+ * @property {number} [worker_count] - Number of worker processes
682
+ * @property {number} [port=1983] - Server port
683
+ * @property {string} [environment] - Environment name for settings file (defaults to NODE_ENV)
684
+ */
685
+
686
+ /**
687
+ * Creates and configures the JoystickDB TCP server.
688
+ * @returns {Promise<net.Server>} Configured TCP server instance with cleanup method
689
+ */
690
+ export const create_server = async () => {
691
+ const { create_context_logger } = create_logger('server');
520
692
  const log = create_context_logger();
521
693
 
522
- // NOTE: Check for recovery token generation flag.
523
- if (process.argv.includes('--generate-recovery-token')) {
524
- try {
525
- initialize_recovery_manager();
526
- const recovery_info = create_recovery_token();
527
-
528
- console.log('Emergency Recovery Token Generated');
529
- console.log(`Visit: ${recovery_info.url}`);
530
- console.log('Token expires in 10 minutes');
531
-
532
- log.info('Recovery token generated via CLI', {
533
- expires_at: new Date(recovery_info.expires_at).toISOString()
534
- });
535
-
536
- process.exit(0);
537
- } catch (error) {
538
- console.error('Failed to generate recovery token:', error.message);
539
- log.error('Recovery token generation failed', { error: error.message });
540
- process.exit(1);
541
- }
542
- }
694
+ const settings = load_server_settings();
543
695
 
544
- // NOTE: Get port configuration from settings or use defaults.
545
- const { tcp_port, http_port } = get_port_configuration();
696
+ await attempt_startup_restore(settings, log);
697
+ await initialize_server_components(settings);
698
+
699
+ initialize_replication_with_logging(log);
700
+ initialize_write_forwarder_with_logging(log);
701
+ start_backup_scheduling(settings, log);
702
+
703
+ connection_manager = create_server_connection_manager();
546
704
 
547
- const options = {
705
+ const { http_port } = get_port_configuration();
706
+ await start_http_server_with_logging(http_port, log);
707
+
708
+ display_startup_messages();
709
+
710
+ const server = net.createServer((socket = {}) => {
711
+ handle_new_socket_connection(socket, log);
712
+ });
713
+
714
+ server.cleanup = create_server_cleanup_function();
715
+
716
+ return server;
717
+ };
718
+
719
+ /**
720
+ * Handles recovery token generation from command line.
721
+ * @param {Function} log - Logger function
722
+ */
723
+ const handle_recovery_token_generation = (log) => {
724
+ try {
725
+ initialize_recovery_manager();
726
+ const recovery_info = create_recovery_token();
727
+
728
+ console.log('Emergency Recovery Token Generated');
729
+ console.log(`Visit: ${recovery_info.url}`);
730
+ console.log('Token expires in 10 minutes');
731
+
732
+ log.info('Recovery token generated via CLI', {
733
+ expires_at: new Date(recovery_info.expires_at).toISOString()
734
+ });
735
+
736
+ process.exit(0);
737
+ } catch (error) {
738
+ console.error('Failed to generate recovery token:', error.message);
739
+ log.error('Recovery token generation failed', { error: error.message });
740
+ process.exit(1);
741
+ }
742
+ };
743
+
744
+ /**
745
+ * Creates server startup options.
746
+ * @returns {Object} Server options object
747
+ */
748
+ const create_server_startup_options = () => {
749
+ const { tcp_port } = get_port_configuration();
750
+
751
+ return {
548
752
  worker_count: process.env.WORKER_COUNT ? parseInt(process.env.WORKER_COUNT) : undefined,
549
753
  port: tcp_port,
550
754
  environment: process.env.NODE_ENV || 'development'
551
755
  };
552
-
553
- const { has_settings } = await import('./lib/load_settings.js');
756
+ };
757
+
758
+ /**
759
+ * Logs server startup information.
760
+ * @param {Object} options - Server options
761
+ * @param {Function} log - Logger function
762
+ */
763
+ const log_server_startup_info = (options, log) => {
764
+ const { tcp_port, http_port } = get_port_configuration();
554
765
  const has_db_settings = has_settings();
555
766
 
556
767
  log.info('Starting JoystickDB server...', {
@@ -561,6 +772,18 @@ if (import.meta.url === `file://${process.argv[1]}`) {
561
772
  has_settings: has_db_settings,
562
773
  port_source: has_db_settings ? 'JOYSTICK_DB_SETTINGS' : 'default'
563
774
  });
775
+ };
776
+
777
+ if (import.meta.url === `file://${process.argv[1]}`) {
778
+ const { create_context_logger } = create_logger('main');
779
+ const log = create_context_logger();
780
+
781
+ if (process.argv.includes('--generate-recovery-token')) {
782
+ handle_recovery_token_generation(log);
783
+ }
784
+
785
+ const options = create_server_startup_options();
786
+ log_server_startup_info(options, log);
564
787
 
565
788
  start_cluster(options);
566
789
  }