@joystick.js/db-canary 0.0.0-canary.2250 → 0.0.0-canary.2252

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 (49) 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 +92 -119
  23. package/src/client/index.js +279 -345
  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 +563 -201
  37. package/src/server/lib/operations/find_one.js +544 -188
  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/tests/server/cluster/master_read_write_operations.test.js +5 -14
  43. package/tests/server/integration/authentication_integration.test.js +18 -10
  44. package/tests/server/integration/backup_integration.test.js +35 -27
  45. package/tests/server/lib/api_key_manager.test.js +88 -32
  46. package/tests/server/lib/development_mode.test.js +2 -2
  47. package/tests/server/lib/operations/admin.test.js +20 -12
  48. package/tests/server/lib/operations/delete_one.test.js +10 -4
  49. package/tests/server/lib/operations/find_array_queries.test.js +261 -0
@@ -96,98 +96,149 @@ class ClusterMaster extends EventEmitter {
96
96
  }
97
97
 
98
98
  /**
99
- * Starts the cluster master and spawns worker processes.
100
- * @throws {Error} When startup fails
99
+ * Determines database path from settings or uses default.
100
+ * @returns {string} Database path
101
101
  */
102
- async start() {
103
- const start_time = Date.now();
104
-
102
+ get_database_path() {
103
+ let database_path = './data';
105
104
  try {
106
- this.settings = load_settings(this.settings_file);
107
- this.log.info('Settings loaded successfully', { settings_file: this.settings_file });
108
-
109
- // NOTE: Initialize database and auth manager.
110
- // NOTE: Initialize database with data_path from settings if available.
111
- let database_path = './data'; // Default path
112
- try {
113
- const settings = get_settings();
114
- if (settings?.data_path) {
115
- database_path = settings.data_path;
116
- }
117
- } catch (error) {
118
- // NOTE: Settings not available, use default path.
105
+ const settings = get_settings();
106
+ if (settings?.data_path) {
107
+ database_path = settings.data_path;
119
108
  }
109
+ } catch (error) {
110
+ // Settings not available, use default path
111
+ }
112
+ return database_path;
113
+ }
114
+
115
+ /**
116
+ * Initializes core database and authentication systems.
117
+ */
118
+ async initialize_core_systems() {
119
+ const database_path = this.get_database_path();
120
+
121
+ initialize_database(database_path);
122
+ initialize_auth_manager();
123
+ await initialize_api_key_manager();
124
+
125
+ this.log.info('Database and auth manager initialized');
126
+ }
127
+
128
+ /**
129
+ * Handles startup database restore if configured.
130
+ */
131
+ async handle_startup_restore() {
132
+ if (!this.settings?.restore_from) {
133
+ return;
134
+ }
135
+
136
+ try {
137
+ this.log.info('Startup restore requested', { backup_filename: this.settings.restore_from });
120
138
 
121
- initialize_database(database_path);
122
- initialize_auth_manager();
123
- await initialize_api_key_manager();
124
- this.log.info('Database and auth manager initialized');
139
+ const restore_result = await restore_backup(this.settings.restore_from);
125
140
 
126
- // NOTE: Handle startup restore if configured.
127
- if (this.settings?.restore_from) {
128
- try {
129
- this.log.info('Startup restore requested', { backup_filename: this.settings.restore_from });
130
-
131
- const restore_result = await restore_backup(this.settings.restore_from);
132
-
133
- this.log.info('Startup restore completed', {
134
- backup_filename: this.settings.restore_from,
135
- duration_ms: restore_result.duration_ms
136
- });
137
-
138
- // NOTE: Remove restore_from from settings after successful restore.
139
- const updated_settings = { ...this.settings };
140
- delete updated_settings.restore_from;
141
-
142
- writeFileSync(this.settings_file, JSON.stringify(updated_settings, null, 2));
143
-
144
- // NOTE: Reload settings to reflect the change.
145
- this.settings = load_settings(this.settings_file);
146
-
147
- this.log.info('Removed restore_from from settings after successful restore');
148
- } catch (restore_error) {
149
- this.log.error('Startup restore failed', {
150
- backup_filename: this.settings.restore_from,
151
- error: restore_error.message
152
- });
153
-
154
- // NOTE: Continue with existing database if restore fails.
155
- this.log.info('Continuing with existing database after restore failure');
156
- }
157
- }
141
+ this.log.info('Startup restore completed', {
142
+ backup_filename: this.settings.restore_from,
143
+ duration_ms: restore_result.duration_ms
144
+ });
158
145
 
159
- // NOTE: Start backup scheduling if S3 is configured.
160
- if (this.settings?.s3) {
161
- try {
162
- start_backup_schedule();
163
- this.log.info('Backup scheduling started');
164
- } catch (backup_schedule_error) {
165
- this.log.warn('Failed to start backup scheduling', { error: backup_schedule_error.message });
166
- }
167
- }
146
+ this.remove_restore_from_settings();
168
147
 
169
- // NOTE: Start HTTP server for setup if authentication is not configured.
170
- if (is_setup_required()) {
171
- try {
172
- const { http_port } = get_port_configuration();
173
- const http_server_instance = await start_http_server(http_port);
174
- if (http_server_instance) {
175
- this.log.info('HTTP setup server started', { http_port });
176
- }
177
- } catch (http_error) {
178
- this.log.warn('Failed to start HTTP setup server', { error: http_error.message });
179
- }
180
- }
148
+ } catch (restore_error) {
149
+ this.log.error('Startup restore failed', {
150
+ backup_filename: this.settings.restore_from,
151
+ error: restore_error.message
152
+ });
181
153
 
182
- for (let i = 0; i < this.worker_count; i++) {
183
- this.spawn_worker();
154
+ this.log.info('Continuing with existing database after restore failure');
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Removes restore_from setting after successful restore.
160
+ */
161
+ remove_restore_from_settings() {
162
+ const updated_settings = { ...this.settings };
163
+ delete updated_settings.restore_from;
164
+
165
+ writeFileSync(this.settings_file, JSON.stringify(updated_settings, null, 2));
166
+ this.settings = load_settings(this.settings_file);
167
+
168
+ this.log.info('Removed restore_from from settings after successful restore');
169
+ }
170
+
171
+ /**
172
+ * Starts backup scheduling if S3 is configured.
173
+ */
174
+ start_backup_scheduling() {
175
+ if (!this.settings?.s3) {
176
+ return;
177
+ }
178
+
179
+ try {
180
+ start_backup_schedule();
181
+ this.log.info('Backup scheduling started');
182
+ } catch (backup_schedule_error) {
183
+ this.log.warn('Failed to start backup scheduling', { error: backup_schedule_error.message });
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Starts HTTP setup server if authentication is not configured.
189
+ */
190
+ async start_setup_server() {
191
+ if (!is_setup_required()) {
192
+ return;
193
+ }
194
+
195
+ try {
196
+ const { http_port } = get_port_configuration();
197
+ const http_server_instance = await start_http_server(http_port);
198
+ if (http_server_instance) {
199
+ this.log.info('HTTP setup server started', { http_port });
184
200
  }
201
+ } catch (http_error) {
202
+ this.log.warn('Failed to start HTTP setup server', { error: http_error.message });
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Spawns all worker processes.
208
+ */
209
+ spawn_all_workers() {
210
+ for (let i = 0; i < this.worker_count; i++) {
211
+ this.spawn_worker();
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Displays development mode startup message if applicable.
217
+ */
218
+ display_development_message() {
219
+ if (is_development_mode()) {
220
+ const { tcp_port, http_port } = get_port_configuration();
221
+ display_development_startup_message(tcp_port, http_port);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Starts the cluster master and spawns worker processes.
227
+ * @throws {Error} When startup fails
228
+ */
229
+ async start() {
230
+ const start_time = Date.now();
231
+
232
+ try {
233
+ this.settings = load_settings(this.settings_file);
234
+ this.log.info('Settings loaded successfully', { settings_file: this.settings_file });
185
235
 
186
- // NOTE: Display development mode startup message if in development.
187
- if (is_development_mode()) {
188
- const { tcp_port, http_port } = get_port_configuration();
189
- display_development_startup_message(tcp_port, http_port);
190
- }
236
+ await this.initialize_core_systems();
237
+ await this.handle_startup_restore();
238
+ this.start_backup_scheduling();
239
+ await this.start_setup_server();
240
+ this.spawn_all_workers();
241
+ this.display_development_message();
191
242
 
192
243
  const duration_ms = Date.now() - start_time;
193
244
  this.log.info('Master process started successfully', {
@@ -657,30 +708,33 @@ await client.ping();
657
708
  }
658
709
 
659
710
  /**
660
- * Performs graceful shutdown of the cluster master and all workers.
711
+ * Stops HTTP server gracefully.
661
712
  */
662
- async shutdown() {
663
- const shutdown_start = Date.now();
664
- this.log.info('Initiating graceful shutdown');
665
- this.shutting_down = true;
666
-
667
- // NOTE: Stop HTTP server.
713
+ async stop_http_server_gracefully() {
668
714
  try {
669
715
  await stop_http_server();
670
716
  this.log.info('HTTP server stopped');
671
717
  } catch (http_stop_error) {
672
718
  this.log.warn('Failed to stop HTTP server', { error: http_stop_error.message });
673
719
  }
674
-
675
- // NOTE: Stop backup scheduling.
720
+ }
721
+
722
+ /**
723
+ * Stops backup scheduling gracefully.
724
+ */
725
+ stop_backup_scheduling_gracefully() {
676
726
  try {
677
727
  stop_backup_schedule();
678
728
  this.log.info('Backup scheduling stopped');
679
729
  } catch (backup_stop_error) {
680
730
  this.log.warn('Failed to stop backup scheduling', { error: backup_stop_error.message });
681
731
  }
682
-
683
- // NOTE: Send shutdown signal to all workers.
732
+ }
733
+
734
+ /**
735
+ * Sends shutdown signal to all workers.
736
+ */
737
+ send_shutdown_signals() {
684
738
  for (const [worker_id, worker_info] of this.workers) {
685
739
  try {
686
740
  worker_info.worker.send({ type: 'shutdown' });
@@ -688,28 +742,37 @@ await client.ping();
688
742
  this.log.warn('Error sending shutdown signal to worker', { worker_id, error: error.message });
689
743
  }
690
744
  }
691
-
692
- // NOTE: Wait for pending writes to complete.
693
- if (this.write_queue.length > 0) {
694
- this.log.info('Waiting for pending writes to complete', {
695
- pending_writes: this.write_queue.length
696
- });
697
- await new Promise((resolve) => {
698
- const timeout = setTimeout(() => {
699
- this.log.warn('Timeout waiting for writes to complete, proceeding with shutdown');
700
- resolve();
701
- }, process.env.NODE_ENV === 'test' ? 1000 : 5000);
702
-
703
- this.once('writes_completed', () => {
704
- clearTimeout(timeout);
705
- resolve();
706
- });
707
- });
745
+ }
746
+
747
+ /**
748
+ * Waits for pending writes to complete with timeout.
749
+ */
750
+ async wait_for_pending_writes() {
751
+ if (this.write_queue.length === 0) {
752
+ return;
708
753
  }
709
-
710
- this.log.info('All writes completed, disconnecting workers');
711
-
712
- // NOTE: Disconnect all workers.
754
+
755
+ this.log.info('Waiting for pending writes to complete', {
756
+ pending_writes: this.write_queue.length
757
+ });
758
+
759
+ await new Promise((resolve) => {
760
+ const timeout = setTimeout(() => {
761
+ this.log.warn('Timeout waiting for writes to complete, proceeding with shutdown');
762
+ resolve();
763
+ }, process.env.NODE_ENV === 'test' ? 1000 : 5000);
764
+
765
+ this.once('writes_completed', () => {
766
+ clearTimeout(timeout);
767
+ resolve();
768
+ });
769
+ });
770
+ }
771
+
772
+ /**
773
+ * Disconnects all workers gracefully.
774
+ */
775
+ disconnect_all_workers() {
713
776
  for (const [worker_id, worker_info] of this.workers) {
714
777
  try {
715
778
  worker_info.worker.disconnect();
@@ -717,22 +780,32 @@ await client.ping();
717
780
  this.log.warn('Error disconnecting worker', { worker_id, error: error.message });
718
781
  }
719
782
  }
720
-
721
- // NOTE: Wait for workers to exit gracefully, but with a timeout for forced termination.
783
+ }
784
+
785
+ /**
786
+ * Force kills remaining workers after timeout.
787
+ */
788
+ force_kill_remaining_workers() {
789
+ for (const [worker_id, worker_info] of this.workers) {
790
+ this.log.warn('Force killing worker after timeout', { worker_id });
791
+ try {
792
+ worker_info.worker.kill('SIGKILL');
793
+ } catch (error) {
794
+ this.log.warn('Error force killing worker', { worker_id, error: error.message });
795
+ }
796
+ }
797
+ this.workers.clear();
798
+ }
799
+
800
+ /**
801
+ * Waits for workers to exit gracefully with timeout.
802
+ */
803
+ async wait_for_workers_to_exit() {
722
804
  const graceful_timeout = process.env.NODE_ENV === 'test' ? 500 : 3000;
723
805
 
724
806
  await new Promise((resolve) => {
725
807
  const timeout = setTimeout(() => {
726
- // NOTE: Force kill any remaining workers.
727
- for (const [worker_id, worker_info] of this.workers) {
728
- this.log.warn('Force killing worker after timeout', { worker_id });
729
- try {
730
- worker_info.worker.kill('SIGKILL');
731
- } catch (error) {
732
- this.log.warn('Error force killing worker', { worker_id, error: error.message });
733
- }
734
- }
735
- this.workers.clear();
808
+ this.force_kill_remaining_workers();
736
809
  resolve();
737
810
  }, graceful_timeout);
738
811
 
@@ -746,49 +819,85 @@ await client.ping();
746
819
  };
747
820
  check_workers();
748
821
  });
749
-
750
- // NOTE: Clean up database connections in master process.
822
+ }
823
+
824
+ /**
825
+ * Cleans up database connections.
826
+ */
827
+ cleanup_database_connections() {
751
828
  try {
752
829
  cleanup_database();
753
830
  this.log.info('Database cleanup completed');
754
831
  } catch (cleanup_error) {
755
832
  this.log.warn('Error during database cleanup', { error: cleanup_error.message });
756
833
  }
757
-
758
- // NOTE: Clear all internal state.
834
+ }
835
+
836
+ /**
837
+ * Clears all internal state.
838
+ */
839
+ clear_internal_state() {
759
840
  this.authenticated_sessions.clear();
760
841
  this.write_queue.length = 0;
761
842
  this.pending_writes.clear();
762
-
763
- // NOTE: In test environment, aggressively clean up cluster state to prevent worker reuse.
764
- if (process.env.NODE_ENV === 'test') {
765
- try {
766
- // Force kill any remaining workers in the global cluster state
767
- for (const worker_id in cluster.workers) {
768
- const worker = cluster.workers[worker_id];
769
- if (worker && !worker.isDead()) {
770
- this.log.info('Force killing remaining cluster worker', { worker_id, worker_pid: worker.process.pid });
771
- try {
772
- worker.kill('SIGKILL');
773
- } catch (kill_error) {
774
- this.log.warn('Error force killing remaining worker', { worker_id, error: kill_error.message });
775
- }
843
+ }
844
+
845
+ /**
846
+ * Performs aggressive cleanup for test environment.
847
+ */
848
+ perform_test_environment_cleanup() {
849
+ if (process.env.NODE_ENV !== 'test') {
850
+ return;
851
+ }
852
+
853
+ try {
854
+ // Force kill any remaining workers in the global cluster state
855
+ for (const worker_id in cluster.workers) {
856
+ const worker = cluster.workers[worker_id];
857
+ if (worker && !worker.isDead()) {
858
+ this.log.info('Force killing remaining cluster worker', { worker_id, worker_pid: worker.process.pid });
859
+ try {
860
+ worker.kill('SIGKILL');
861
+ } catch (kill_error) {
862
+ this.log.warn('Error force killing remaining worker', { worker_id, error: kill_error.message });
776
863
  }
777
864
  }
778
-
779
- // Clear the global cluster workers registry
780
- for (const worker_id in cluster.workers) {
781
- delete cluster.workers[worker_id];
782
- }
783
-
784
- // Remove all cluster event listeners to prevent memory leaks
785
- cluster.removeAllListeners();
786
-
787
- this.log.info('Aggressive cluster cleanup completed for test environment');
788
- } catch (cleanup_error) {
789
- this.log.warn('Error during aggressive cluster cleanup', { error: cleanup_error.message });
790
865
  }
866
+
867
+ // Clear the global cluster workers registry
868
+ for (const worker_id in cluster.workers) {
869
+ delete cluster.workers[worker_id];
870
+ }
871
+
872
+ // Remove all cluster event listeners to prevent memory leaks
873
+ cluster.removeAllListeners();
874
+
875
+ this.log.info('Aggressive cluster cleanup completed for test environment');
876
+ } catch (cleanup_error) {
877
+ this.log.warn('Error during aggressive cluster cleanup', { error: cleanup_error.message });
791
878
  }
879
+ }
880
+
881
+ /**
882
+ * Performs graceful shutdown of the cluster master and all workers.
883
+ */
884
+ async shutdown() {
885
+ const shutdown_start = Date.now();
886
+ this.log.info('Initiating graceful shutdown');
887
+ this.shutting_down = true;
888
+
889
+ await this.stop_http_server_gracefully();
890
+ this.stop_backup_scheduling_gracefully();
891
+ this.send_shutdown_signals();
892
+ await this.wait_for_pending_writes();
893
+
894
+ this.log.info('All writes completed, disconnecting workers');
895
+ this.disconnect_all_workers();
896
+ await this.wait_for_workers_to_exit();
897
+
898
+ this.cleanup_database_connections();
899
+ this.clear_internal_state();
900
+ this.perform_test_environment_cleanup();
792
901
 
793
902
  const shutdown_duration = Date.now() - shutdown_start;
794
903
  this.log.info('Shutdown complete', { shutdown_duration_ms: shutdown_duration });
@@ -196,29 +196,37 @@ class ClusterWorker {
196
196
  });
197
197
  }
198
198
 
199
+ /**
200
+ * Validates and processes a single parsed message.
201
+ * @param {Object} socket - Socket connection
202
+ * @param {Object} parsed_data - Parsed message data
203
+ * @returns {boolean} True if message was processed successfully
204
+ */
205
+ process_single_message(socket, parsed_data) {
206
+ const op_type = parsed_data?.op || null;
207
+
208
+ if (!op_type) {
209
+ send_error(socket, { message: 'Missing operation type' });
210
+ return false;
211
+ }
212
+
213
+ const is_valid_op_type = this.check_op_type(op_type);
214
+
215
+ if (!is_valid_op_type) {
216
+ send_error(socket, { message: 'Invalid operation type' });
217
+ return false;
218
+ }
219
+
220
+ this.route_operation(socket, op_type, parsed_data?.data || {});
221
+ return true;
222
+ }
223
+
199
224
  handle_socket_data(socket, raw_data) {
200
225
  try {
201
- // NOTE: Use the message parser to handle length-prefixed MessagePack messages.
202
226
  const messages = socket.message_parser.parse_messages(raw_data);
203
227
 
204
228
  for (const message of messages) {
205
- // NOTE: Message is already decoded from MessagePack by the message parser.
206
- const parsed_data = message;
207
- const op_type = parsed_data?.op || null;
208
-
209
- if (!op_type) {
210
- send_error(socket, { message: 'Missing operation type' });
211
- continue;
212
- }
213
-
214
- const is_valid_op_type = this.check_op_type(op_type);
215
-
216
- if (!is_valid_op_type) {
217
- send_error(socket, { message: 'Invalid operation type' });
218
- continue;
219
- }
220
-
221
- this.route_operation(socket, op_type, parsed_data?.data || {});
229
+ this.process_single_message(socket, message);
222
230
  }
223
231
 
224
232
  } catch (error) {