@joystick.js/db-canary 0.0.0-canary.2242 → 0.0.0-canary.2244

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 (48) hide show
  1. package/README.md +13 -0
  2. package/dist/client/index.js +1 -1
  3. package/dist/server/cluster/worker.js +1 -1
  4. package/dist/server/index.js +1 -1
  5. package/dist/server/lib/development_mode.js +1 -1
  6. package/dist/server/lib/op_types.js +1 -1
  7. package/dist/server/lib/operation_dispatcher.js +1 -1
  8. package/dist/server/lib/operations/delete_many.js +1 -0
  9. package/package.json +2 -2
  10. package/src/client/index.js +33 -0
  11. package/src/server/cluster/worker.js +6 -5
  12. package/src/server/index.js +1 -0
  13. package/src/server/lib/development_mode.js +2 -2
  14. package/src/server/lib/op_types.js +1 -0
  15. package/src/server/lib/operation_dispatcher.js +5 -0
  16. package/src/server/lib/operations/delete_many.js +130 -0
  17. package/test_data_api_key_1758220165907_u8d8j2z37/data.mdb +0 -0
  18. package/test_data_api_key_1758220165907_u8d8j2z37/lock.mdb +0 -0
  19. package/test_data_api_key_1758220166138_q079fkt44/data.mdb +0 -0
  20. package/test_data_api_key_1758220166138_q079fkt44/lock.mdb +0 -0
  21. package/test_data_api_key_1758220166370_elaxmwvuj/data.mdb +0 -0
  22. package/test_data_api_key_1758220166370_elaxmwvuj/lock.mdb +0 -0
  23. package/test_data_api_key_1758220166487_689q46e05/data.mdb +0 -0
  24. package/test_data_api_key_1758220166487_689q46e05/lock.mdb +0 -0
  25. package/test_data_api_key_1758220174956_9lhw6u6la/data.mdb +0 -0
  26. package/test_data_api_key_1758220174956_9lhw6u6la/lock.mdb +0 -0
  27. package/test_data_api_key_1758220175070_1hruzftqf/data.mdb +0 -0
  28. package/test_data_api_key_1758220175070_1hruzftqf/lock.mdb +0 -0
  29. package/test_data_api_key_1758220175183_ean83s4od/data.mdb +0 -0
  30. package/test_data_api_key_1758220175183_ean83s4od/lock.mdb +0 -0
  31. package/test_data_api_key_1758220736169_8xv9swdwn/data.mdb +0 -0
  32. package/test_data_api_key_1758220736169_8xv9swdwn/lock.mdb +0 -0
  33. package/test_data_api_key_1758220736405_tzltnwwf5/data.mdb +0 -0
  34. package/test_data_api_key_1758220736405_tzltnwwf5/lock.mdb +0 -0
  35. package/test_data_api_key_1758220736640_avs9xxx7j/data.mdb +0 -0
  36. package/test_data_api_key_1758220736640_avs9xxx7j/lock.mdb +0 -0
  37. package/test_data_api_key_1758220736757_c4hhzan82/data.mdb +0 -0
  38. package/test_data_api_key_1758220736757_c4hhzan82/lock.mdb +0 -0
  39. package/test_data_api_key_1758220744324_89rzc4cak/data.mdb +0 -0
  40. package/test_data_api_key_1758220744324_89rzc4cak/lock.mdb +0 -0
  41. package/test_data_api_key_1758220744436_y2244449u/data.mdb +0 -0
  42. package/test_data_api_key_1758220744436_y2244449u/lock.mdb +0 -0
  43. package/test_data_api_key_1758220744548_968u1bkrk/data.mdb +0 -0
  44. package/test_data_api_key_1758220744548_968u1bkrk/lock.mdb +0 -0
  45. package/tests/client/index.test.js +393 -0
  46. package/tests/server/cluster/master_read_write_operations.test.js +7 -6
  47. package/tests/server/lib/operations/delete_many.test.js +263 -0
  48. package/types/client/index.d.ts +17 -0
package/README.md CHANGED
@@ -518,6 +518,18 @@ await products.delete_one({
518
518
  stock: { $lte: 0 },
519
519
  discontinued: true
520
520
  });
