@joystick.js/db-canary 0.0.0-canary.2221 → 0.0.0-canary.2223

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.
@@ -1,10 +1,10 @@
1
- import a from"cluster";import l from"os";import{EventEmitter as u}from"events";import{writeFileSync as p}from"fs";import{load_settings as _,get_port_configuration as h}from"../lib/load_settings.js";import{restore_backup as w,start_backup_schedule as g,stop_backup_schedule as f}from"../lib/backup_manager.js";import m from"../lib/logger.js";import{initialize_database as k,check_and_grow_map_size as y,cleanup_database as b}from"../lib/query_engine.js";import{setup_authentication as v,verify_password as q,initialize_auth_manager as D}from"../lib/auth_manager.js";import x from"../lib/operations/insert_one.js";import E from"../lib/operations/update_one.js";import T from"../lib/operations/delete_one.js";import S from"../lib/operations/bulk_write.js";import W from"../lib/operations/find_one.js";import F from"../lib/operations/find.js";import N from"../lib/operations/create_index.js";import P from"../lib/operations/drop_index.js";import z from"../lib/operations/get_indexes.js";import{start_http_server as O,stop_http_server as R,is_setup_required as A}from"../lib/http_server.js";import{is_development_mode as I,display_development_startup_message as j}from"../lib/development_mode.js";import{initialize_api_key_manager as L}from"../lib/api_key_manager.js";class M extends u{constructor(t={}){super(),this.workers=new Map,this.write_queue=[],this.processing_writes=!1,this.authenticated_sessions=new Map,this.worker_count=t.worker_count||l.cpus().length,this.port=t.port||1983,this.settings_file=t.settings_file||"settings.db.json",this.settings=null,this.pending_writes=new Map,this.write_id_counter=0,this.shutting_down=!1,this.master_id=`master_${Date.now()}_${Math.random()}`;const{create_context_logger:e}=m("master");this.log=e({port:this.port,worker_count:this.worker_count,master_id:this.master_id}),this.setup_master()}setup_master(){a.setupPrimary({exec:new URL("./index.js",import.meta.url).pathname,args:[],silent:!1}),a.on("exit",(t,e,r)=>{this.log.warn("Worker died",{worker_pid:t.process.pid,exit_code:e,signal:r}),this.handle_worker_death(t)}),a.on("message",(t,e)=>{this.handle_worker_message(t,e)})}async start(){const t=Date.now();try{if(this.settings=_(this.settings_file),this.log.info("Settings loaded successfully",{settings_file:this.settings_file}),k(),D(),await L(),this.log.info("Database and auth manager initialized"),this.settings?.restore_from)try{this.log.info("Startup restore requested",{backup_filename:this.settings.restore_from});const r=await w(this.settings.restore_from);this.log.info("Startup restore completed",{backup_filename:this.settings.restore_from,duration_ms:r.duration_ms});const s={...this.settings};delete s.restore_from,p(this.settings_file,JSON.stringify(s,null,2)),this.settings=_(this.settings_file),this.log.info("Removed restore_from from settings after successful restore")}catch(r){this.log.error("Startup restore failed",{backup_filename:this.settings.restore_from,error:r.message}),this.log.info("Continuing with existing database after restore failure")}if(this.settings?.s3)try{g(),this.log.info("Backup scheduling started")}catch(r){this.log.warn("Failed to start backup scheduling",{error:r.message})}if(A())try{const{http_port:r}=h();await O(r)&&this.log.info("HTTP setup server started",{http_port:r})}catch(r){this.log.warn("Failed to start HTTP setup server",{error:r.message})}for(let r=0;r<this.worker_count;r++)this.spawn_worker();if(I()){const{tcp_port:r,http_port:s}=h();j(r,s)}const e=Date.now()-t;this.log.info("Master process started successfully",{workers_spawned:this.worker_count,startup_duration_ms:e})}catch(e){this.log.error("Failed to start master process",{error:e.message}),process.exit(1)}}spawn_worker(){const t=Date.now();this.log.info("Spawning worker");const e=a.fork({WORKER_PORT:this.port,WORKER_SETTINGS:JSON.stringify(this.settings)});this.workers.set(e.id,{worker:e,connections:0,last_heartbeat:Date.now(),status:"starting"});const r=Date.now()-t;return this.log.info("Worker spawned successfully",{worker_id:e.id,worker_pid:e.process.pid,spawn_duration_ms:r}),e}handle_worker_death(t){this.workers.delete(t.id),this.shutting_down||(this.log.info("Respawning worker after death",{dead_worker_id:t.id,respawn_delay_ms:1e3}),setTimeout(()=>{this.spawn_worker()},1e3))}handle_worker_message(t,e){switch(e.type){case"worker_ready":this.handle_worker_ready_for_config(t,e);break;case"server_ready":this.handle_worker_server_ready(t,e);break;case"write_request":this.handle_write_request(t,e);break;case"auth_request":this.handle_auth_request(t,e);break;case"setup_request":this.handle_setup_request(t,e);break;case"connection_count":this.update_worker_connections(t,e);break;case"heartbeat":this.handle_worker_heartbeat(t,e);break;default:this.log.warn("Unknown message type received",{message_type:e.type,worker_id:t.id})}}handle_worker_ready_for_config(t,e){this.log.info("Worker ready for config, sending configuration",{worker_id:t.id,worker_pid:t.process.pid,master_id:this.master_id}),t.send({type:"config",data:{port:this.port,settings:this.settings,master_id:this.master_id}})}handle_worker_server_ready(t,e){const r=this.workers.get(t.id);r&&(r.status="ready",this.log.info("Worker server ready",{worker_id:t.id,worker_pid:t.process.pid}))}async handle_write_request(t,e){if(this.shutting_down){t.send({type:"write_response",data:{write_id:e.data.write_id,success:!1,error:"Server is shutting down"}});return}const{write_id:r,op_type:s,data:o,socket_id:i}=e.data;try{const n={write_id:r,worker_id:t.id,op_type:s,data:o,socket_id:i,timestamp:Date.now()};this.write_queue.push(n),this.process_write_queue()}catch(n){t.send({type:"write_response",data:{write_id:r,success:!1,error:n.message}})}}async process_write_queue(){if(!(this.processing_writes||this.write_queue.length===0)){for(this.processing_writes=!0;this.write_queue.length>0;){const t=this.write_queue.shift();await this.execute_write_operation(t)}this.processing_writes=!1,this.shutting_down&&this.write_queue.length===0&&this.emit("writes_completed")}}async execute_write_operation(t){const{write_id:e,worker_id:r,op_type:s,data:o,socket_id:i}=t,n=this.workers.get(r);if(!n){this.log.error("Worker not found for write operation",{worker_id:r});return}try{const c=await this.perform_database_operation(s,o);n.worker.send({type:"write_response",data:{write_id:e,success:!0,result:c}}),this.broadcast_write_notification(s,o,r)}catch(c){this.log.error("Write operation failed",{write_id:e,op_type:s,worker_id:r,error_message:c.message}),n.worker.send({type:"write_response",data:{write_id:e,success:!1,error:c.message}})}}async perform_database_operation(t,e){const r=Date.now();this.log.info("Executing database operation",{op_type:t});try{let s;const o=e.database||"default";switch(t){case"insert_one":s=await x(o,e.collection,e.document,e.options);break;case"update_one":s=await E(o,e.collection,e.filter,e.update,e.options);break;case"delete_one":s=await T(o,e.collection,e.filter,e.options);break;case"bulk_write":s=await S(o,e.collection,e.operations,e.options);break;case"find_one":s=await W(o,e.collection,e.filter,e.options);break;case"find":s=await F(o,e.collection,e.filter,e.options);break;case"create_index":s=await N(o,e.collection,e.field,e.options);break;case"drop_index":s=await P(o,e.collection,e.field);break;case"get_indexes":s=await z(o,e.collection);break;default:throw new Error(`Unsupported database operation: ${t}`)}const i=Date.now()-r;return this.log.log_operation(t,i,{result:s}),["find_one","find","get_indexes"].includes(t)||setImmediate(()=>y()),s}catch(s){const o=Date.now()-r;throw this.log.error("Database operation failed",{op_type:t,duration_ms:o,error_message:s.message}),s}}broadcast_write_notification(t,e,r){const s={type:"write_notification",data:{op_type:t,data:e,timestamp:Date.now()}};for(const[o,i]of this.workers)o!==r&&i.status==="ready"&&i.worker.send(s)}async handle_auth_request(t,e){const{auth_id:r,socket_id:s,password:o}=e.data;try{const i=await q(o,"cluster_client");i&&this.authenticated_sessions.set(s,{authenticated_at:Date.now(),worker_id:t.id}),t.send({type:"auth_response",data:{auth_id:r,success:i,message:i?"Authentication successful":"Authentication failed"}})}catch(i){t.send({type:"auth_response",data:{auth_id:r,success:!1,message:`Authentication error: ${i.message}`}})}}handle_setup_request(t,e){const{setup_id:r,socket_id:s}=e.data;try{const o=v(),i=`===
1
+ import a from"cluster";import l from"os";import{EventEmitter as u}from"events";import{writeFileSync as p}from"fs";import{load_settings as _,get_settings as w,get_port_configuration as h}from"../lib/load_settings.js";import{restore_backup as g,start_backup_schedule as f,stop_backup_schedule as m}from"../lib/backup_manager.js";import k from"../lib/logger.js";import{initialize_database as y,check_and_grow_map_size as b,cleanup_database as v}from"../lib/query_engine.js";import{setup_authentication as q,verify_password as D,initialize_auth_manager as x}from"../lib/auth_manager.js";import E from"../lib/operations/insert_one.js";import T from"../lib/operations/update_one.js";import S from"../lib/operations/delete_one.js";import W from"../lib/operations/bulk_write.js";import F from"../lib/operations/find_one.js";import N from"../lib/operations/find.js";import P from"../lib/operations/create_index.js";import z from"../lib/operations/drop_index.js";import O from"../lib/operations/get_indexes.js";import{start_http_server as R,stop_http_server as A,is_setup_required as I}from"../lib/http_server.js";import{is_development_mode as j,display_development_startup_message as L}from"../lib/development_mode.js";import{initialize_api_key_manager as M}from"../lib/api_key_manager.js";class $ extends u{constructor(r={}){super(),this.workers=new Map,this.write_queue=[],this.processing_writes=!1,this.authenticated_sessions=new Map,this.worker_count=r.worker_count||l.cpus().length,this.port=r.port||1983,this.settings_file=r.settings_file||"settings.db.json",this.settings=null,this.pending_writes=new Map,this.write_id_counter=0,this.shutting_down=!1,this.master_id=`master_${Date.now()}_${Math.random()}`;const{create_context_logger:e}=k("master");this.log=e({port:this.port,worker_count:this.worker_count,master_id:this.master_id}),this.setup_master()}setup_master(){a.setupPrimary({exec:new URL("./index.js",import.meta.url).pathname,args:[],silent:!1}),a.on("exit",(r,e,o)=>{this.log.warn("Worker died",{worker_pid:r.process.pid,exit_code:e,signal:o}),this.handle_worker_death(r)}),a.on("message",(r,e)=>{this.handle_worker_message(r,e)})}async start(){const r=Date.now();try{this.settings=_(this.settings_file),this.log.info("Settings loaded successfully",{settings_file:this.settings_file});let e="./data";try{const t=w();t?.data_path&&(e=t.data_path)}catch{}if(y(e),x(),await M(),this.log.info("Database and auth manager initialized"),this.settings?.restore_from)try{this.log.info("Startup restore requested",{backup_filename:this.settings.restore_from});const t=await g(this.settings.restore_from);this.log.info("Startup restore completed",{backup_filename:this.settings.restore_from,duration_ms:t.duration_ms});const s={...this.settings};delete s.restore_from,p(this.settings_file,JSON.stringify(s,null,2)),this.settings=_(this.settings_file),this.log.info("Removed restore_from from settings after successful restore")}catch(t){this.log.error("Startup restore failed",{backup_filename:this.settings.restore_from,error:t.message}),this.log.info("Continuing with existing database after restore failure")}if(this.settings?.s3)try{f(),this.log.info("Backup scheduling started")}catch(t){this.log.warn("Failed to start backup scheduling",{error:t.message})}if(I())try{const{http_port:t}=h();await R(t)&&this.log.info("HTTP setup server started",{http_port:t})}catch(t){this.log.warn("Failed to start HTTP setup server",{error:t.message})}for(let t=0;t<this.worker_count;t++)this.spawn_worker();if(j()){const{tcp_port:t,http_port:s}=h();L(t,s)}const o=Date.now()-r;this.log.info("Master process started successfully",{workers_spawned:this.worker_count,startup_duration_ms:o})}catch(e){this.log.error("Failed to start master process",{error:e.message}),process.exit(1)}}spawn_worker(){const r=Date.now();this.log.info("Spawning worker");const e=a.fork({WORKER_PORT:this.port,WORKER_SETTINGS:JSON.stringify(this.settings)});this.workers.set(e.id,{worker:e,connections:0,last_heartbeat:Date.now(),status:"starting"});const o=Date.now()-r;return this.log.info("Worker spawned successfully",{worker_id:e.id,worker_pid:e.process.pid,spawn_duration_ms:o}),e}handle_worker_death(r){this.workers.delete(r.id),this.shutting_down||(this.log.info("Respawning worker after death",{dead_worker_id:r.id,respawn_delay_ms:1e3}),setTimeout(()=>{this.spawn_worker()},1e3))}handle_worker_message(r,e){switch(e.type){case"worker_ready":this.handle_worker_ready_for_config(r,e);break;case"server_ready":this.handle_worker_server_ready(r,e);break;case"write_request":this.handle_write_request(r,e);break;case"auth_request":this.handle_auth_request(r,e);break;case"setup_request":this.handle_setup_request(r,e);break;case"connection_count":this.update_worker_connections(r,e);break;case"heartbeat":this.handle_worker_heartbeat(r,e);break;default:this.log.warn("Unknown message type received",{message_type:e.type,worker_id:r.id})}}handle_worker_ready_for_config(r,e){this.log.info("Worker ready for config, sending configuration",{worker_id:r.id,worker_pid:r.process.pid,master_id:this.master_id}),r.send({type:"config",data:{port:this.port,settings:this.settings,master_id:this.master_id}})}handle_worker_server_ready(r,e){const o=this.workers.get(r.id);o&&(o.status="ready",this.log.info("Worker server ready",{worker_id:r.id,worker_pid:r.process.pid}))}async handle_write_request(r,e){if(this.shutting_down){r.send({type:"write_response",data:{write_id:e.data.write_id,success:!1,error:"Server is shutting down"}});return}const{write_id:o,op_type:t,data:s,socket_id:i}=e.data;try{const n={write_id:o,worker_id:r.id,op_type:t,data:s,socket_id:i,timestamp:Date.now()};this.write_queue.push(n),this.process_write_queue()}catch(n){r.send({type:"write_response",data:{write_id:o,success:!1,error:n.message}})}}async process_write_queue(){if(!(this.processing_writes||this.write_queue.length===0)){for(this.processing_writes=!0;this.write_queue.length>0;){const r=this.write_queue.shift();await this.execute_write_operation(r)}this.processing_writes=!1,this.shutting_down&&this.write_queue.length===0&&this.emit("writes_completed")}}async execute_write_operation(r){const{write_id:e,worker_id:o,op_type:t,data:s,socket_id:i}=r,n=this.workers.get(o);if(!n){this.log.error("Worker not found for write operation",{worker_id:o});return}try{const c=await this.perform_database_operation(t,s);n.worker.send({type:"write_response",data:{write_id:e,success:!0,result:c}}),this.broadcast_write_notification(t,s,o)}catch(c){this.log.error("Write operation failed",{write_id:e,op_type:t,worker_id:o,error_message:c.message}),n.worker.send({type:"write_response",data:{write_id:e,success:!1,error:c.message}})}}async perform_database_operation(r,e){const o=Date.now();this.log.info("Executing database operation",{op_type:r});try{let t;const s=e.database||"default";switch(r){case"insert_one":t=await E(s,e.collection,e.document,e.options);break;case"update_one":t=await T(s,e.collection,e.filter,e.update,e.options);break;case"delete_one":t=await S(s,e.collection,e.filter,e.options);break;case"bulk_write":t=await W(s,e.collection,e.operations,e.options);break;case"find_one":t=await F(s,e.collection,e.filter,e.options);break;case"find":t=await N(s,e.collection,e.filter,e.options);break;case"create_index":t=await P(s,e.collection,e.field,e.options);break;case"drop_index":t=await z(s,e.collection,e.field);break;case"get_indexes":t=await O(s,e.collection);break;default:throw new Error(`Unsupported database operation: ${r}`)}const i=Date.now()-o;return this.log.log_operation(r,i,{result:t}),["find_one","find","get_indexes"].includes(r)||setImmediate(()=>b()),t}catch(t){const s=Date.now()-o;throw this.log.error("Database operation failed",{op_type:r,duration_ms:s,error_message:t.message}),t}}broadcast_write_notification(r,e,o){const t={type:"write_notification",data:{op_type:r,data:e,timestamp:Date.now()}};for(const[s,i]of this.workers)s!==o&&i.status==="ready"&&i.worker.send(t)}async handle_auth_request(r,e){const{auth_id:o,socket_id:t,password:s}=e.data;try{const i=await D(s,"cluster_client");i&&this.authenticated_sessions.set(t,{authenticated_at:Date.now(),worker_id:r.id}),r.send({type:"auth_response",data:{auth_id:o,success:i,message:i?"Authentication successful":"Authentication failed"}})}catch(i){r.send({type:"auth_response",data:{auth_id:o,success:!1,message:`Authentication error: ${i.message}`}})}}handle_setup_request(r,e){const{setup_id:o,socket_id:t}=e.data;try{const s=q(),i=`===
2
2
  JoystickDB Setup
3
3
 
4
4
  Your database has been setup. Follow the instructions below carefully to avoid issues.
5
5
 
6
6
  Password:
7
- ${o}
7
+ ${s}
8
8
 
9
9
  Store this password in your environment settings or another secure location. When connecting a client, make sure to provide this password via the client's options object like this:
10
10
 
@@ -13,8 +13,8 @@ import joystickdb from '@joystickdb/client';
13
13
  const client = joystickdb.client({
14
14
  host: 'localhost',
15
15
  port: 1983,
16
- password: '${o}'
16
+ password: '${s}'
17
17
  });
18
18
 
19
19
  await client.ping();
20
- ===`;t.send({type:"setup_response",data:{setup_id:r,success:!0,password:o,instructions:i,message:"Authentication setup completed successfully"}})}catch(o){t.send({type:"setup_response",data:{setup_id:r,success:!1,error:o.message}})}}update_worker_connections(t,e){const r=this.workers.get(t.id);r&&(r.connections=e.data.count)}handle_worker_heartbeat(t,e){const r=this.workers.get(t.id);r&&(r.last_heartbeat=Date.now())}get_cluster_stats(){const t={master_pid:process.pid,worker_count:this.workers.size,total_connections:0,write_queue_length:this.write_queue.length,authenticated_sessions:this.authenticated_sessions.size,workers:[]};for(const[e,r]of this.workers)t.total_connections+=r.connections,t.workers.push({id:e,pid:r.worker.process.pid,connections:r.connections,status:r.status,last_heartbeat:r.last_heartbeat});return t}async shutdown(){const t=Date.now();this.log.info("Initiating graceful shutdown"),this.shutting_down=!0;try{await R(),this.log.info("HTTP server stopped")}catch(s){this.log.warn("Failed to stop HTTP server",{error:s.message})}try{f(),this.log.info("Backup scheduling stopped")}catch(s){this.log.warn("Failed to stop backup scheduling",{error:s.message})}for(const[s,o]of this.workers)try{o.worker.send({type:"shutdown"})}catch(i){this.log.warn("Error sending shutdown signal to worker",{worker_id:s,error:i.message})}this.write_queue.length>0&&(this.log.info("Waiting for pending writes to complete",{pending_writes:this.write_queue.length}),await new Promise(s=>{const o=setTimeout(()=>{this.log.warn("Timeout waiting for writes to complete, proceeding with shutdown"),s()},process.env.NODE_ENV==="test"?1e3:5e3);this.once("writes_completed",()=>{clearTimeout(o),s()})})),this.log.info("All writes completed, disconnecting workers");for(const[s,o]of this.workers)try{o.worker.disconnect()}catch(i){this.log.warn("Error disconnecting worker",{worker_id:s,error:i.message})}const e=process.env.NODE_ENV==="test"?500:3e3;await new Promise(s=>{const o=setTimeout(()=>{for(const[n,c]of this.workers){this.log.warn("Force killing worker after timeout",{worker_id:n});try{c.worker.kill("SIGKILL")}catch(d){this.log.warn("Error force killing worker",{worker_id:n,error:d.message})}}this.workers.clear(),s()},e),i=()=>{this.workers.size===0?(clearTimeout(o),s()):setTimeout(i,50)};i()});try{b(),this.log.info("Database cleanup completed")}catch(s){this.log.warn("Error during database cleanup",{error:s.message})}if(this.authenticated_sessions.clear(),this.write_queue.length=0,this.pending_writes.clear(),process.env.NODE_ENV==="test")try{for(const s in a.workers){const o=a.workers[s];if(o&&!o.isDead()){this.log.info("Force killing remaining cluster worker",{worker_id:s,worker_pid:o.process.pid});try{o.kill("SIGKILL")}catch(i){this.log.warn("Error force killing remaining worker",{worker_id:s,error:i.message})}}}for(const s in a.workers)delete a.workers[s];a.removeAllListeners(),this.log.info("Aggressive cluster cleanup completed for test environment")}catch(s){this.log.warn("Error during aggressive cluster cleanup",{error:s.message})}const r=Date.now()-t;this.log.info("Shutdown complete",{shutdown_duration_ms:r})}}var he=M;export{he as default};
20
+ ===`;r.send({type:"setup_response",data:{setup_id:o,success:!0,password:s,instructions:i,message:"Authentication setup completed successfully"}})}catch(s){r.send({type:"setup_response",data:{setup_id:o,success:!1,error:s.message}})}}update_worker_connections(r,e){const o=this.workers.get(r.id);o&&(o.connections=e.data.count)}handle_worker_heartbeat(r,e){const o=this.workers.get(r.id);o&&(o.last_heartbeat=Date.now())}get_cluster_stats(){const r={master_pid:process.pid,worker_count:this.workers.size,total_connections:0,write_queue_length:this.write_queue.length,authenticated_sessions:this.authenticated_sessions.size,workers:[]};for(const[e,o]of this.workers)r.total_connections+=o.connections,r.workers.push({id:e,pid:o.worker.process.pid,connections:o.connections,status:o.status,last_heartbeat:o.last_heartbeat});return r}async shutdown(){const r=Date.now();this.log.info("Initiating graceful shutdown"),this.shutting_down=!0;try{await A(),this.log.info("HTTP server stopped")}catch(t){this.log.warn("Failed to stop HTTP server",{error:t.message})}try{m(),this.log.info("Backup scheduling stopped")}catch(t){this.log.warn("Failed to stop backup scheduling",{error:t.message})}for(const[t,s]of this.workers)try{s.worker.send({type:"shutdown"})}catch(i){this.log.warn("Error sending shutdown signal to worker",{worker_id:t,error:i.message})}this.write_queue.length>0&&(this.log.info("Waiting for pending writes to complete",{pending_writes:this.write_queue.length}),await new Promise(t=>{const s=setTimeout(()=>{this.log.warn("Timeout waiting for writes to complete, proceeding with shutdown"),t()},process.env.NODE_ENV==="test"?1e3:5e3);this.once("writes_completed",()=>{clearTimeout(s),t()})})),this.log.info("All writes completed, disconnecting workers");for(const[t,s]of this.workers)try{s.worker.disconnect()}catch(i){this.log.warn("Error disconnecting worker",{worker_id:t,error:i.message})}const e=process.env.NODE_ENV==="test"?500:3e3;await new Promise(t=>{const s=setTimeout(()=>{for(const[n,c]of this.workers){this.log.warn("Force killing worker after timeout",{worker_id:n});try{c.worker.kill("SIGKILL")}catch(d){this.log.warn("Error force killing worker",{worker_id:n,error:d.message})}}this.workers.clear(),t()},e),i=()=>{this.workers.size===0?(clearTimeout(s),t()):setTimeout(i,50)};i()});try{v(),this.log.info("Database cleanup completed")}catch(t){this.log.warn("Error during database cleanup",{error:t.message})}if(this.authenticated_sessions.clear(),this.write_queue.length=0,this.pending_writes.clear(),process.env.NODE_ENV==="test")try{for(const t in a.workers){const s=a.workers[t];if(s&&!s.isDead()){this.log.info("Force killing remaining cluster worker",{worker_id:t,worker_pid:s.process.pid});try{s.kill("SIGKILL")}catch(i){this.log.warn("Error force killing remaining worker",{worker_id:t,error:i.message})}}}for(const t in a.workers)delete a.workers[t];a.removeAllListeners(),this.log.info("Aggressive cluster cleanup completed for test environment")}catch(t){this.log.warn("Error during aggressive cluster cleanup",{error:t.message})}const o=Date.now()-r;this.log.info("Shutdown complete",{shutdown_duration_ms:o})}}var de=$;export{de as default};
@@ -1 +1 @@
1
- import h from"net";import p from"../lib/op_types.js";import{send_success as u,send_error as d,send_message as l}from"../lib/send_response.js";import{shutdown_write_queue as g}from"../lib/write_queue.js";import{create_message_parser as w,encode_message as c}from"../lib/tcp_protocol.js";import m from"../lib/logger.js";import{initialize_database as f,cleanup_database as y}from"../lib/query_engine.js";import{handle_admin_operation as b,handle_ping_operation as v}from"../lib/operation_dispatcher.js";class k{constructor(){this.server=null,this.connections=new Map,this.connection_count=0,this.settings=null,this.port=null,this.write_id_counter=0,this.pending_writes=new Map,this.authenticated_clients=new Set,this.heartbeat_interval=null;const{create_context_logger:e}=m("worker");this.log=e({worker_pid:process.pid}),this.setup_worker()}setup_worker(){process.on("message",e=>{this.handle_master_message(e)}),process.on("SIGTERM",()=>{this.shutdown()}),process.on("SIGINT",()=>{this.shutdown()}),this.send_heartbeat(),this.heartbeat_interval=setInterval(()=>{this.send_heartbeat()},5e3),process.connected&&process.send({type:"worker_ready"})}handle_master_message(e){switch(e.type){case"config":this.handle_config(e);break;case"write_response":this.handle_write_response(e);break;case"auth_response":this.handle_auth_response(e);break;case"setup_response":this.handle_setup_response(e);break;case"write_notification":this.handle_write_notification(e);break;case"shutdown":this.shutdown();break;default:this.log.warn("Unknown message type received from master",{message_type:e.type})}}handle_config(e){const t=e.data.master_id;if(this.master_id&&this.master_id!==t){this.log.info("Worker already configured by different master, ignoring config message",{current_master_id:this.master_id,incoming_master_id:t,current_port:this.port,new_port:e.data.port});return}if(this.port!==null&&this.master_id===t){this.log.info("Worker already configured by same master, ignoring duplicate config message",{master_id:t,current_port:this.port,new_port:e.data.port});return}this.log.info("Received config message",{port:e.data.port,master_id:t}),this.port=e.data.port,this.settings=e.data.settings,this.master_id=t;try{f(),this.log.info("Database initialized in worker process")}catch(s){this.log.error("Failed to initialize database in worker process",{error:s.message})}this.log.info("Starting server",{port:this.port}),this.start_server()}start_server(){this.server=h.createServer(e=>{this.handle_connection(e)}),this.server.listen(this.port,()=>{this.log.info("Server listening",{port:this.port}),process.connected&&process.send({type:"server_ready"})}),this.server.on("error",e=>{this.log.error("Server error",{error:e.message})})}handle_connection(e){const t=`${process.pid}_${Date.now()}_${Math.random()}`;e.id=t,e.message_parser=w(),this.connections.set(t,e),this.connection_count++,this.update_connection_count(),e.on("data",s=>{this.handle_socket_data(e,s)}),e.on("end",()=>{this.handle_socket_end(e)}),e.on("error",s=>{this.log.error("Socket error",{socket_id:t,error_message:s.message}),this.handle_socket_end(e)})}handle_socket_data(e,t){try{const s=e.message_parser.parse_messages(t);for(const r of s){const a=r,i=a?.op||null;if(!i){d(e,{message:"Missing operation type"});continue}if(!this.check_op_type(i)){d(e,{message:"Invalid operation type"});continue}this.route_operation(e,i,a?.data||{})}}catch(s){this.log.error("Data parsing error",{socket_id:e.id,error_message:s.message}),d(e,{message:"Invalid data format"})}}handle_socket_end(e){e.id&&(this.connections.delete(e.id),this.authenticated_clients.delete(e.id),this.connection_count--,this.update_connection_count()),this.log.info("Client disconnected",{socket_id:e.id})}check_op_type(e=""){return e?p.includes(e):!1}route_operation(e,t,s){switch(t){case"authentication":this.handle_authentication(e,s);break;case"setup":this.handle_setup(e,s);break;case"find_one":case"find":case"get_indexes":this.handle_read_operation(e,t,s);break;case"create_index":case"drop_index":this.handle_write_operation(e,t,s);break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":this.handle_write_operation(e,t,s);break;case"ping":v(e);break;case"admin":b(e,s,this.is_authenticated.bind(this));break;default:d(e,{message:`Unsupported operation: ${t}`})}}handle_authentication(e,t){if(this.is_authenticated(e))l(e,"Already authenticated");else{const s=`${e.id}_${Date.now()}`;process.send({type:"auth_request",data:{auth_id:s,socket_id:e.id,password:t.password}}),this.pending_writes.set(s,{socket:e,type:"auth"})}}handle_setup(e,t){const s=`${e.id}_${Date.now()}`;process.send({type:"setup_request",data:{setup_id:s,socket_id:e.id}}),this.pending_writes.set(s,{socket:e,type:"setup"})}handle_read_operation(e,t,s){if(!this.is_authenticated(e)){d(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"read"})}handle_write_operation(e,t,s){if(!this.is_authenticated(e)){d(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"write"})}handle_write_response(e){const{write_id:t,success:s,result:r,error:a}=e.data,i=this.pending_writes.get(t);if(!i){this.log.warn("No pending write found",{write_id:t});return}const{socket:o}=i;if(this.pending_writes.delete(t),o.destroyed||!o.writable){this.log.warn("Socket disconnected before response could be sent",{write_id:t});return}try{if(s){let n;Array.isArray(r)?n={ok:1,documents:r}:r&&typeof r=="object"?n={ok:1,...r}:n={ok:1,result:r};const _=c(n);o.write(_)}else{const _=c({ok:0,error:a});o.write(_)}}catch(n){this.log.error("Error sending response to client",{write_id:t,error:n.message})}}handle_auth_response(e){const{auth_id:t,success:s,message:r}=e.data,a=this.pending_writes.get(t);if(!a){this.log.warn("No pending auth found",{auth_id:t});return}const{socket:i}=a;if(this.pending_writes.delete(t),i.destroyed||!i.writable){this.log.warn("Socket disconnected before auth response could be sent",{auth_id:t});return}try{if(s){this.authenticated_clients.add(i.id);const n=c({ok:1,version:"1.0.0",message:r});i.write(n)}else d(i,{message:r}),i.end()}catch(o){this.log.error("Error sending auth response to client",{auth_id:t,error:o.message})}}handle_setup_response(e){const{setup_id:t,success:s,password:r,message:a,error:i}=e.data,o=this.pending_writes.get(t);if(!o){this.log.warn("No pending setup found",{setup_id:t});return}const{socket:n}=o;this.pending_writes.delete(t),s?u(n,{password:r,message:a}):d(n,{message:i})}handle_write_notification(e){this.log.info("Received write notification",{op_type:e.data.op_type,timestamp:e.data.timestamp})}is_authenticated(e){return this.authenticated_clients.has(e.id)}update_connection_count(){process.connected&&process.send({type:"connection_count",data:{count:this.connection_count}})}send_heartbeat(){if(process.connected)try{process.send({type:"heartbeat",data:{timestamp:Date.now()}})}catch{clearInterval(this.heartbeat_interval)}}async shutdown(){const e=Date.now();this.log.info("Initiating graceful shutdown");try{await g(),this.log.info("Write queue shutdown complete")}catch(s){this.log.error("Error shutting down write queue",{error:s.message})}try{await y(),this.log.info("Database cleanup complete")}catch(s){this.log.error("Error cleaning up database",{error:s.message})}this.server&&this.server.close(()=>{this.log.info("Server closed")});for(const[s,r]of this.connections)r.end();const t=process.env.NODE_ENV==="test"?100:5e3;setTimeout(()=>{const s=Date.now()-e;this.log.info("Worker shutdown complete",{shutdown_duration_ms:s}),process.exit(0)},t)}}const M=new k;
1
+ import h from"net";import p from"../lib/op_types.js";import{send_success as u,send_error as d,send_message as l}from"../lib/send_response.js";import{shutdown_write_queue as g}from"../lib/write_queue.js";import{create_message_parser as w,encode_message as c}from"../lib/tcp_protocol.js";import m from"../lib/logger.js";import{initialize_database as f,cleanup_database as y}from"../lib/query_engine.js";import{handle_admin_operation as b,handle_ping_operation as v}from"../lib/operation_dispatcher.js";import{get_settings as k}from"../lib/load_settings.js";class ${constructor(){this.server=null,this.connections=new Map,this.connection_count=0,this.settings=null,this.port=null,this.write_id_counter=0,this.pending_writes=new Map,this.authenticated_clients=new Set,this.heartbeat_interval=null;const{create_context_logger:e}=m("worker");this.log=e({worker_pid:process.pid}),this.setup_worker()}setup_worker(){process.on("message",e=>{this.handle_master_message(e)}),process.on("SIGTERM",()=>{this.shutdown()}),process.on("SIGINT",()=>{this.shutdown()}),this.send_heartbeat(),this.heartbeat_interval=setInterval(()=>{this.send_heartbeat()},5e3),process.connected&&process.send({type:"worker_ready"})}handle_master_message(e){switch(e.type){case"config":this.handle_config(e);break;case"write_response":this.handle_write_response(e);break;case"auth_response":this.handle_auth_response(e);break;case"setup_response":this.handle_setup_response(e);break;case"write_notification":this.handle_write_notification(e);break;case"shutdown":this.shutdown();break;default:this.log.warn("Unknown message type received from master",{message_type:e.type})}}handle_config(e){const t=e.data.master_id;if(this.master_id&&this.master_id!==t){this.log.info("Worker already configured by different master, ignoring config message",{current_master_id:this.master_id,incoming_master_id:t,current_port:this.port,new_port:e.data.port});return}if(this.port!==null&&this.master_id===t){this.log.info("Worker already configured by same master, ignoring duplicate config message",{master_id:t,current_port:this.port,new_port:e.data.port});return}this.log.info("Received config message",{port:e.data.port,master_id:t}),this.port=e.data.port,this.settings=e.data.settings,this.master_id=t;try{let s="./data";try{const r=k();r?.data_path&&(s=r.data_path)}catch{}f(s),this.log.info("Database initialized in worker process",{database_path:s})}catch(s){this.log.error("Failed to initialize database in worker process",{error:s.message})}this.log.info("Starting server",{port:this.port}),this.start_server()}start_server(){this.server=h.createServer(e=>{this.handle_connection(e)}),this.server.listen(this.port,()=>{this.log.info("Server listening",{port:this.port}),process.connected&&process.send({type:"server_ready"})}),this.server.on("error",e=>{this.log.error("Server error",{error:e.message})})}handle_connection(e){const t=`${process.pid}_${Date.now()}_${Math.random()}`;e.id=t,e.message_parser=w(),this.connections.set(t,e),this.connection_count++,this.update_connection_count(),e.on("data",s=>{this.handle_socket_data(e,s)}),e.on("end",()=>{this.handle_socket_end(e)}),e.on("error",s=>{this.log.error("Socket error",{socket_id:t,error_message:s.message}),this.handle_socket_end(e)})}handle_socket_data(e,t){try{const s=e.message_parser.parse_messages(t);for(const r of s){const a=r,i=a?.op||null;if(!i){d(e,{message:"Missing operation type"});continue}if(!this.check_op_type(i)){d(e,{message:"Invalid operation type"});continue}this.route_operation(e,i,a?.data||{})}}catch(s){this.log.error("Data parsing error",{socket_id:e.id,error_message:s.message}),d(e,{message:"Invalid data format"})}}handle_socket_end(e){e.id&&(this.connections.delete(e.id),this.authenticated_clients.delete(e.id),this.connection_count--,this.update_connection_count()),this.log.info("Client disconnected",{socket_id:e.id})}check_op_type(e=""){return e?p.includes(e):!1}route_operation(e,t,s){switch(t){case"authentication":this.handle_authentication(e,s);break;case"setup":this.handle_setup(e,s);break;case"find_one":case"find":case"get_indexes":this.handle_read_operation(e,t,s);break;case"create_index":case"drop_index":this.handle_write_operation(e,t,s);break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":this.handle_write_operation(e,t,s);break;case"ping":v(e);break;case"admin":b(e,s,this.is_authenticated.bind(this));break;default:d(e,{message:`Unsupported operation: ${t}`})}}handle_authentication(e,t){if(this.is_authenticated(e))l(e,"Already authenticated");else{const s=`${e.id}_${Date.now()}`;process.send({type:"auth_request",data:{auth_id:s,socket_id:e.id,password:t.password}}),this.pending_writes.set(s,{socket:e,type:"auth"})}}handle_setup(e,t){const s=`${e.id}_${Date.now()}`;process.send({type:"setup_request",data:{setup_id:s,socket_id:e.id}}),this.pending_writes.set(s,{socket:e,type:"setup"})}handle_read_operation(e,t,s){if(!this.is_authenticated(e)){d(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"read"})}handle_write_operation(e,t,s){if(!this.is_authenticated(e)){d(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"write"})}handle_write_response(e){const{write_id:t,success:s,result:r,error:a}=e.data,i=this.pending_writes.get(t);if(!i){this.log.warn("No pending write found",{write_id:t});return}const{socket:o}=i;if(this.pending_writes.delete(t),o.destroyed||!o.writable){this.log.warn("Socket disconnected before response could be sent",{write_id:t});return}try{if(s){let n;Array.isArray(r)?n={ok:1,documents:r}:r&&typeof r=="object"?n={ok:1,...r}:n={ok:1,result:r};const _=c(n);o.write(_)}else{const _=c({ok:0,error:a});o.write(_)}}catch(n){this.log.error("Error sending response to client",{write_id:t,error:n.message})}}handle_auth_response(e){const{auth_id:t,success:s,message:r}=e.data,a=this.pending_writes.get(t);if(!a){this.log.warn("No pending auth found",{auth_id:t});return}const{socket:i}=a;if(this.pending_writes.delete(t),i.destroyed||!i.writable){this.log.warn("Socket disconnected before auth response could be sent",{auth_id:t});return}try{if(s){this.authenticated_clients.add(i.id);const n=c({ok:1,version:"1.0.0",message:r});i.write(n)}else d(i,{message:r}),i.end()}catch(o){this.log.error("Error sending auth response to client",{auth_id:t,error:o.message})}}handle_setup_response(e){const{setup_id:t,success:s,password:r,message:a,error:i}=e.data,o=this.pending_writes.get(t);if(!o){this.log.warn("No pending setup found",{setup_id:t});return}const{socket:n}=o;this.pending_writes.delete(t),s?u(n,{password:r,message:a}):d(n,{message:i})}handle_write_notification(e){this.log.info("Received write notification",{op_type:e.data.op_type,timestamp:e.data.timestamp})}is_authenticated(e){return this.authenticated_clients.has(e.id)}update_connection_count(){process.connected&&process.send({type:"connection_count",data:{count:this.connection_count}})}send_heartbeat(){if(process.connected)try{process.send({type:"heartbeat",data:{timestamp:Date.now()}})}catch{clearInterval(this.heartbeat_interval)}}async shutdown(){const e=Date.now();this.log.info("Initiating graceful shutdown");try{await g(),this.log.info("Write queue shutdown complete")}catch(s){this.log.error("Error shutting down write queue",{error:s.message})}try{await y(),this.log.info("Database cleanup complete")}catch(s){this.log.error("Error cleaning up database",{error:s.message})}this.server&&this.server.close(()=>{this.log.info("Server closed")});for(const[s,r]of this.connections)r.end();const t=process.env.NODE_ENV==="test"?100:5e3;setTimeout(()=>{const s=Date.now()-e;this.log.info("Worker shutdown complete",{shutdown_duration_ms:s}),process.exit(0)},t)}}const z=new $;
@@ -1 +1 @@
1
- import k from"net";import{decode as C}from"msgpackr";import E from"./lib/op_types.js";import S from"./lib/safe_json_parse.js";import{load_settings as y,get_settings as g,get_port_configuration as v}from"./lib/load_settings.js";import{send_error as f}from"./lib/send_response.js";import{start_cluster as q}from"./cluster/index.js";import x from"./lib/logger.js";import{initialize_database as N,cleanup_database as A}from"./lib/query_engine.js";import{create_message_parser as B,encode_message as _}from"./lib/tcp_protocol.js";import{create_connection_manager as D}from"./lib/connection_manager.js";import{shutdown_write_queue as $}from"./lib/write_queue.js";import{setup_authentication as F,verify_password as J,get_client_ip as K,is_rate_limited as P,initialize_auth_manager as j,reset_auth_state as G}from"./lib/auth_manager.js";import{initialize_api_key_manager as M}from"./lib/api_key_manager.js";import{is_development_mode as W,display_development_startup_message as H,warn_undefined_node_env as U}from"./lib/development_mode.js";import{restore_backup as V,start_backup_schedule as Y,stop_backup_schedule as L}from"./lib/backup_manager.js";import{initialize_replication_manager as Q,shutdown_replication_manager as X}from"./lib/replication_manager.js";import{initialize_write_forwarder as Z,shutdown_write_forwarder as ee}from"./lib/write_forwarder.js";import{handle_database_operation as re,handle_admin_operation as te,handle_ping_operation as oe}from"./lib/operation_dispatcher.js";import{start_http_server as ne,stop_http_server as se}from"./lib/http_server.js";import{create_recovery_token as ae,initialize_recovery_manager as T,reset_recovery_state as ie}from"./lib/recovery_manager.js";const d=new Set;let c=null;const ce=async(t,r={})=>{if(!r?.password){const s=_({ok:0,error:"Authentication operation requires password to be set in data."});t.write(s),t.end();return}try{const o=K(t);if(P(o)){const n=_({ok:0,error:"Too many failed attempts. Please try again later."});t.write(n),t.end();return}if(!await J(r.password,o)){const n=_({ok:0,error:"Authentication failed"});t.write(n),t.end();return}d.add(t.id);const e=_({ok:1,version:"1.0.0",message:"Authentication successful"});t.write(e)}catch(o){const s={ok:0,error:`Authentication error: ${o.message}`},a=_(s);t.write(a),t.end()}},_e=async(t,r={})=>{try{const s={ok:1,password:F(),message:"Authentication setup completed successfully. Save this password - it will not be shown again."},a=_(s);t.write(a)}catch(o){const s={ok:0,error:`Setup error: ${o.message}`},a=_(s);t.write(a)}},pe=(t="")=>{if(!t)throw new Error("Must pass an op type for operation.");return E.includes(t)},Be=t=>{try{if(typeof t=="string")return S(t);if(Buffer.isBuffer(t)){const r=C(t);return typeof r=="string"?S(r):r}else return t}catch{return null}},b=t=>d.has(t.id),De=async()=>{const{create_context_logger:t}=x("server"),r=t();let o=null;try{y(),o=g()}catch{}if(o?.restore_from)try{r.info("Startup restore requested",{backup_filename:o.restore_from});const e=await V(o.restore_from);r.info("Startup restore completed",{backup_filename:o.restore_from,duration_ms:e.duration_ms});const i={...o};delete i.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(i),y(),o=g(),r.info("Removed restore_from from settings after successful restore")}catch(e){r.error("Startup restore failed",{backup_filename:o.restore_from,error:e.message}),r.info("Continuing with fresh database after restore failure")}N(),j(),await M(),T();try{Q(),r.info("Replication manager initialized")}catch(e){r.warn("Failed to initialize replication manager",{error:e.message})}try{Z(),r.info("Write forwarder initialized")}catch(e){r.warn("Failed to initialize write forwarder",{error:e.message})}if(o?.s3)try{Y(),r.info("Backup scheduling started")}catch(e){r.warn("Failed to start backup scheduling",{error:e.message})}c=D({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3});let s=null;try{const{http_port:e}=v();s=await ne(e),s&&r.info("HTTP server started",{http_port:e})}catch(e){r.warn("Failed to start HTTP server",{error:e.message})}if(W()){const{tcp_port:e,http_port:i}=v();H(e,i)}else U();const a=k.createServer((e={})=>{if(!c.add_connection(e))return;const i=B();e.on("data",async n=>{c.update_activity(e.id);try{const h=i.parse_messages(n);for(const O of h){const u=O,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!pe(l)){f(e,{message:"Invalid operation type"});continue}const z=c.create_request_timeout(e.id,l);try{switch(l){case"authentication":await ce(e,u?.data||{});break;case"setup":await _e(e,u?.data||{});break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":case"find_one":case"find":case"create_index":case"drop_index":case"get_indexes":await re(e,l,u?.data||{},b,n.length,c,d);break;case"ping":oe(e);break;case"admin":await te(e,u?.data||{},b,c,d);break;case"reload":if(!b(e)){f(e,{message:"Authentication required"});break}try{let p=null;try{p=g()}catch{}let m=null;try{await y(),m=g()}catch{m={port:1983,authentication:{}}}const w={ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:p?p.port!==m.port:!1,authentication_changed:p?p.authentication?.password_hash!==m.authentication?.password_hash:!1},timestamp:new Date().toISOString()},I=_(w);e.write(I)}catch(p){const m={ok:0,error:`Reload operation failed: ${p.message}`},w=_(m);e.write(w)}break;default:f(e,{message:`Operation ${l} not implemented`})}}finally{clearTimeout(z)}}}catch(h){r.error("Message parsing failed",{client_id:e.id,error:h.message}),f(e,{message:"Invalid message format"}),e.end()}}),e.on("end",()=>{r.info("Client disconnected",{socket_id:e.id}),d.delete(e.id),c.remove_connection(e.id)}),e.on("error",n=>{r.error("Socket error",{socket_id:e.id,error:n.message}),d.delete(e.id),c.remove_connection(e.id)})});return a.cleanup=async()=>{try{await se(),L(),await X(),await ee(),c&&c.shutdown(),d.clear(),await $(),await new Promise(e=>setTimeout(e,100)),await A(),G(),ie()}catch{}},a};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:t}=x("main"),r=t();if(process.argv.includes("--generate-recovery-token"))try{T();const n=ae();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${n.url}`),console.log("Token expires in 10 minutes"),r.info("Recovery token generated via CLI",{expires_at:new Date(n.expires_at).toISOString()}),process.exit(0)}catch(n){console.error("Failed to generate recovery token:",n.message),r.error("Recovery token generation failed",{error:n.message}),process.exit(1)}const{tcp_port:o,http_port:s}=v(),a={worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:o,environment:process.env.NODE_ENV||"development"},{has_settings:e}=await import("./lib/load_settings.js"),i=e();r.info("Starting JoystickDB server...",{workers:a.worker_count||"auto",tcp_port:o,http_port:s,environment:a.environment,has_settings:i,port_source:i?"JOYSTICK_DB_SETTINGS":"default"}),q(a)}export{ce as authentication,pe as check_op_type,De as create_server,Be as parse_data,_e as setup};
1
+ import E from"net";import{decode as q}from"msgpackr";import N from"./lib/op_types.js";import x from"./lib/safe_json_parse.js";import{load_settings as v,get_settings as h,get_port_configuration as b}from"./lib/load_settings.js";import{send_error as f}from"./lib/send_response.js";import{start_cluster as A}from"./cluster/index.js";import T from"./lib/logger.js";import{initialize_database as B,cleanup_database as D}from"./lib/query_engine.js";import{create_message_parser as $,encode_message as c}from"./lib/tcp_protocol.js";import{create_connection_manager as F}from"./lib/connection_manager.js";import{shutdown_write_queue as J}from"./lib/write_queue.js";import{setup_authentication as K,verify_password as P,get_client_ip as j,is_rate_limited as G,initialize_auth_manager as M,reset_auth_state as W}from"./lib/auth_manager.js";import{initialize_api_key_manager as H}from"./lib/api_key_manager.js";import{is_development_mode as O,display_development_startup_message as U,warn_undefined_node_env as V}from"./lib/development_mode.js";import{restore_backup as Y,start_backup_schedule as L,stop_backup_schedule as Q}from"./lib/backup_manager.js";import{initialize_replication_manager as X,shutdown_replication_manager as Z}from"./lib/replication_manager.js";import{initialize_write_forwarder as ee,shutdown_write_forwarder as re}from"./lib/write_forwarder.js";import{handle_database_operation as te,handle_admin_operation as oe,handle_ping_operation as ae}from"./lib/operation_dispatcher.js";import{start_http_server as ne,stop_http_server as se}from"./lib/http_server.js";import{create_recovery_token as ie,initialize_recovery_manager as z,reset_recovery_state as ce}from"./lib/recovery_manager.js";const p=new Set;let i=null;const _e=async(t,r={})=>{if(!r?.password){const n=c({ok:0,error:"Authentication operation requires password to be set in data."});t.write(n),t.end();return}try{const o=j(t);if(G(o)){const a=c({ok:0,error:"Too many failed attempts. Please try again later."});t.write(a),t.end();return}if(!await P(r.password,o)){const a=c({ok:0,error:"Authentication failed"});t.write(a),t.end();return}p.add(t.id);const d=c({ok:1,version:"1.0.0",message:"Authentication successful"});t.write(d)}catch(o){const n={ok:0,error:`Authentication error: ${o.message}`},s=c(n);t.write(s),t.end()}},pe=async(t,r={})=>{try{const n={ok:1,password:K(),message:"Authentication setup completed successfully. Save this password - it will not be shown again."},s=c(n);t.write(s)}catch(o){const n={ok:0,error:`Setup error: ${o.message}`},s=c(n);t.write(s)}},de=(t="")=>{if(!t)throw new Error("Must pass an op type for operation.");return N.includes(t)},De=t=>{try{if(typeof t=="string")return x(t);if(Buffer.isBuffer(t)){const r=q(t);return typeof r=="string"?x(r):r}else return t}catch{return null}},S=t=>O()?!0:p.has(t.id),$e=async()=>{const{create_context_logger:t}=T("server"),r=t();let o=null;try{v(),o=h()}catch{}if(o?.restore_from)try{r.info("Startup restore requested",{backup_filename:o.restore_from});const e=await Y(o.restore_from);r.info("Startup restore completed",{backup_filename:o.restore_from,duration_ms:e.duration_ms});const a={...o};delete a.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(a),v(),o=h(),r.info("Removed restore_from from settings after successful restore")}catch(e){r.error("Startup restore failed",{backup_filename:o.restore_from,error:e.message}),r.info("Continuing with fresh database after restore failure")}let n="./data";o?.data_path&&(n=o.data_path),B(n),M(),await H(),z();try{X(),r.info("Replication manager initialized")}catch(e){r.warn("Failed to initialize replication manager",{error:e.message})}try{ee(),r.info("Write forwarder initialized")}catch(e){r.warn("Failed to initialize write forwarder",{error:e.message})}if(o?.s3)try{L(),r.info("Backup scheduling started")}catch(e){r.warn("Failed to start backup scheduling",{error:e.message})}i=F({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3});let s=null;try{const{http_port:e}=b();s=await ne(e),s&&r.info("HTTP server started",{http_port:e})}catch(e){r.warn("Failed to start HTTP server",{error:e.message})}if(O()){const{tcp_port:e,http_port:a}=b();U(e,a)}else V();const d=E.createServer((e={})=>{if(!i.add_connection(e))return;const a=$();e.on("data",async g=>{i.update_activity(e.id);try{const w=a.parse_messages(g);for(const I of w){const u=I,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!de(l)){f(e,{message:"Invalid operation type"});continue}const R=i.create_request_timeout(e.id,l);try{switch(l){case"authentication":await _e(e,u?.data||{});break;case"setup":await pe(e,u?.data||{});break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":case"find_one":case"find":case"create_index":case"drop_index":case"get_indexes":await te(e,l,u?.data||{},S,g.length,i,p);break;case"ping":ae(e);break;case"admin":await oe(e,u?.data||{},S,i,p);break;case"reload":if(!S(e)){f(e,{message:"Authentication required"});break}try{let _=null;try{_=h()}catch{}let m=null;try{await v(),m=h()}catch{m={port:1983,authentication:{}}}const y={ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:_?_.port!==m.port:!1,authentication_changed:_?_.authentication?.password_hash!==m.authentication?.password_hash:!1},timestamp:new Date().toISOString()},k=c(y);e.write(k)}catch(_){const m={ok:0,error:`Reload operation failed: ${_.message}`},y=c(m);e.write(y)}break;default:f(e,{message:`Operation ${l} not implemented`})}}finally{clearTimeout(R)}}}catch(w){r.error("Message parsing failed",{client_id:e.id,error:w.message}),f(e,{message:"Invalid message format"}),e.end()}}),e.on("end",()=>{r.info("Client disconnected",{socket_id:e.id}),p.delete(e.id),i.remove_connection(e.id)}),e.on("error",g=>{r.error("Socket error",{socket_id:e.id,error:g.message}),p.delete(e.id),i.remove_connection(e.id)})});return d.cleanup=async()=>{try{await se(),Q(),await Z(),await re(),i&&i.shutdown(),p.clear(),await J(),await new Promise(e=>setTimeout(e,100)),await D(),W(),ce()}catch{}},d};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:t}=T("main"),r=t();if(process.argv.includes("--generate-recovery-token"))try{z();const a=ie();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${a.url}`),console.log("Token expires in 10 minutes"),r.info("Recovery token generated via CLI",{expires_at:new Date(a.expires_at).toISOString()}),process.exit(0)}catch(a){console.error("Failed to generate recovery token:",a.message),r.error("Recovery token generation failed",{error:a.message}),process.exit(1)}const{tcp_port:o,http_port:n}=b(),s={worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:o,environment:process.env.NODE_ENV||"development"},{has_settings:d}=await import("./lib/load_settings.js"),e=d();r.info("Starting JoystickDB server...",{workers:s.worker_count||"auto",tcp_port:o,http_port:n,environment:s.environment,has_settings:e,port_source:e?"JOYSTICK_DB_SETTINGS":"default"}),A(s)}export{_e as authentication,de as check_op_type,$e as create_server,De as parse_data,pe as setup};
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@joystick.js/db-canary",
3
3
  "type": "module",
4
- "version": "0.0.0-canary.2221",
5
- "canary_version": "0.0.0-canary.2220",
4
+ "version": "0.0.0-canary.2223",
5
+ "canary_version": "0.0.0-canary.2222",
6
6
  "description": "JoystickDB - A minimalist database server for the Joystick framework",
7
7
  "main": "./dist/server/index.js",
8
8
  "scripts": {
@@ -2,7 +2,7 @@ import cluster from 'cluster';
2
2
  import os from 'os';
3
3
  import { EventEmitter } from 'events';
4
4
  import { writeFileSync } from 'fs';
5
- import { load_settings, get_port_configuration } from '../lib/load_settings.js';
5
+ import { load_settings, get_settings, get_port_configuration } from '../lib/load_settings.js';
6
6
  import {
7
7
  restore_backup,
8
8
  start_backup_schedule,
@@ -107,7 +107,18 @@ class ClusterMaster extends EventEmitter {
107
107
  this.log.info('Settings loaded successfully', { settings_file: this.settings_file });
108
108
 
109
109
  // NOTE: Initialize database and auth manager.
110
- initialize_database();
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.
119
+ }
120
+
121
+ initialize_database(database_path);
111
122
  initialize_auth_manager();
112
123
  await initialize_api_key_manager();
113
124
  this.log.info('Database and auth manager initialized');
@@ -12,6 +12,7 @@ import { create_message_parser, encode_message } from '../lib/tcp_protocol.js';
12
12
  import create_logger from '../lib/logger.js';
13
13
  import { initialize_database, cleanup_database } from '../lib/query_engine.js';
14
14
  import { handle_admin_operation, handle_ping_operation } from '../lib/operation_dispatcher.js';
15
+ import { get_settings } from '../lib/load_settings.js';
15
16
 
16
17
  /**
17
18
  * Cluster worker process that handles TCP connections and routes operations to master.
@@ -131,8 +132,19 @@ class ClusterWorker {
131
132
 
132
133
  // NOTE: Initialize database for read operations in worker process.
133
134
  try {
134
- initialize_database();
135
- this.log.info('Database initialized in worker process');
135
+ // NOTE: Initialize database with data_path from settings if available.
136
+ let database_path = './data'; // Default path
137
+ try {
138
+ const settings = get_settings();
139
+ if (settings?.data_path) {
140
+ database_path = settings.data_path;
141
+ }
142
+ } catch (error) {
143
+ // NOTE: Settings not available, use default path.
144
+ }
145
+
146
+ initialize_database(database_path);
147
+ this.log.info('Database initialized in worker process', { database_path });
136
148
  } catch (error) {
137
149
  this.log.error('Failed to initialize database in worker process', { error: error.message });
138
150
  }
@@ -181,10 +181,16 @@ export const parse_data = (raw_data) => {
181
181
 
182
182
  /**
183
183
  * Checks if a socket connection is authenticated.
184
+ * In development mode, authentication is bypassed for TCP connections.
184
185
  * @param {net.Socket} socket - Socket connection to check
185
- * @returns {boolean} True if socket is authenticated
186
+ * @returns {boolean} True if socket is authenticated or in development mode
186
187
  */
187
188
  const check_authentication = (socket) => {
189
+ // NOTE: Bypass authentication in development mode for TCP connections.
190
+ if (is_development_mode()) {
191
+ return true;
192
+ }
193
+
188
194
  return authenticated_clients.has(socket.id);
189
195
  };
190
196
 
@@ -255,7 +261,13 @@ export const create_server = async () => {
255
261
  }
256
262
  }
257
263
 
258
- initialize_database();
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;
268
+ }
269
+
270
+ initialize_database(database_path);
259
271
  initialize_auth_manager();
260
272
  await initialize_api_key_manager();
261
273
  initialize_recovery_manager();
@@ -0,0 +1,195 @@
1
+ import test from 'ava';
2
+ import net from 'net';
3
+ import { create_server } from '../../../src/server/index.js';
4
+ import { encode_message, create_message_parser } from '../../../src/server/lib/tcp_protocol.js';
5
+ import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js';
6
+ import { reset_auth_state } from '../../../src/server/lib/auth_manager.js';
7
+
8
+ // Dynamic port allocation
9
+ let current_port = 4000;
10
+ const get_next_port = () => {
11
+ return ++current_port;
12
+ };
13
+
14
+ let server;
15
+ let port;
16
+ let original_node_env;
17
+
18
+ test.beforeEach(async () => {
19
+ // Store original NODE_ENV
20
+ original_node_env = process.env.NODE_ENV;
21
+
22
+ // Set NODE_ENV to development
23
+ process.env.NODE_ENV = 'development';
24
+
25
+ // Reset auth state and clean up environment variables
26
+ reset_auth_state();
27
+ delete process.env.JOYSTICK_DB_SETTINGS;
28
+
29
+ // Initialize database for testing
30
+ initialize_database();
31
+
32
+ // Create server with dynamic port
33
+ const test_port = get_next_port();
34
+ server = await create_server();
35
+
36
+ // Start server on specific port
37
+ await new Promise((resolve) => {
38
+ server.listen(test_port, () => {
39
+ port = test_port;
40
+ setTimeout(resolve, 100);
41
+ });
42
+ });
43
+ });
44
+
45
+ test.afterEach(async () => {
46
+ // Restore original NODE_ENV
47
+ process.env.NODE_ENV = original_node_env;
48
+
49
+ if (server) {
50
+ await server.cleanup();
51
+ server.close();
52
+ server = null;
53
+ }
54
+
55
+ // Clean up database
56
+ try {
57
+ await cleanup_database(true); // Remove test database directory
58
+ } catch (error) {
59
+ // Ignore cleanup errors
60
+ }
61
+
62
+ // Clean up environment variables
63
+ reset_auth_state();
64
+ delete process.env.JOYSTICK_DB_SETTINGS;
65
+ });
66
+
67
+ const create_client = () => {
68
+ return new Promise((resolve, reject) => {
69
+ const client = net.createConnection(port, 'localhost');
70
+ const parser = create_message_parser();
71
+
72
+ client.on('connect', () => {
73
+ resolve({
74
+ client,
75
+ send: (data) => {
76
+ const encoded = encode_message(data);
77
+ client.write(encoded);
78
+ },
79
+ receive: () => {
80
+ return new Promise((resolve) => {
81
+ const handler = (data) => {
82
+ try {
83
+ const messages = parser.parse_messages(data);
84
+ for (const message of messages) {
85
+ client.off('data', handler);
86
+ resolve(message);
87
+ return;
88
+ }
89
+ } catch (error) {
90
+ // Continue listening
91
+ }
92
+ };
93
+ client.on('data', handler);
94
+ });
95
+ },
96
+ close: () => {
97
+ client.end();
98
+ }
99
+ });
100
+ });
101
+
102
+ client.on('error', reject);
103
+ });
104
+ };
105
+
106
+ test('development mode - database operations work without authentication', async (t) => {
107
+ const { client, send, receive, close } = await create_client();
108
+
109
+ try {
110
+ // Try ping operation without authentication - should work in development mode
111
+ send({ op: 'ping' });
112
+ const response = await receive();
113
+
114
+ t.is(response.ok, 1);
115
+ // Ping operation just returns { ok: 1 } without a message
116
+ } finally {
117
+ close();
118
+ }
119
+ });
120
+
121
+ test('development mode - find operation works without authentication', async (t) => {
122
+ const { client, send, receive, close } = await create_client();
123
+
124
+ try {
125
+ // Try find operation without authentication - should work in development mode
126
+ send({ op: 'find', data: { collection: 'users', filter: {} } });
127
+ const response = await receive();
128
+
129
+ t.is(response.ok, 1);
130
+ t.true(Array.isArray(response.documents));
131
+ } finally {
132
+ close();
133
+ }
134
+ });
135
+
136
+ test('development mode - insert operation works without authentication', async (t) => {
137
+ const { client, send, receive, close } = await create_client();
138
+
139
+ try {
140
+ // Try insert operation without authentication - should work in development mode
141
+ send({
142
+ op: 'insert_one',
143
+ data: {
144
+ collection: 'users',
145
+ document: { name: 'Test User', email: 'test@example.com' }
146
+ }
147
+ });
148
+ const response = await receive();
149
+
150
+ t.is(response.ok, 1);
151
+ t.true(typeof response.inserted_id === 'string');
152
+ } finally {
153
+ close();
154
+ }
155
+ });
156
+
157
+ test('development mode - admin operation works without authentication', async (t) => {
158
+ const { client, send, receive, close } = await create_client();
159
+
160
+ try {
161
+ // Try admin operation without authentication - should work in development mode
162
+ send({ op: 'admin' });
163
+ const response = await receive();
164
+
165
+ t.true(typeof response.authentication === 'object');
166
+ t.is(response.authentication.authenticated_clients, 0); // No clients authenticated since we bypassed auth
167
+ t.true(typeof response.server === 'object');
168
+ t.true(typeof response.database === 'object');
169
+ } finally {
170
+ close();
171
+ }
172
+ });
173
+
174
+ test('development mode - authentication still works if explicitly used', async (t) => {
175
+ const { client, send, receive, close } = await create_client();
176
+
177
+ try {
178
+ // Setup authentication first
179
+ send({ op: 'setup' });
180
+ const setup_response = await receive();
181
+
182
+ t.true(setup_response.ok === 1 || setup_response.ok === true);
183
+ t.is(typeof setup_response.password, 'string');
184
+
185
+ // Now authenticate with the password
186
+ send({ op: 'authentication', data: { password: setup_response.password } });
187
+ const auth_response = await receive();
188
+
189
+ t.is(auth_response.ok, 1);
190
+ t.is(auth_response.version, '1.0.0');
191
+ t.is(auth_response.message, 'Authentication successful');
192
+ } finally {
193
+ close();
194
+ }
195
+ });