@joystick.js/db-canary 0.0.0-canary.2274 → 0.0.0-canary.2276

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 (39) hide show
  1. package/README.md +87 -104
  2. package/debug_test_runner.js +208 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/server/cluster/master.js +2 -2
  5. package/dist/server/cluster/worker.js +1 -1
  6. package/dist/server/index.js +1 -1
  7. package/dist/server/lib/auto_index_manager.js +1 -1
  8. package/dist/server/lib/bulk_insert_optimizer.js +1 -1
  9. package/dist/server/lib/http_server.js +3 -3
  10. package/dist/server/lib/operation_dispatcher.js +1 -1
  11. package/dist/server/lib/operations/admin.js +1 -1
  12. package/dist/server/lib/operations/update_one.js +1 -1
  13. package/dist/server/lib/simple_sync_manager.js +1 -0
  14. package/dist/server/lib/sync_receiver.js +1 -0
  15. package/full_debug_test_runner.js +197 -0
  16. package/package.json +10 -7
  17. package/src/client/index.js +1 -0
  18. package/src/server/cluster/master.js +8 -2
  19. package/src/server/cluster/worker.js +9 -3
  20. package/src/server/index.js +25 -24
  21. package/src/server/lib/auto_index_manager.js +8 -3
  22. package/src/server/lib/bulk_insert_optimizer.js +79 -0
  23. package/src/server/lib/http_server.js +7 -0
  24. package/src/server/lib/operation_dispatcher.js +16 -10
  25. package/src/server/lib/operations/admin.js +64 -31
  26. package/src/server/lib/operations/update_one.js +251 -1
  27. package/src/server/lib/simple_sync_manager.js +444 -0
  28. package/src/server/lib/sync_receiver.js +461 -0
  29. package/tests/client/index.test.js +7 -0
  30. package/tests/performance/isolated_5000000_test.js +184 -0
  31. package/tests/server/lib/http_server.test.js +3 -12
  32. package/tests/server/lib/operations/update_one.test.js +161 -0
  33. package/tests/server/lib/simple_sync_system.test.js +124 -0
  34. package/dist/server/lib/replication_manager.js +0 -1
  35. package/dist/server/lib/write_forwarder.js +0 -1
  36. package/src/server/lib/replication_manager.js +0 -727
  37. package/src/server/lib/write_forwarder.js +0 -636
  38. package/tests/server/lib/replication_manager.test.js +0 -202
  39. package/tests/server/lib/write_forwarder.test.js +0 -258
@@ -87,3 +87,164 @@ test('update_one - should throw on unsupported operator', async (t) => {
87
87
  const { inserted_id } = await insert_one('default', 'users', { name: 'Grace' });
88
88
  await t.throwsAsync(() => update_one('default', 'users', { _id: inserted_id }, { $foo: { a: 1 } }), { message: 'Unsupported update operator: $foo' });
89
89
  });