521
+
522
+ // Delete multiple documents at once
523
+ await users.delete_many({
524
+ status: 'inactive',
525
+ last_login: { $lt: new Date(Date.now() - 365*24*60*60*1000) }
526
+ });
527
+
528
+ await products.delete_many({
529
+ stock: { $lte: 0 }
530
+ }, {
531
+ limit: 100 // Optional: limit number of deletions
532
+ });
521
533
  ```
522
534
 
523
535
 
@@ -1649,6 +1661,7 @@ const monitor_performance = async () => {
1649
1661
  - `collection.find(filter, options)` - Find multiple documents
1650
1662
  - `collection.update_one(filter, update, options)` - Update single document
1651
1663
  - `collection.delete_one(filter, options)` - Delete single document
1664
+ - `collection.delete_many(filter, options)` - Delete multiple documents
1652
1665
  - `collection.bulk_write(operations, options)` - Bulk operations
1653
1666
 
1654
1667
  #### Index Management (Collection Chaining API)
@@ -1 +1 @@
1
- import u from"net";import{EventEmitter as h}from"events";import{encode as d,decode as l}from"msgpackr";import m from"./database.js";const p=n=>{const e=d(n,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0}),t=Buffer.allocUnsafe(4);return t.writeUInt32BE(e.length,0),Buffer.concat([t,e])},f=()=>{let n=Buffer.alloc(0),e=null;return{parse_messages:i=>{n=Buffer.concat([n,i]);const r=[];for(;n.length>0;){if(e===null){if(n.length<4)break;e=n.readUInt32BE(0),n=n.slice(4)}if(n.length<e)break;const a=n.slice(0,e);n=n.slice(e),e=null;try{const c=l(a,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0});r.push(c)}catch(c){throw new Error(`Invalid message format: ${c.message}`)}}return r},reset:()=>{n=Buffer.alloc(0),e=null}}};class _ extends h{constructor(e={}){super(),this.host=e.host||"localhost",this.port=e.port||1983,this.password=e.password||null,this.timeout=e.timeout||5e3,this.reconnect=e.reconnect!==!1,this.max_reconnect_attempts=e.max_reconnect_attempts||10,this.reconnect_delay=e.reconnect_delay||1e3,this.socket=null,this.message_parser=null,this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.reconnect_attempts=0,this.reconnect_timeout=null,this.pending_requests=new Map,this.request_id_counter=0,this.request_queue=[],e.auto_connect!==!1&&this.connect()}connect(){if(this.is_connecting||this.is_connected)return;this.is_connecting=!0,this.socket=new u.Socket,this.message_parser=f();const e=setTimeout(()=>{this.socket&&!this.is_connected&&(this.socket.destroy(),this.handle_connection_error(new Error("Connection timeout")))},this.timeout);this.socket.connect(this.port,this.host,()=>{clearTimeout(e),this.is_connected=!0,this.is_connecting=!1,this.reconnect_attempts=0,this.emit("connect"),this.password?this.authenticate():(this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue())}),this.socket.on("data",t=>{try{const s=this.message_parser.parse_messages(t);for(const i of s)this.handle_message(i)}catch(s){this.emit("error",new Error(`Message parsing failed: ${s.message}`))}}),this.socket.on("error",t=>{clearTimeout(e),this.handle_connection_error(t)}),this.socket.on("close",()=>{clearTimeout(e),this.handle_disconnect()})}async authenticate(){if(!this.password){this.emit("error",new Error('Password required for authentication. Provide password in client options: joystickdb.client({ password: "your_password" })')),this.disconnect();return}try{if((await this.send_request("authentication",{password:this.password})).ok===1)this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue();else throw new Error("Authentication failed")}catch(e){this.emit("error",new Error(`Authentication error: ${e.message}`)),this.disconnect()}}handle_message(e){if(this.pending_requests.size>0){const[t,{resolve:s,reject:i,timeout:r}]=this.pending_requests.entries().next().value;if(clearTimeout(r),this.pending_requests.delete(t),e.ok===1||e.ok===!0)s(e);else if(e.ok===0||e.ok===!1){const a=typeof e.error=="string"?e.error:JSON.stringify(e.error)||"Operation failed";i(new Error(a))}else s(e)}else this.emit("response",e)}handle_connection_error(e){this.is_connecting=!1,this.is_connected=!1,this.is_authenticated=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.destroy(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[t,{reject:s,timeout:i}]of this.pending_requests)clearTimeout(i),s(new Error("Connection lost"));this.pending_requests.clear(),this.emit("error",e),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}handle_disconnect(){this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.socket&&(this.socket.removeAllListeners(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[e,{reject:t,timeout:s}]of this.pending_requests)clearTimeout(s),t(new Error("Connection closed"));this.pending_requests.clear(),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}schedule_reconnect(){this.reconnect_attempts++;const e=Math.min(this.reconnect_delay*Math.pow(2,this.reconnect_attempts-1),3e4);this.emit("reconnecting",{attempt:this.reconnect_attempts,delay:e}),this.reconnect_timeout=setTimeout(()=>{this.connect()},e)}send_request(e,t={},s=!0){return new Promise((i,r)=>{const a=++this.request_id_counter,o={message:{op:e,data:t},resolve:i,reject:r,request_id:a};if(!this.is_connected||e!=="authentication"&&e!=="setup"&&e!=="ping"&&!this.is_authenticated)if(s){this.request_queue.push(o);return}else{r(new Error("Not connected or authenticated"));return}this.send_request_now(o)})}send_request_now(e){const{message:t,resolve:s,reject:i,request_id:r}=e,a=setTimeout(()=>{this.pending_requests.delete(r),i(new Error("Request timeout"))},this.timeout);this.pending_requests.set(r,{resolve:s,reject:i,timeout:a});try{const c=p(t);this.socket.write(c)}catch(c){clearTimeout(a),this.pending_requests.delete(r),i(c)}}process_request_queue(){for(;this.request_queue.length>0&&this.is_connected&&this.is_authenticated;){const e=this.request_queue.shift();this.send_request_now(e)}}disconnect(){this.reconnect=!1,this.reconnect_timeout&&(clearTimeout(this.reconnect_timeout),this.reconnect_timeout=null),this.socket&&this.socket.end()}async backup_now(){return this.send_request("admin",{admin_action:"backup_now"})}async list_backups(){return this.send_request("admin",{admin_action:"list_backups"})}async restore_backup(e){return this.send_request("admin",{admin_action:"restore_backup",backup_name:e})}async get_replication_status(){return this.send_request("admin",{admin_action:"get_replication_status"})}async add_secondary(e){return this.send_request("admin",{admin_action:"add_secondary",...e})}async remove_secondary(e){return this.send_request("admin",{admin_action:"remove_secondary",secondary_id:e})}async sync_secondaries(){return this.send_request("admin",{admin_action:"sync_secondaries"})}async get_secondary_health(){return this.send_request("admin",{admin_action:"get_secondary_health"})}async get_forwarder_status(){return this.send_request("admin",{admin_action:"get_forwarder_status"})}async ping(){return this.send_request("ping",{},!1)}async reload(){return this.send_request("reload")}async get_auto_index_stats(){return this.send_request("admin",{admin_action:"get_auto_index_stats"})}async setup(){const e=await this.send_request("setup",{},!1);return e.data&&e.data.instructions&&console.log(e.data.instructions),e}db(e){return new m(this,e)}async list_databases(){return this.send_request("admin",{admin_action:"list_databases"})}}class g{constructor(e,t,s){this.client=e,this.database_name=t,this.collection_name=s}async insert_one(e,t={}){return this.client.send_request("insert_one",{database:this.database_name,collection:this.collection_name,document:e,options:t})}async find_one(e={},t={}){return(await this.client.send_request("find_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).document}async find(e={},t={}){return(await this.client.send_request("find",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).documents||[]}async update_one(e,t,s={}){return this.client.send_request("update_one",{database:this.database_name,collection:this.collection_name,filter:e,update:t,options:s})}async delete_one(e,t={}){return this.client.send_request("delete_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async bulk_write(e,t={}){return this.client.send_request("bulk_write",{database:this.database_name,collection:this.collection_name,operations:e,options:t})}async create_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:t})}async upsert_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:{...t,upsert:!0}})}async drop_index(e){return this.client.send_request("drop_index",{database:this.database_name,collection:this.collection_name,field:e})}async get_indexes(){return this.client.send_request("get_indexes",{database:this.database_name,collection:this.collection_name})}}_.Collection=g;const q={client:n=>new _(n)};var x=q;export{x as default};
1
+ import u from"net";import{EventEmitter as h}from"events";import{encode as d,decode as l}from"msgpackr";import m from"./database.js";const p=n=>{const e=d(n,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0}),t=Buffer.allocUnsafe(4);return t.writeUInt32BE(e.length,0),Buffer.concat([t,e])},f=()=>{let n=Buffer.alloc(0),e=null;return{parse_messages:i=>{n=Buffer.concat([n,i]);const r=[];for(;n.length>0;){if(e===null){if(n.length<4)break;e=n.readUInt32BE(0),n=n.slice(4)}if(n.length<e)break;const a=n.slice(0,e);n=n.slice(e),e=null;try{const c=l(a,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0});r.push(c)}catch(c){throw new Error(`Invalid message format: ${c.message}`)}}return r},reset:()=>{n=Buffer.alloc(0),e=null}}};class _ extends h{constructor(e={}){super(),this.host=e.host||"localhost",this.port=e.port||1983,this.password=e.password||null,this.timeout=e.timeout||5e3,this.reconnect=e.reconnect!==!1,this.max_reconnect_attempts=e.max_reconnect_attempts||10,this.reconnect_delay=e.reconnect_delay||1e3,this.socket=null,this.message_parser=null,this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.reconnect_attempts=0,this.reconnect_timeout=null,this.pending_requests=new Map,this.request_id_counter=0,this.request_queue=[],e.auto_connect!==!1&&this.connect()}connect(){if(this.is_connecting||this.is_connected)return;this.is_connecting=!0,this.socket=new u.Socket,this.message_parser=f();const e=setTimeout(()=>{this.socket&&!this.is_connected&&(this.socket.destroy(),this.handle_connection_error(new Error("Connection timeout")))},this.timeout);this.socket.connect(this.port,this.host,()=>{clearTimeout(e),this.is_connected=!0,this.is_connecting=!1,this.reconnect_attempts=0,this.emit("connect"),this.password?this.authenticate():(this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue())}),this.socket.on("data",t=>{try{const s=this.message_parser.parse_messages(t);for(const i of s)this.handle_message(i)}catch(s){this.emit("error",new Error(`Message parsing failed: ${s.message}`))}}),this.socket.on("error",t=>{clearTimeout(e),this.handle_connection_error(t)}),this.socket.on("close",()=>{clearTimeout(e),this.handle_disconnect()})}async authenticate(){if(!this.password){this.emit("error",new Error('Password required for authentication. Provide password in client options: joystickdb.client({ password: "your_password" })')),this.disconnect();return}try{if((await this.send_request("authentication",{password:this.password})).ok===1)this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue();else throw new Error("Authentication failed")}catch(e){this.emit("error",new Error(`Authentication error: ${e.message}`)),this.disconnect()}}handle_message(e){if(this.pending_requests.size>0){const[t,{resolve:s,reject:i,timeout:r}]=this.pending_requests.entries().next().value;if(clearTimeout(r),this.pending_requests.delete(t),e.ok===1||e.ok===!0)s(e);else if(e.ok===0||e.ok===!1){const a=typeof e.error=="string"?e.error:JSON.stringify(e.error)||"Operation failed";i(new Error(a))}else s(e)}else this.emit("response",e)}handle_connection_error(e){this.is_connecting=!1,this.is_connected=!1,this.is_authenticated=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.destroy(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[t,{reject:s,timeout:i}]of this.pending_requests)clearTimeout(i),s(new Error("Connection lost"));this.pending_requests.clear(),this.emit("error",e),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}handle_disconnect(){this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.socket&&(this.socket.removeAllListeners(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[e,{reject:t,timeout:s}]of this.pending_requests)clearTimeout(s),t(new Error("Connection closed"));this.pending_requests.clear(),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}schedule_reconnect(){this.reconnect_attempts++;const e=Math.min(this.reconnect_delay*Math.pow(2,this.reconnect_attempts-1),3e4);this.emit("reconnecting",{attempt:this.reconnect_attempts,delay:e}),this.reconnect_timeout=setTimeout(()=>{this.connect()},e)}send_request(e,t={},s=!0){return new Promise((i,r)=>{const a=++this.request_id_counter,o={message:{op:e,data:t},resolve:i,reject:r,request_id:a};if(!this.is_connected||e!=="authentication"&&e!=="setup"&&e!=="ping"&&!this.is_authenticated)if(s){this.request_queue.push(o);return}else{r(new Error("Not connected or authenticated"));return}this.send_request_now(o)})}send_request_now(e){const{message:t,resolve:s,reject:i,request_id:r}=e,a=setTimeout(()=>{this.pending_requests.delete(r),i(new Error("Request timeout"))},this.timeout);this.pending_requests.set(r,{resolve:s,reject:i,timeout:a});try{const c=p(t);this.socket.write(c)}catch(c){clearTimeout(a),this.pending_requests.delete(r),i(c)}}process_request_queue(){for(;this.request_queue.length>0&&this.is_connected&&this.is_authenticated;){const e=this.request_queue.shift();this.send_request_now(e)}}disconnect(){this.reconnect=!1,this.reconnect_timeout&&(clearTimeout(this.reconnect_timeout),this.reconnect_timeout=null),this.socket&&this.socket.end()}async backup_now(){return this.send_request("admin",{admin_action:"backup_now"})}async list_backups(){return this.send_request("admin",{admin_action:"list_backups"})}async restore_backup(e){return this.send_request("admin",{admin_action:"restore_backup",backup_name:e})}async get_replication_status(){return this.send_request("admin",{admin_action:"get_replication_status"})}async add_secondary(e){return this.send_request("admin",{admin_action:"add_secondary",...e})}async remove_secondary(e){return this.send_request("admin",{admin_action:"remove_secondary",secondary_id:e})}async sync_secondaries(){return this.send_request("admin",{admin_action:"sync_secondaries"})}async get_secondary_health(){return this.send_request("admin",{admin_action:"get_secondary_health"})}async get_forwarder_status(){return this.send_request("admin",{admin_action:"get_forwarder_status"})}async ping(){return this.send_request("ping",{},!1)}async reload(){return this.send_request("reload")}async get_auto_index_stats(){return this.send_request("admin",{admin_action:"get_auto_index_stats"})}async setup(){const e=await this.send_request("setup",{},!1);return e.data&&e.data.instructions&&console.log(e.data.instructions),e}async delete_many(e,t={},s={}){return this.send_request("delete_many",{collection:e,filter:t,options:s})}db(e){return new m(this,e)}async list_databases(){return this.send_request("admin",{admin_action:"list_databases"})}}class q{constructor(e,t,s){this.client=e,this.database_name=t,this.collection_name=s}async insert_one(e,t={}){return this.client.send_request("insert_one",{database:this.database_name,collection:this.collection_name,document:e,options:t})}async find_one(e={},t={}){return(await this.client.send_request("find_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).document}async find(e={},t={}){return(await this.client.send_request("find",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).documents||[]}async update_one(e,t,s={}){return this.client.send_request("update_one",{database:this.database_name,collection:this.collection_name,filter:e,update:t,options:s})}async delete_one(e,t={}){return this.client.send_request("delete_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async delete_many(e={},t={}){return this.client.send_request("delete_many",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async bulk_write(e,t={}){return this.client.send_request("bulk_write",{database:this.database_name,collection:this.collection_name,operations:e,options:t})}async create_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:t})}async upsert_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:{...t,upsert:!0}})}async drop_index(e){return this.client.send_request("drop_index",{database:this.database_name,collection:this.collection_name,field:e})}async get_indexes(){return this.client.send_request("get_indexes",{database:this.database_name,collection:this.collection_name})}}_.Collection=q;const g={client:n=>new _(n)};var x=g;export{x as default};
@@ -1 +1 @@
1
- import p from"net";import u from"../lib/op_types.js";import{send_success as l,send_error as a,send_message as g}from"../lib/send_response.js";import{shutdown_write_queue as w}from"../lib/write_queue.js";import{create_message_parser as m,encode_message as h}from"../lib/tcp_protocol.js";import f from"../lib/logger.js";import{initialize_database as y,cleanup_database as b}from"../lib/query_engine.js";import{handle_admin_operation as v,handle_ping_operation as k}from"../lib/operation_dispatcher.js";import{get_settings as $}from"../lib/load_settings.js";import{is_development_mode as S}from"../lib/development_mode.js";class D{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}=f("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=$();r?.data_path&&(s=r.data_path)}catch{}y(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=p.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=m(),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 o=r,i=o?.op||null;if(!i){a(e,{message:"Missing operation type"});continue}if(!this.check_op_type(i)){a(e,{message:"Invalid operation type"});continue}this.route_operation(e,i,o?.data||{})}}catch(s){this.log.error("Data parsing error",{socket_id:e.id,error_message:s.message}),a(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?u.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":k(e);break;case"admin":v(e,s,this.is_authenticated.bind(this));break;default:a(e,{message:`Unsupported operation: ${t}`})}}handle_authentication(e,t){if(this.is_authenticated(e))g(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)){a(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",op_type:t})}handle_write_operation(e,t,s){if(!this.is_authenticated(e)){a(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",op_type:t})}handle_write_response(e){const{write_id:t,success:s,result:r,error:o}=e.data,i=this.pending_writes.get(t);if(!i){this.log.warn("No pending write found",{write_id:t});return}const{socket:n,op_type:d}=i;if(this.pending_writes.delete(t),n.destroyed||!n.writable){this.log.warn("Socket disconnected before response could be sent",{write_id:t});return}try{if(s){let _;d==="find"?_={ok:1,documents:r}:d==="find_one"?_={ok:1,document:r}:_={ok:1,result:r};const c=h(_);n.write(c)}else{const c=h({ok:0,error:o});n.write(c)}}catch(_){this.log.error("Error sending response to client",{write_id:t,error:_.message})}}handle_auth_response(e){const{auth_id:t,success:s,message:r}=e.data,o=this.pending_writes.get(t);if(!o){this.log.warn("No pending auth found",{auth_id:t});return}const{socket:i}=o;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 d=h({ok:1,version:"1.0.0",message:r});i.write(d)}else a(i,{message:r}),i.end()}catch(n){this.log.error("Error sending auth response to client",{auth_id:t,error:n.message})}}handle_setup_response(e){const{setup_id:t,success:s,password:r,message:o,error:i}=e.data,n=this.pending_writes.get(t);if(!n){this.log.warn("No pending setup found",{setup_id:t});return}const{socket:d}=n;this.pending_writes.delete(t),s?l(d,{password:r,message:o}):a(d,{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 S()?!0: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 w(),this.log.info("Write queue shutdown complete")}catch(s){this.log.error("Error shutting down write queue",{error:s.message})}try{await b(),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 C=new D;
1
+ import p from"net";import u from"../lib/op_types.js";import{send_success as l,send_error as a,send_message as g}from"../lib/send_response.js";import{shutdown_write_queue as w}from"../lib/write_queue.js";import{create_message_parser as m,encode_message as h}from"../lib/tcp_protocol.js";import f from"../lib/logger.js";import{initialize_database as y,cleanup_database as b}from"../lib/query_engine.js";import{handle_admin_operation as v,handle_ping_operation as k}from"../lib/operation_dispatcher.js";import{get_settings as $}from"../lib/load_settings.js";import{is_development_mode as S}from"../lib/development_mode.js";class D{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}=f("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=$();r?.data_path&&(s=r.data_path)}catch{}y(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=p.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=m(),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 o=r,i=o?.op||null;if(!i){a(e,{message:"Missing operation type"});continue}if(!this.check_op_type(i)){a(e,{message:"Invalid operation type"});continue}this.route_operation(e,i,o?.data||{})}}catch(s){this.log.error("Data parsing error",{socket_id:e.id,error_message:s.message}),a(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?u.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":k(e);break;case"admin":v(e,s,this.is_authenticated.bind(this));break;default:a(e,{message:`Unsupported operation: ${t}`})}}handle_authentication(e,t){if(this.is_authenticated(e))g(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)){a(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",op_type:t})}handle_write_operation(e,t,s){if(!this.is_authenticated(e)){a(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",op_type:t})}handle_write_response(e){const{write_id:t,success:s,result:r,error:o}=e.data,i=this.pending_writes.get(t);if(!i){this.log.warn("No pending write found",{write_id:t});return}const{socket:n,op_type:d}=i;if(this.pending_writes.delete(t),n.destroyed||!n.writable){this.log.warn("Socket disconnected before response could be sent",{write_id:t});return}try{if(s){let _;d==="find_one"?_={ok:1,document:r}:d==="find"?_={ok:1,documents:r}:_={ok:1,...r};const c=h(_);n.write(c)}else{const c=h({ok:0,error:o});n.write(c)}}catch(_){this.log.error("Error sending response to client",{write_id:t,error:_.message})}}handle_auth_response(e){const{auth_id:t,success:s,message:r}=e.data,o=this.pending_writes.get(t);if(!o){this.log.warn("No pending auth found",{auth_id:t});return}const{socket:i}=o;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 d=h({ok:1,version:"1.0.0",message:r});i.write(d)}else a(i,{message:r}),i.end()}catch(n){this.log.error("Error sending auth response to client",{auth_id:t,error:n.message})}}handle_setup_response(e){const{setup_id:t,success:s,password:r,message:o,error:i}=e.data,n=this.pending_writes.get(t);if(!n){this.log.warn("No pending setup found",{setup_id:t});return}const{socket:d}=n;this.pending_writes.delete(t),s?l(d,{password:r,message:o}):a(d,{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 S()?!0: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 w(),this.log.info("Write queue shutdown complete")}catch(s){this.log.error("Error shutting down write queue",{error:s.message})}try{await b(),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 C=new D;
@@ -1 +1 @@
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};
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"delete_many":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};
@@ -1,4 +1,4 @@
1
- import s from"./logger.js";const{create_context_logger:t}=s("development_mode"),n=t(),l=()=>process.env.NODE_ENV==="development"||process.env.NODE_ENV==="test",r=(e,o)=>{console.log(`
1
+ import s from"./logger.js";const{create_context_logger:t}=s("development_mode"),n=t(),l=()=>process.env.NODE_ENV==="development",r=(e,o)=>{console.log(`
2
2
  JoystickDB Development Mode
3
3
  `),console.log("Development environment detected (NODE_ENV=development)."),console.log(`Security features have been bypassed for local development.
4
4
  `),console.log("Default admin user created:"),console.log(" Username: admin"),console.log(` Password: password
@@ -1 +1 @@
1
- const e=["authentication","setup","find_one","find","insert_one","update_one","delete_one","bulk_write","create_index","drop_index","get_indexes","admin","ping","reload"];var n=e;export{n as default};
1
+ const e=["authentication","setup","find_one","find","insert_one","update_one","delete_one","delete_many","bulk_write","create_index","drop_index","get_indexes","admin","ping","reload"];var n=e;export{n as default};
@@ -1 +1 @@
1
- import{encode_message as s}from"./tcp_protocol.js";import{get_write_forwarder as g}from"./write_forwarder.js";import{get_replication_manager as h}from"./replication_manager.js";import{check_and_grow_map_size as b}from"./query_engine.js";import{performance_monitor as f}from"./performance_monitor.js";import x from"./logger.js";import k from"./operations/insert_one.js";import v from"./operations/update_one.js";import q from"./operations/delete_one.js";import D from"./operations/bulk_write.js";import A from"./operations/find_one.js";import I from"./operations/find.js";import $ from"./operations/create_index.js";import C from"./operations/drop_index.js";import E from"./operations/get_indexes.js";import L from"./operations/admin.js";const{create_context_logger:U}=x("operation_dispatcher"),Z=o=>!o||typeof o!="string"||o.length>64||["admin","config","local"].includes(o.toLowerCase())?!1:/^[a-zA-Z0-9_-]+$/.test(o),Y=async(o,r,e,u,c=0,a=null,_=null)=>{const l=U(),m=Date.now();if(!u(o)){const t=s({ok:0,error:"Authentication required"});o.write(t),f.log_structured_operation(o.id,r,null,0,"error","Authentication required",c,0);return}const i=e.database||"default";if(!Z(i)){const t=s({ok:0,error:"Invalid database name. Database names must be alphanumeric with underscores/hyphens, max 64 characters, and cannot be reserved names (admin, config, local)."});o.write(t),f.log_structured_operation(o.id,r,e.collection,0,"error","Invalid database name",c,0);return}if(!await g().forward_operation(o,r,e))try{let n;switch(r){case"insert_one":n=await k(i,e.collection,e.document,e.options);break;case"update_one":n=await v(i,e.collection,e.filter,e.update,e.options);break;case"delete_one":n=await q(i,e.collection,e.filter,e.options);break;case"bulk_write":n=await D(i,e.collection,e.operations,e.options);break;case"find_one":n=await A(i,e.collection,e.filter,e.options);break;case"find":n=await I(i,e.collection,e.filter,e.options);break;case"create_index":n=await $(i,e.collection,e.field,e.options);break;case"drop_index":n=await C(i,e.collection,e.field);break;case"get_indexes":n=await E(i,e.collection);break;default:throw new Error(`Unsupported operation: ${r}`)}const t=Date.now()-m;let d;r==="find_one"?d={ok:1,document:n}:r==="find"?d={ok:1,documents:n}:d={ok:1,...n};const p=s(d),w=p.length;o.write(p),f.log_structured_operation(o.id,r,e.collection,t,"success",null,c,w),l.info("Database operation completed",{client_id:o.id,op:r,collection:e.collection,duration_ms:t,status:"success",request_size:c,response_size:w}),r!=="find"&&r!=="find_one"&&r!=="get_indexes"&&(h().queue_replication(r,e.collection,e),setImmediate(()=>b()))}catch(n){const t=Date.now()-m;f.log_structured_operation(o.id,r,e.collection,t,"error",n.message,c,0),l.error("Database operation failed",{client_id:o.id,op:r,collection:e.collection,duration_ms:t,status:"error",error:n.message,request_size:c});const d={ok:0,error:n.message},p=s(d);o.write(p)}},y=async(o,r,e,u=null,c=null)=>{if(!e(o)){const _=s({ok:!1,error:"Authentication required"});o.write(_);return}try{const a=r?.admin_action,l=await L(a,r||{},u,c);if(a){const m={ok:1,...l},i=s(m);o.write(i)}else{const m={ok:!0,...l},i=s(m);o.write(i)}}catch(a){const _={ok:0,error:`Admin operation failed: ${a.message}`},l=s(_);o.write(l)}},ee=o=>{const e=s({ok:1});o.write(e)};export{y as handle_admin_operation,Y as handle_database_operation,ee as handle_ping_operation};
1
+ import{encode_message as s}from"./tcp_protocol.js";import{get_write_forwarder as g}from"./write_forwarder.js";import{get_replication_manager as h}from"./replication_manager.js";import{check_and_grow_map_size as b}from"./query_engine.js";import{performance_monitor as f}from"./performance_monitor.js";import x from"./logger.js";import k from"./operations/insert_one.js";import v from"./operations/update_one.js";import q from"./operations/delete_one.js";import D from"./operations/delete_many.js";import A from"./operations/bulk_write.js";import I from"./operations/find_one.js";import $ from"./operations/find.js";import C from"./operations/create_index.js";import E from"./operations/drop_index.js";import L from"./operations/get_indexes.js";import U from"./operations/admin.js";const{create_context_logger:Z}=x("operation_dispatcher"),j=o=>!o||typeof o!="string"||o.length>64||["admin","config","local"].includes(o.toLowerCase())?!1:/^[a-zA-Z0-9_-]+$/.test(o),ee=async(o,r,e,u,c=0,a=null,d=null)=>{const l=Z(),m=Date.now();if(!u(o)){const t=s({ok:0,error:"Authentication required"});o.write(t),f.log_structured_operation(o.id,r,null,0,"error","Authentication required",c,0);return}const i=e.database||"default";if(!j(i)){const t=s({ok:0,error:"Invalid database name. Database names must be alphanumeric with underscores/hyphens, max 64 characters, and cannot be reserved names (admin, config, local)."});o.write(t),f.log_structured_operation(o.id,r,e.collection,0,"error","Invalid database name",c,0);return}if(!await g().forward_operation(o,r,e))try{let n;switch(r){case"insert_one":n=await k(i,e.collection,e.document,e.options);break;case"update_one":n=await v(i,e.collection,e.filter,e.update,e.options);break;case"delete_one":n=await q(i,e.collection,e.filter,e.options);break;case"delete_many":n=await D(i,e.collection,e.filter,e.options);break;case"bulk_write":n=await A(i,e.collection,e.operations,e.options);break;case"find_one":n=await I(i,e.collection,e.filter,e.options);break;case"find":n=await $(i,e.collection,e.filter,e.options);break;case"create_index":n=await C(i,e.collection,e.field,e.options);break;case"drop_index":n=await E(i,e.collection,e.field);break;case"get_indexes":n=await L(i,e.collection);break;default:throw new Error(`Unsupported operation: ${r}`)}const t=Date.now()-m;let _;r==="find_one"?_={ok:1,document:n}:r==="find"?_={ok:1,documents:n}:_={ok:1,...n};const p=s(_),w=p.length;o.write(p),f.log_structured_operation(o.id,r,e.collection,t,"success",null,c,w),l.info("Database operation completed",{client_id:o.id,op:r,collection:e.collection,duration_ms:t,status:"success",request_size:c,response_size:w}),r!=="find"&&r!=="find_one"&&r!=="get_indexes"&&(h().queue_replication(r,e.collection,e),setImmediate(()=>b()))}catch(n){const t=Date.now()-m;f.log_structured_operation(o.id,r,e.collection,t,"error",n.message,c,0),l.error("Database operation failed",{client_id:o.id,op:r,collection:e.collection,duration_ms:t,status:"error",error:n.message,request_size:c});const _={ok:0,error:n.message},p=s(_);o.write(p)}},oe=async(o,r,e,u=null,c=null)=>{if(!e(o)){const d=s({ok:!1,error:"Authentication required"});o.write(d);return}try{const a=r?.admin_action,l=await U(a,r||{},u,c);if(a){const m={ok:1,...l},i=s(m);o.write(i)}else{const m={ok:!0,...l},i=s(m);o.write(i)}}catch(a){const d={ok:0,error:`Admin operation failed: ${a.message}`},l=s(d);o.write(l)}},re=o=>{const e=s({ok:1});o.write(e)};export{oe as handle_admin_operation,ee as handle_database_operation,re as handle_ping_operation};
@@ -0,0 +1 @@
1
+ import{get_database as _}from"../query_engine.js";import{update_indexes_on_delete as w}from"../index_manager.js";import{get_write_queue as p}from"../write_queue.js";import g from"../logger.js";const{create_context_logger:y}=g("delete_many"),b=(t,e)=>{if(!e||Object.keys(e).length===0)return!0;for(const[r,o]of Object.entries(e))if(t[r]!==o)return!1;return!0},h=async(t,e,r,o={})=>{const s=y();if(!t)throw new Error("Database name is required");if(!e)throw new Error("Collection name is required");if(!r||typeof r!="object")throw new Error("Filter must be a valid object");const{limit:n}=o;if(n!==void 0&&(typeof n!="number"||n<0))throw new Error("Limit must be a non-negative number");const c=_();let i=0;const d=[];await c.transaction(()=>{const a=`${t}:${e}:`,l=c.getRange({start:a,end:a+"\xFF"});for(const{key:f,value:m}of l){if(n!==void 0&&i>=n)break;try{const u=JSON.parse(m);b(u,r)&&(c.remove(f),d.push(u),i++)}catch{continue}}});for(const a of d)await w(t,e,a);return s.info("Delete many operation completed",{database:t,collection:e,deleted_count:i,limit:n||"none"}),{acknowledged:!0,deleted_count:i,operation_time:new Date().toISOString()}},k=async(t,e,r,o={})=>await p().enqueue_write_operation(()=>h(t,e,r,o),{operation:"delete_many",database:t,collection:e,filter_keys:Object.keys(r||{}),limit:o.limit});var O=k;export{O as default};
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.2242",
5
- "canary_version": "0.0.0-canary.2241",
4
+ "version": "0.0.0-canary.2244",
5
+ "canary_version": "0.0.0-canary.2243",
6
6
  "description": "JoystickDB - A minimalist database server for the Joystick framework",
7
7
  "main": "./dist/server/index.js",
8
8
  "scripts": {
@@ -551,6 +551,23 @@ class JoystickDBClient extends EventEmitter {
551
551
  return result;
552
552
  }
553
553
 
554
+ // NOTE: Database Operations.
555
+ /**
556
+ * Deletes multiple documents from a collection.
557
+ * @param {string} collection - Collection name
558
+ * @param {Object} [filter={}] - Query filter to match documents
559
+ * @param {Object} [options={}] - Delete options
560
+ * @param {number} [options.limit] - Maximum number of documents to delete
561
+ * @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
562
+ */
563
+ async delete_many(collection, filter = {}, options = {}) {
564
+ return this.send_request('delete_many', {
565
+ collection,
566
+ filter,
567
+ options
568
+ });
569
+ }
570
+
554
571
  // NOTE: Database Interface.
555
572
  /**
556
573
  * Returns a database interface for method chaining operations.
@@ -668,6 +685,22 @@ class Collection {
668
685
  });
669
686
  }
670
687
 
688
+ /**
689
+ * Deletes multiple documents from the collection.
690
+ * @param {Object} [filter={}] - Query filter to match documents
691
+ * @param {Object} [options={}] - Delete options
692
+ * @param {number} [options.limit] - Maximum number of documents to delete
693
+ * @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
694
+ */
695
+ async delete_many(filter = {}, options = {}) {
696
+ return this.client.send_request('delete_many', {
697
+ database: this.database_name,
698
+ collection: this.collection_name,
699
+ filter,
700
+ options
701
+ });
702
+ }
703
+
671
704
  /**
672
705
  * Performs multiple write operations in a single request.
673
706
  * @param {Array<Object>} operations - Array of write operations
@@ -385,14 +385,15 @@ class ClusterWorker {
385
385
 
386
386
  try {
387
387
  if (success) {
388
- // NOTE: Handle different result types properly based on operation type
388
+ // NOTE: Format response consistently with operation dispatcher.
389
389
  let response;
390
- if (op_type === 'find') {
391
- response = { ok: 1, documents: result };
392
- } else if (op_type === 'find_one') {
390
+ if (op_type === 'find_one') {
393
391
  response = { ok: 1, document: result };
392
+ } else if (op_type === 'find') {
393
+ response = { ok: 1, documents: result };
394
394
  } else {
395
- response = { ok: 1, result };
395
+ // NOTE: For all other operations, spread the result properties directly.
396
+ response = { ok: 1, ...result };
396
397
  }
397
398
  const encoded_response = encode_message(response);
398
399
  socket.write(encoded_response);
@@ -369,6 +369,7 @@ export const create_server = async () => {
369
369
  case 'insert_one':
370
370
  case 'update_one':
371
371
  case 'delete_one':
372
+ case 'delete_many':
372
373
  case 'bulk_write':
373
374
  case 'find_one':
374
375
  case 'find':
@@ -12,10 +12,10 @@ const log = create_context_logger();
12
12
 
13
13
  /**
14
14
  * Checks if the server is running in development mode.
15
- * @returns {boolean} True if NODE_ENV is set to 'development' or 'test'
15
+ * @returns {boolean} True if NODE_ENV is set to 'development'
16
16
  */
17
17
  const is_development_mode = () => {
18
- return process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
18
+ return process.env.NODE_ENV === 'development';
19
19
  };
20
20
 
21
21
  /**
@@ -17,6 +17,7 @@ const op_types = [
17
17
  "insert_one",
18
18
  "update_one",
19
19
  "delete_one",
20
+ "delete_many",
20
21
  "bulk_write",
21
22
  "create_index",
22
23
  "drop_index",
@@ -15,6 +15,7 @@ import create_logger from './logger.js';
15
15
  import insert_one from './operations/insert_one.js';
16
16
  import update_one from './operations/update_one.js';
17
17
  import delete_one from './operations/delete_one.js';
18
+ import delete_many from './operations/delete_many.js';
18
19
  import bulk_write from './operations/bulk_write.js';
19
20
  import find_one from './operations/find_one.js';
20
21
  import find from './operations/find.js';
@@ -109,6 +110,10 @@ export const handle_database_operation = async (socket, op_type, data, check_aut
109
110
  result = await delete_one(database_name, data.collection, data.filter, data.options);
110
111
  break;
111
112
 
113
+ case 'delete_many':
114
+ result = await delete_many(database_name, data.collection, data.filter, data.options);
115
+ break;
116
+
112
117
  case 'bulk_write':
113
118
  result = await bulk_write(database_name, data.collection, data.operations, data.options);
114
119
  break;
@@ -0,0 +1,130 @@
1
+ import { get_database } from '../query_engine.js';
2
+ import { update_indexes_on_delete } from '../index_manager.js';
3
+ import { get_write_queue } from '../write_queue.js';
4
+ import create_logger from '../logger.js';
5
+
6
+ const { create_context_logger } = create_logger('delete_many');
7
+
8
+ /**
9
+ * Checks if a document matches the given filter criteria (simple equality check).
10
+ * @param {Object} document - Document to test
11
+ * @param {Object} filter - Filter criteria
12
+ * @returns {boolean} True if document matches filter
13
+ */
14
+ const matches_filter = (document, filter) => {
15
+ if (!filter || Object.keys(filter).length === 0) {
16
+ return true;
17
+ }
18
+
19
+ for (const [field, value] of Object.entries(filter)) {
20
+ if (document[field] !== value) {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ return true;
26
+ };
27
+
28
+ /**
29
+ * Internal implementation of delete_many operation without write queue serialization.
30
+ * @param {string} database_name - Name of the database
31
+ * @param {string} collection_name - Name of the collection
32
+ * @param {Object} filter - Filter criteria to match documents for deletion
33
+ * @param {Object} [options={}] - Delete options
34
+ * @param {number} [options.limit] - Maximum number of documents to delete
35
+ * @returns {Promise<Object>} Delete result with acknowledged and deleted_count
36
+ * @throws {Error} When database name, collection name is missing or filter is invalid
37
+ */
38
+ const delete_many_internal = async (database_name, collection_name, filter, options = {}) => {
39
+ const log = create_context_logger();
40
+
41
+ if (!database_name) {
42
+ throw new Error('Database name is required');
43
+ }
44
+
45
+ if (!collection_name) {
46
+ throw new Error('Collection name is required');
47
+ }
48
+
49
+ if (!filter || typeof filter !== 'object') {
50
+ throw new Error('Filter must be a valid object');
51
+ }
52
+
53
+ const { limit } = options;
54
+
55
+ if (limit !== undefined && (typeof limit !== 'number' || limit < 0)) {
56
+ throw new Error('Limit must be a non-negative number');
57
+ }
58
+
59
+ const db = get_database();
60
+ let deleted_count = 0;
61
+ const deleted_documents = [];
62
+
63
+ await db.transaction(() => {
64
+ const collection_prefix = `${database_name}:${collection_name}:`;
65
+
66
+ const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
67
+ for (const { key, value } of range) {
68
+ // NOTE: Check limit before processing more documents.
69
+ if (limit !== undefined && deleted_count >= limit) {
70
+ break;
71
+ }
72
+
73
+ try {
74
+ const document = JSON.parse(value);
75
+ if (matches_filter(document, filter)) {
76
+ db.remove(key);
77
+ deleted_documents.push(document);
78
+ deleted_count++;
79
+ }
80
+ } catch (parse_error) {
81
+ // Skip documents that can't be parsed
82
+ continue;
83
+ }
84
+ }
85
+ });
86
+
87
+ // NOTE: Update indexes for all deleted documents.
88
+ for (const deleted_document of deleted_documents) {
89
+ await update_indexes_on_delete(database_name, collection_name, deleted_document);
90
+ }
91
+
92
+ log.info('Delete many operation completed', {
93
+ database: database_name,
94
+ collection: collection_name,
95
+ deleted_count,
96
+ limit: limit || 'none'
97
+ });
98
+
99
+ return {
100
+ acknowledged: true,
101
+ deleted_count,
102
+ operation_time: new Date().toISOString()
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Deletes multiple documents from a collection with write queue serialization.
108
+ * @param {string} database_name - Name of the database
109
+ * @param {string} collection_name - Name of the collection
110
+ * @param {Object} filter - Filter criteria to match documents for deletion
111
+ * @param {Object} [options={}] - Delete options
112
+ * @param {number} [options.limit] - Maximum number of documents to delete
113
+ * @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
114
+ */
115
+ const delete_many = async (database_name, collection_name, filter, options = {}) => {
116
+ const write_queue = get_write_queue();
117
+
118
+ return await write_queue.enqueue_write_operation(
119
+ () => delete_many_internal(database_name, collection_name, filter, options),
120
+ {
121
+ operation: 'delete_many',
122
+ database: database_name,
123
+ collection: collection_name,
124
+ filter_keys: Object.keys(filter || {}),
125
+ limit: options.limit
126
+ }
127
+ );
128
+ };
129
+
130
+ export default delete_many;