90
+
91
+ test('update_one - should update a document with $add_to_set (adds new item)', async (t) => {
92
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Helen', tags: ['a', 'b'] });
93
+ await update_one('default', 'users', { _id: inserted_id }, { $add_to_set: { tags: 'c' } });
94
+ const db = get_database();
95
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
96
+ t.deepEqual(doc.tags, ['a', 'b', 'c']);
97
+ });
98
+
99
+ test('update_one - should update a document with $add_to_set (skips duplicate)', async (t) => {
100
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Isaac', tags: ['a', 'b'] });
101
+ await update_one('default', 'users', { _id: inserted_id }, { $add_to_set: { tags: 'b' } });
102
+ const db = get_database();
103
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
104
+ t.deepEqual(doc.tags, ['a', 'b']);
105
+ });
106
+
107
+ test('update_one - should update a document with $add_to_set (creates array if not exists)', async (t) => {
108
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Jack' });
109
+ await update_one('default', 'users', { _id: inserted_id }, { $add_to_set: { tags: 'new' } });
110
+ const db = get_database();
111
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
112
+ t.deepEqual(doc.tags, ['new']);
113
+ });
114
+
115
+ test('update_one - should update a document with $pull_all', async (t) => {
116
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Kate', tags: ['a', 'b', 'c', 'b', 'd'] });
117
+ await update_one('default', 'users', { _id: inserted_id }, { $pull_all: { tags: ['b', 'c'] } });
118
+ const db = get_database();
119
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
120
+ t.deepEqual(doc.tags, ['a', 'd']);
121
+ });
122
+
123
+ test('update_one - should update a document with $pop (remove last)', async (t) => {
124
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Leo', tags: ['a', 'b', 'c'] });
125
+ await update_one('default', 'users', { _id: inserted_id }, { $pop: { tags: 1 } });
126
+ const db = get_database();
127
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
128
+ t.deepEqual(doc.tags, ['a', 'b']);
129
+ });
130
+
131
+ test('update_one - should update a document with $pop (remove first)', async (t) => {
132
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Mia', tags: ['a', 'b', 'c'] });
133
+ await update_one('default', 'users', { _id: inserted_id }, { $pop: { tags: -1 } });
134
+ const db = get_database();
135
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
136
+ t.deepEqual(doc.tags, ['b', 'c']);
137
+ });
138
+
139
+ test('update_one - should update a document with $rename', async (t) => {
140
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Nina', old_field: 'value' });
141
+ await update_one('default', 'users', { _id: inserted_id }, { $rename: { old_field: 'new_field' } });
142
+ const db = get_database();
143
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
144
+ t.is(doc.new_field, 'value');
145
+ t.falsy(doc.old_field);
146
+ });
147
+
148
+ test('update_one - should update a document with $rename (nested fields)', async (t) => {
149
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Oscar', profile: { old_name: 'value' } });
150
+ await update_one('default', 'users', { _id: inserted_id }, { $rename: { 'profile.old_name': 'profile.new_name' } });
151
+ const db = get_database();
152
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
153
+ t.is(doc.profile.new_name, 'value');
154
+ t.falsy(doc.profile.old_name);
155
+ });
156
+
157
+ test('update_one - should update a document with $min (updates when new value is smaller)', async (t) => {
158
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Paul', score: 10 });
159
+ await update_one('default', 'users', { _id: inserted_id }, { $min: { score: 5 } });
160
+ const db = get_database();
161
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
162
+ t.is(doc.score, 5);
163
+ });
164
+
165
+ test('update_one - should update a document with $min (keeps current when new value is larger)', async (t) => {
166
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Quinn', score: 10 });
167
+ await update_one('default', 'users', { _id: inserted_id }, { $min: { score: 15 } });
168
+ const db = get_database();
169
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
170
+ t.is(doc.score, 10);
171
+ });
172
+
173
+ test('update_one - should update a document with $max (updates when new value is larger)', async (t) => {
174
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Rachel', score: 10 });
175
+ await update_one('default', 'users', { _id: inserted_id }, { $max: { score: 15 } });
176
+ const db = get_database();
177
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
178
+ t.is(doc.score, 15);
179
+ });
180
+
181
+ test('update_one - should update a document with $max (keeps current when new value is smaller)', async (t) => {
182
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Sam', score: 10 });
183
+ await update_one('default', 'users', { _id: inserted_id }, { $max: { score: 5 } });
184
+ const db = get_database();
185
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
186
+ t.is(doc.score, 10);
187
+ });
188
+
189
+ test('update_one - should update a document with $mul', async (t) => {
190
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Tom', score: 5 });
191
+ await update_one('default', 'users', { _id: inserted_id }, { $mul: { score: 3 } });
192
+ const db = get_database();
193
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
194
+ t.is(doc.score, 15);
195
+ });
196
+
197
+ test('update_one - should update a document with $mul (missing field defaults to 0)', async (t) => {
198
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Uma' });
199
+ await update_one('default', 'users', { _id: inserted_id }, { $mul: { score: 5 } });
200
+ const db = get_database();
201
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
202
+ t.is(doc.score, 0);
203
+ });
204
+
205
+ test('update_one - should update a document with $current_date (default)', async (t) => {
206
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Victor' });
207
+ const before_time = new Date();
208
+ await update_one('default', 'users', { _id: inserted_id }, { $current_date: { last_seen: true } });
209
+ const after_time = new Date();
210
+
211
+ const db = get_database();
212
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
213
+ t.true(doc.last_seen instanceof Date || typeof doc.last_seen === 'string');
214
+
215
+ const doc_time = new Date(doc.last_seen);
216
+ t.true(doc_time >= before_time && doc_time <= after_time);
217
+ });
218
+
219
+ test('update_one - should update a document with $current_date (timestamp)', async (t) => {
220
+ const { inserted_id } = await insert_one('default', 'users', { name: 'Wendy' });
221
+ await update_one('default', 'users', { _id: inserted_id }, { $current_date: { last_seen: { $type: 'timestamp' } } });
222
+
223
+ const db = get_database();
224
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
225
+ t.true(typeof doc.last_seen === 'string');
226
+ t.true(doc.last_seen.includes('T')); // ISO string format
227
+ });
228
+
229
+ test('update_one - should handle nested fields with all new operators', async (t) => {
230
+ const { inserted_id } = await insert_one('default', 'users', {
231
+ name: 'Xavier',
232
+ profile: {
233
+ score: 10,
234
+ tags: ['old'],
235
+ count: 5
236
+ }
237
+ });
238
+
239
+ await update_one('default', 'users', { _id: inserted_id }, {
240
+ $add_to_set: { 'profile.tags': 'new' },
241
+ $min: { 'profile.score': 8 },
242
+ $inc: { 'profile.count': 2 }
243
+ });
244
+
245
+ const db = get_database();
246
+ const doc = JSON.parse(db.get(`default:users:${inserted_id}`));
247
+ t.deepEqual(doc.profile.tags, ['old', 'new']);
248
+ t.is(doc.profile.score, 8);
249
+ t.is(doc.profile.count, 7);
250
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @fileoverview Basic tests for the simplified primary/secondary sync system.
3
+ * Tests API_KEY authentication, sync operations, and basic functionality.
4
+ */
5
+
6
+ import test from 'ava';
7
+ import { get_simple_sync_manager } from '../../../src/server/lib/simple_sync_manager.js';
8
+ import { get_sync_receiver } from '../../../src/server/lib/sync_receiver.js';
9
+
10
+ test('simple sync manager should initialize correctly', t => {
11
+ const sync_manager = get_simple_sync_manager();
12
+
13
+ t.truthy(sync_manager);
14
+ t.is(typeof sync_manager.initialize, 'function');
15
+ t.is(typeof sync_manager.queue_sync, 'function');
16
+ t.is(typeof sync_manager.get_sync_status, 'function');
17
+ t.is(typeof sync_manager.shutdown, 'function');
18
+ });
19
+
20
+ test('sync receiver should initialize correctly', t => {
21
+ const sync_receiver = get_sync_receiver();
22
+
23
+ t.truthy(sync_receiver);
24
+ t.is(typeof sync_receiver.initialize, 'function');
25
+ t.is(typeof sync_receiver.should_block_client_operation, 'function');
26
+ t.is(typeof sync_receiver.get_sync_status, 'function');
27
+ t.is(typeof sync_receiver.shutdown, 'function');
28
+ });
29
+
30
+ test('sync receiver should block write operations on secondary nodes', t => {
31
+ const sync_receiver = get_sync_receiver();
32
+
33
+ // Mock secondary mode
34
+ sync_receiver.is_secondary = true;
35
+
36
+ // Should block write operations
37
+ t.true(sync_receiver.should_block_client_operation('insert_one'));
38
+ t.true(sync_receiver.should_block_client_operation('update_one'));
39
+ t.true(sync_receiver.should_block_client_operation('delete_one'));
40
+ t.true(sync_receiver.should_block_client_operation('bulk_write'));
41
+
42
+ // Should allow read operations
43
+ t.false(sync_receiver.should_block_client_operation('find'));
44
+ t.false(sync_receiver.should_block_client_operation('find_one'));
45
+ t.false(sync_receiver.should_block_client_operation('count_documents'));
46
+ t.false(sync_receiver.should_block_client_operation('get_indexes'));
47
+
48
+ // Reset
49
+ sync_receiver.is_secondary = false;
50
+ });
51
+
52
+ test('sync receiver should not block operations on primary nodes', t => {
53
+ const sync_receiver = get_sync_receiver();
54
+
55
+ // Mock primary mode (default)
56
+ sync_receiver.is_secondary = false;
57
+
58
+ // Should not block any operations on primary
59
+ t.false(sync_receiver.should_block_client_operation('insert_one'));
60
+ t.false(sync_receiver.should_block_client_operation('update_one'));
61
+ t.false(sync_receiver.should_block_client_operation('find'));
62
+ t.false(sync_receiver.should_block_client_operation('find_one'));
63
+ });
64
+
65
+ test('simple sync manager should provide status information', t => {
66
+ const sync_manager = get_simple_sync_manager();
67
+ const status = sync_manager.get_sync_status();
68
+
69
+ t.is(typeof status, 'object');
70
+ t.is(typeof status.is_primary, 'boolean');
71
+ t.is(typeof status.secondary_count, 'number');
72
+ t.is(typeof status.stats, 'object');
73
+ t.is(typeof status.secondaries, 'object');
74
+ });
75
+
76
+ test('sync receiver should provide status information', t => {
77
+ const sync_receiver = get_sync_receiver();
78
+ const status = sync_receiver.get_sync_status();
79
+
80
+ t.is(typeof status, 'object');
81
+ t.is(typeof status.is_secondary, 'boolean');
82
+ t.is(typeof status.api_key_loaded, 'boolean');
83
+ t.is(typeof status.server_running, 'boolean');
84
+ t.is(typeof status.sync_port, 'number');
85
+ t.is(typeof status.stats, 'object');
86
+ });
87
+
88
+ test('sync manager should handle empty secondary nodes list', t => {
89
+ const sync_manager = get_simple_sync_manager();
90
+
91
+ // Should not throw when updating with empty list
92
+ t.notThrows(() => {
93
+ sync_manager.update_secondary_nodes([]);
94
+ });
95
+ });
96
+
97
+ test('sync receiver should handle manual promotion to primary', t => {
98
+ const sync_receiver = get_sync_receiver();
99
+
100
+ // Mock secondary mode
101
+ sync_receiver.is_secondary = true;
102
+ sync_receiver.server = { close: () => {} }; // Mock server
103
+
104
+ // Should be able to promote to primary
105
+ t.notThrows(() => {
106
+ sync_receiver.promote_to_primary();
107
+ });
108
+
109
+ // Should now be in primary mode
110
+ t.false(sync_receiver.is_secondary);
111
+ });
112
+
113
+ test.after.always(async t => {
114
+ // Clean up any resources
115
+ try {
116
+ const sync_manager = get_simple_sync_manager();
117
+ const sync_receiver = get_sync_receiver();
118
+
119
+ await sync_manager.shutdown();
120
+ await sync_receiver.shutdown();
121
+ } catch (error) {
122
+ // Ignore cleanup errors in tests
123
+ }
124
+ });
@@ -1 +0,0 @@
1
- import p from"net";import u from"crypto";import{get_settings as g}from"./load_settings.js";import{encode_message as d,create_message_parser as m}from"./tcp_protocol.js";import y from"./logger.js";const{create_context_logger:f}=y("replication");class w{constructor(){this.secondary_connections=new Map,this.replication_queue=[],this.processing_replication=!1,this.sequence_number=0,this.enabled=!1,this.mode="async",this.timeout_ms=5e3,this.retry_attempts=3,this.batch_size=100,this.log=f(),this.stats={total_operations_replicated:0,successful_replications:0,failed_replications:0,connected_secondaries:0,avg_replication_latency_ms:0,total_replication_time_ms:0}}initialize(){try{const e=g().replication;if(!e||!e.enabled){this.log.info("Replication disabled in settings");return}this.enabled=e.enabled,this.mode=e.mode||"async",this.timeout_ms=e.timeout_ms||5e3,this.retry_attempts=e.retry_attempts||3,this.batch_size=e.batch_size||100;const n=e.secondaries||[];this.log.info("Initializing replication manager",{enabled:this.enabled,mode:this.mode,secondary_count:n.length,timeout_ms:this.timeout_ms,batch_size:this.batch_size});for(const s of n)s.enabled&&this.connect_to_secondary(s);this.start_replication_processing()}catch(t){this.log.warn("Could not initialize replication - settings not loaded",{error:t.message})}}async connect_to_secondary(t){const{id:e,ip:n,port:s,private_key:i}=t;this.log.info("Connecting to secondary node",{id:e,ip:n,port:s});try{const o=new p.Socket,l=m();o.connect(s,n,()=>{this.log.info("Connected to secondary node",{id:e,ip:n,port:s}),this.authenticate_with_secondary(o,e,i)}),o.on("data",a=>{try{const c=l.parse_messages(a);for(const _ of c)this.handle_secondary_response(e,_)}catch(c){this.log.error("Failed to parse secondary response",{secondary_id:e,error:c.message})}}),o.on("error",a=>{this.log.error("Secondary connection error",{secondary_id:e,error:a.message}),this.secondary_connections.delete(e),this.stats.connected_secondaries=this.secondary_connections.size,setTimeout(()=>{this.connect_to_secondary(t)},5e3)}),o.on("close",()=>{this.log.warn("Secondary connection closed",{secondary_id:e}),this.secondary_connections.delete(e),this.stats.connected_secondaries=this.secondary_connections.size,setTimeout(()=>{this.connect_to_secondary(t)},5e3)}),this.secondary_connections.set(e,{socket:o,id:e,ip:n,port:s,authenticated:!1,last_ping:Date.now(),pending_operations:new Map}),this.stats.connected_secondaries=this.secondary_connections.size}catch(o){this.log.error("Failed to connect to secondary",{secondary_id:e,ip:n,port:s,error:o.message})}}authenticate_with_secondary(t,e,n){try{const s=Date.now(),i=`${e}:${s}`,o=u.createHmac("sha256",Buffer.from(n,"base64")).update(i).digest("base64"),a=d({type:"replication_auth",node_id:e,timestamp:s,signature:o});t.write(a),this.log.debug("Sent authentication to secondary",{secondary_id:e})}catch(s){this.log.error("Failed to authenticate with secondary",{secondary_id:e,error:s.message})}}handle_secondary_response(t,e){const n=this.secondary_connections.get(t);if(n)try{const s=typeof e=="string"?JSON.parse(e):e;switch(s.type){case"auth_success":n.authenticated=!0,this.log.info("Secondary authentication successful",{secondary_id:t});break;case"auth_failed":this.log.error("Secondary authentication failed",{secondary_id:t}),n.socket.end();break;case"replication_ack":this.handle_replication_acknowledgment(t,s);break;case"ping_response":n.last_ping=Date.now();break;default:this.log.debug("Unknown message from secondary",{secondary_id:t,type:s.type})}}catch(s){this.log.error("Failed to handle secondary response",{secondary_id:t,error:s.message})}}handle_replication_acknowledgment(t,e){const n=this.secondary_connections.get(t);if(!n)return;const{sequence_number:s,status:i,error:o}=e,l=n.pending_operations.get(s);if(l){const a=Date.now()-l.sent_at;this.stats.total_replication_time_ms+=a,i==="success"?(this.stats.successful_replications++,this.log.debug("Replication acknowledged",{secondary_id:t,sequence_number:s,latency_ms:a})):(this.stats.failed_replications++,this.log.error("Replication failed on secondary",{secondary_id:t,sequence_number:s,error:o,latency_ms:a})),n.pending_operations.delete(s);const c=this.stats.successful_replications+this.stats.failed_replications;this.stats.avg_replication_latency_ms=c>0?Math.round(this.stats.total_replication_time_ms/c):0}}queue_replication(t,e,n,s=null){if(!this.enabled||this.secondary_connections.size===0)return;const i={operation:t,collection:e,data:n,transaction_id:s,timestamp:Date.now(),sequence_number:++this.sequence_number};this.replication_queue.push(i),this.stats.total_operations_replicated++,this.log.debug("Queued operation for replication",{operation:t,collection:e,sequence_number:i.sequence_number,queue_length:this.replication_queue.length}),this.processing_replication||setImmediate(()=>this.process_replication_queue())}start_replication_processing(){this.processing_replication||(this.processing_replication=!0,this.process_replication_queue())}async process_replication_queue(){for(;this.replication_queue.length>0&&this.enabled;){const t=this.replication_queue.splice(0,this.batch_size);if(t.length===0)break;await this.replicate_batch(t),this.replication_queue.length>0&&await new Promise(e=>setTimeout(e,10))}this.replication_queue.length>0&&setImmediate(()=>this.process_replication_queue())}async replicate_batch(t){const e={type:"replication",timestamp:Date.now(),sequence_number:t[0].sequence_number,operations:t},n=d(e),s=Array.from(this.secondary_connections.values()).filter(i=>i.authenticated);if(s.length===0){this.log.warn("No authenticated secondaries available for replication");return}this.log.debug("Replicating batch to secondaries",{batch_size:t.length,secondary_count:s.length,sequence_number:t[0].sequence_number});for(const i of s)try{i.socket.write(n);for(const o of t)i.pending_operations.set(o.sequence_number,{operation:o,sent_at:Date.now()})}catch(o){this.log.error("Failed to send replication to secondary",{secondary_id:i.id,error:o.message})}}async add_secondary(t){const{id:e}=t;if(this.secondary_connections.has(e))throw new Error(`Secondary node ${e} already exists`);return this.log.info("Adding new secondary node",{id:e}),await this.connect_to_secondary(t),{success:!0,message:`Secondary node ${e} added successfully`,secondary_id:e}}remove_secondary(t){const e=this.secondary_connections.get(t);if(!e)throw new Error(`Secondary node ${t} not found`);return this.log.info("Removing secondary node",{secondary_id:t}),e.socket.end(),this.secondary_connections.delete(t),this.stats.connected_secondaries=this.secondary_connections.size,{success:!0,message:`Secondary node ${t} removed successfully`,secondary_id:t}}async sync_secondaries(){if(!this.enabled)throw new Error("Replication is not enabled");this.log.info("Forcing secondary synchronization"),await this.process_replication_queue();const t={type:"ping",timestamp:Date.now()},e=d(t),n=[];for(const[s,i]of this.secondary_connections)try{i.authenticated?(i.socket.write(e),n.push({secondary_id:s,status:"ping_sent"})):n.push({secondary_id:s,status:"not_authenticated"})}catch(o){n.push({secondary_id:s,status:"error",error:o.message})}return{success:!0,message:"Secondary synchronization initiated",results:n}}get_replication_status(){const t=[];for(const[e,n]of this.secondary_connections)t.push({id:e,ip:n.ip,port:n.port,authenticated:n.authenticated,last_ping:n.last_ping,pending_operations:n.pending_operations.size,connected:!n.socket.destroyed});return{enabled:this.enabled,mode:this.mode,connected_secondaries:this.stats.connected_secondaries,queue_length:this.replication_queue.length,processing:this.processing_replication,stats:this.stats,secondaries:t}}get_secondary_health(){const t=[],e=Date.now();for(const[n,s]of this.secondary_connections){const i=e-s.last_ping,o=s.authenticated&&!s.socket.destroyed&&i<3e4;t.push({id:n,ip:s.ip,port:s.port,healthy:o,authenticated:s.authenticated,connected:!s.socket.destroyed,last_ping_age_ms:i,pending_operations:s.pending_operations.size})}return{total_secondaries:t.length,healthy_secondaries:t.filter(n=>n.healthy).length,secondaries:t}}async shutdown(){this.log.info("Shutting down replication manager"),this.enabled=!1,this.processing_replication=!1;for(const[t,e]of this.secondary_connections)try{e.socket.end()}catch(n){this.log.warn("Error closing secondary connection",{secondary_id:t,error:n.message})}this.secondary_connections.clear(),this.replication_queue=[],this.stats.connected_secondaries=0,this.log.info("Replication manager shutdown complete")}}let r=null;const b=()=>(r||(r=new w),r),v=()=>{b().initialize()},R=async()=>{r&&(await r.shutdown(),r=null)};export{b as get_replication_manager,v as initialize_replication_manager,R as shutdown_replication_manager};
@@ -1 +0,0 @@
1
- import h from"net";import m from"crypto";import{get_settings as p}from"./load_settings.js";import{encode_message as a,create_message_parser as l}from"./tcp_protocol.js";import f from"./logger.js";const{create_context_logger:w}=f("write_forwarder");class g{constructor(){this.primary_connection=null,this.is_secondary_mode=!1,this.primary_config=null,this.pending_forwards=new Map,this.forward_timeout_ms=1e4,this.reconnect_delay_ms=5e3,this.log=w(),this.stats={total_forwards:0,successful_forwards:0,failed_forwards:0,avg_forward_latency_ms:0,total_forward_time_ms:0,connection_attempts:0,last_connection_attempt:null}}initialize(){try{const r=p();if(r.mode!=="secondary"||!r.primary){this.log.info("Node not configured as secondary - write forwarding disabled");return}this.is_secondary_mode=!0,this.primary_config=r.primary,this.forward_timeout_ms=r.replication?.timeout_ms||1e4,this.log.info("Initializing write forwarder for secondary mode",{primary_ip:this.primary_config.ip,primary_port:this.primary_config.port,timeout_ms:this.forward_timeout_ms}),this.connect_to_primary()}catch(r){this.log.warn("Could not initialize write forwarder - settings not loaded",{error:r.message})}}async connect_to_primary(){if(!this.is_secondary_mode||!this.primary_config)return;const{ip:r,port:t,public_key:o}=this.primary_config;this.log.info("Connecting to primary node",{ip:r,port:t}),this.stats.connection_attempts++,this.stats.last_connection_attempt=Date.now();try{const e=new h.Socket,n=l();e.connect(t,r,()=>{this.log.info("Connected to primary node",{ip:r,port:t}),this.authenticate_with_primary(e,o)}),e.on("data",i=>{try{const s=n.parse_messages(i);for(const d of s)this.handle_primary_response(d)}catch(s){this.log.error("Failed to parse primary response",{error:s.message})}}),e.on("error",i=>{this.log.error("Primary connection error",{error:i.message}),this.primary_connection=null,this.fail_pending_forwards("Primary connection error"),setTimeout(()=>{this.connect_to_primary()},this.reconnect_delay_ms)}),e.on("close",()=>{this.log.warn("Primary connection closed"),this.primary_connection=null,this.fail_pending_forwards("Primary connection closed"),setTimeout(()=>{this.connect_to_primary()},this.reconnect_delay_ms)}),this.primary_connection={socket:e,authenticated:!1,last_ping:Date.now()}}catch(e){this.log.error("Failed to connect to primary",{ip:r,port:t,error:e.message}),setTimeout(()=>{this.connect_to_primary()},this.reconnect_delay_ms)}}authenticate_with_primary(r,t){try{const o=Date.now(),e=`secondary-${process.pid}`,n=`${e}:${o}`,s={op:"authentication",data:{password:m.createHmac("sha256",Buffer.from(t,"base64")).update(n).digest("base64"),node_type:"secondary",node_id:e}},d=a(s);r.write(d),this.log.debug("Sent authentication to primary")}catch(o){this.log.error("Failed to authenticate with primary",{error:o.message})}}handle_primary_response(r){if(this.primary_connection)try{const t=typeof r=="string"?JSON.parse(r):r;if(t.ok===1&&t.message==="Authentication successful"){this.primary_connection.authenticated=!0,this.log.info("Primary authentication successful");return}if(t.forward_id){this.handle_forward_response(t);return}if(t.ok===1&&!t.forward_id){this.primary_connection.last_ping=Date.now();return}this.log.debug("Unhandled primary response",{type:typeof t,keys:Object.keys(t)})}catch(t){this.log.error("Failed to handle primary response",{error:t.message})}}handle_forward_response(r){const{forward_id:t}=r,o=this.pending_forwards.get(t);if(!o){this.log.warn("Received response for unknown forward",{forward_id:t});return}const e=Date.now()-o.sent_at;this.stats.total_forward_time_ms+=e,clearTimeout(o.timeout),this.pending_forwards.delete(t);const n={...r};delete n.forward_id,r.ok===1||r.ok===!0?(this.stats.successful_forwards++,this.log.debug("Forward operation successful",{forward_id:t,latency_ms:e,operation:o.operation})):(this.stats.failed_forwards++,this.log.error("Forward operation failed",{forward_id:t,latency_ms:e,operation:o.operation,error:r.error}));const i=this.stats.successful_forwards+this.stats.failed_forwards;this.stats.avg_forward_latency_ms=i>0?Math.round(this.stats.total_forward_time_ms/i):0;try{const s=a(n);o.client_socket.write(s)}catch(s){this.log.error("Failed to send response to client",{forward_id:t,error:s.message})}}should_forward_operation(r){return this.is_secondary_mode?["insert_one","update_one","delete_one","bulk_write","create_index","drop_index"].includes(r):!1}async forward_operation(r,t,o){if(!this.is_secondary_mode||!this.should_forward_operation(t))return!1;if(!this.primary_connection||!this.primary_connection.authenticated){this.log.error("Cannot forward operation - not connected to primary",{operation:t,connected:!!this.primary_connection,authenticated:this.primary_connection?.authenticated||!1});const s=a({ok:0,error:"Secondary node not connected to primary"});return r.write(s),!0}const e=this.generate_forward_id();this.log.debug("Forwarding operation to primary",{forward_id:e,operation:t,client_id:r.id});const n={op:t,data:o,forward_id:e,forwarded_by:`secondary-${process.pid}`,original_client_id:r.id,timestamp:Date.now()};try{const i=a(n);this.primary_connection.socket.write(i);const s=setTimeout(()=>{this.handle_forward_timeout(e)},this.forward_timeout_ms);return this.pending_forwards.set(e,{client_socket:r,operation:t,data:o,sent_at:Date.now(),timeout:s}),this.stats.total_forwards++,!0}catch(i){this.log.error("Failed to forward operation",{forward_id:e,operation:t,error:i.message});const s={ok:0,error:`Failed to forward operation: ${i.message}`},d=a(s);return r.write(d),!0}}handle_forward_timeout(r){const t=this.pending_forwards.get(r);if(!t)return;this.log.error("Forward operation timed out",{forward_id:r,operation:t.operation,timeout_ms:this.forward_timeout_ms}),this.pending_forwards.delete(r),this.stats.failed_forwards++;const o=Date.now()-t.sent_at;this.stats.total_forward_time_ms+=o;const e=this.stats.successful_forwards+this.stats.failed_forwards;this.stats.avg_forward_latency_ms=e>0?Math.round(this.stats.total_forward_time_ms/e):0;try{const i=a({ok:0,error:"Operation forwarding timed out"});t.client_socket.write(i)}catch(n){this.log.error("Failed to send timeout error to client",{forward_id:r,error:n.message})}}fail_pending_forwards(r){for(const[t,o]of this.pending_forwards){clearTimeout(o.timeout),this.log.error("Failing pending forward operation",{forward_id:t,operation:o.operation,reason:r});try{const e={ok:0,error:`Forward operation failed: ${r}`},n=a(e);o.client_socket.write(n)}catch(e){this.log.error("Failed to send error to client",{forward_id:t,error:e.message})}}this.stats.failed_forwards+=this.pending_forwards.size,this.pending_forwards.clear()}generate_forward_id(){return`fwd_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}get_forwarder_status(){return{enabled:this.is_secondary_mode,connected_to_primary:!!this.primary_connection&&this.primary_connection.authenticated,primary_config:this.primary_config?{ip:this.primary_config.ip,port:this.primary_config.port}:null,pending_forwards:this.pending_forwards.size,stats:this.stats}}async shutdown(){if(this.log.info("Shutting down write forwarder"),this.fail_pending_forwards("Server shutting down"),this.primary_connection){try{this.primary_connection.socket.end()}catch(r){this.log.warn("Error closing primary connection",{error:r.message})}this.primary_connection=null}this.is_secondary_mode=!1,this.log.info("Write forwarder shutdown complete")}}let c=null;const u=()=>(c||(c=new g),c),$=()=>{u().initialize()},z=async()=>{c&&(await c.shutdown(),c=null)};export{u as get_write_forwarder,$ as initialize_write_forwarder,z as shutdown_write_forwarder};