@joystick.js/db-canary 0.0.0-canary.2275 → 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.
@@ -0,0 +1,461 @@
1
+ /**
2
+ * @fileoverview Sync receiver for secondary nodes in JoystickDB.
3
+ * Receives and processes sync messages from primary nodes with API_KEY authentication.
4
+ * Secondary nodes are read-only except for authenticated sync operations.
5
+ */
6
+
7
+ import net from 'net';
8
+ import fs from 'fs/promises';
9
+ import { get_settings } from './load_settings.js';
10
+ import { create_message_parser, encode_message } from './tcp_protocol.js';
11
+ import create_logger from './logger.js';
12
+ import insert_one from './operations/insert_one.js';
13
+ import update_one from './operations/update_one.js';
14
+ import delete_one from './operations/delete_one.js';
15
+ import delete_many from './operations/delete_many.js';
16
+ import bulk_write from './operations/bulk_write.js';
17
+ import create_index_operation from './operations/create_index.js';
18
+ import drop_index_operation from './operations/drop_index.js';
19
+
20
+ const { create_context_logger } = create_logger('sync_receiver');
21
+
22
+ /**
23
+ * Sync receiver that processes authenticated sync messages from primary nodes.
24
+ * Validates API_KEY and applies operations to local database.
25
+ */
26
+ class SyncReceiver {
27
+ /**
28
+ * Creates a new SyncReceiver instance.
29
+ */
30
+ constructor() {
31
+ /** @type {boolean} Whether this node is configured as secondary */
32
+ this.is_secondary = false;
33
+
34
+ /** @type {string|null} API_KEY loaded from file for authentication */
35
+ this.api_key = null;
36
+
37
+ /** @type {string|null} Path to API_KEY file */
38
+ this.api_key_file_path = null;
39
+
40
+ /** @type {net.Server|null} TCP server for receiving sync messages */
41
+ this.server = null;
42
+
43
+ /** @type {number} TCP port for sync operations */
44
+ this.sync_port = 1985;
45
+
46
+ /** @type {Object} Logger instance */
47
+ this.log = create_context_logger();
48
+
49
+ /** @type {Object} Sync statistics */
50
+ this.stats = {
51
+ total_received: 0,
52
+ successful_syncs: 0,
53
+ failed_syncs: 0,
54
+ auth_failures: 0,
55
+ operations_applied: 0
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Initializes the sync receiver with settings configuration.
61
+ */
62
+ async initialize() {
63
+ try {
64
+ const settings = get_settings();
65
+
66
+ if (settings.primary === true) {
67
+ this.log.info('Node configured as primary - sync receiver disabled');
68
+ return;
69
+ }
70
+
71
+ if (settings.primary === false) {
72
+ this.is_secondary = true;
73
+ this.api_key_file_path = settings.secondary_sync_key;
74
+ this.sync_port = settings.sync_port || 1985;
75
+
76
+ if (!this.api_key_file_path) {
77
+ this.log.error('Secondary node missing secondary_sync_key configuration');
78
+ return;
79
+ }
80
+
81
+ await this.load_api_key();
82
+
83
+ if (!this.api_key) {
84
+ this.log.error('Failed to load API_KEY - sync receiver disabled');
85
+ return;
86
+ }
87
+
88
+ this.log.info('Initializing sync receiver for secondary node', {
89
+ api_key_file: this.api_key_file_path,
90
+ sync_port: this.sync_port
91
+ });
92
+
93
+ this.start_server();
94
+ }
95
+
96
+ } catch (error) {
97
+ this.log.warn('Could not initialize sync receiver - settings not loaded', {
98
+ error: error.message
99
+ });
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Loads API_KEY from configured file path.
105
+ */
106
+ async load_api_key() {
107
+ try {
108
+ const key_content = await fs.readFile(this.api_key_file_path, 'utf8');
109
+ this.api_key = key_content.trim();
110
+
111
+ this.log.info('API_KEY loaded successfully', {
112
+ file_path: this.api_key_file_path,
113
+ key_length: this.api_key.length
114
+ });
115
+
116
+ } catch (error) {
117
+ this.log.error('Failed to load API_KEY from file', {
118
+ file_path: this.api_key_file_path,
119
+ error: error.message
120
+ });
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Starts the TCP server to receive sync messages.
126
+ */
127
+ start_server() {
128
+ try {
129
+ this.server = net.createServer((socket) => {
130
+ this.log.debug('Sync connection established', {
131
+ remote_address: socket.remoteAddress,
132
+ remote_port: socket.remotePort
133
+ });
134
+
135
+ const message_parser = create_message_parser();
136
+
137
+ socket.on('data', (data) => {
138
+ try {
139
+ const messages = message_parser.parse_messages(data);
140
+ for (const message of messages) {
141
+ this.handle_sync_message(socket, message).catch((error) => {
142
+ this.log.error('Failed to handle sync message', {
143
+ error: error.message,
144
+ remote_address: socket.remoteAddress
145
+ });
146
+ });
147
+ }
148
+ } catch (error) {
149
+ this.log.error('Failed to parse sync message', {
150
+ error: error.message,
151
+ remote_address: socket.remoteAddress
152
+ });
153
+ }
154
+ });
155
+
156
+ socket.on('error', (error) => {
157
+ this.log.error('Sync connection error', {
158
+ error: error.message,
159
+ remote_address: socket.remoteAddress
160
+ });
161
+ });
162
+
163
+ socket.on('close', () => {
164
+ this.log.debug('Sync connection closed', {
165
+ remote_address: socket.remoteAddress
166
+ });
167
+ });
168
+ });
169
+
170
+ this.server.listen(this.sync_port, () => {
171
+ this.log.info('Sync receiver server started', {
172
+ port: this.sync_port
173
+ });
174
+ });
175
+
176
+ this.server.on('error', (error) => {
177
+ this.log.error('Sync receiver server error', {
178
+ error: error.message,
179
+ port: this.sync_port
180
+ });
181
+ });
182
+ } catch (error) {
183
+ this.log.error('Failed to start sync receiver server', {
184
+ error: error.message,
185
+ port: this.sync_port
186
+ });
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Handles incoming sync message from primary node.
192
+ * @param {net.Socket} socket - Connection socket
193
+ * @param {Object|string} raw_message - Raw sync message
194
+ */
195
+ async handle_sync_message(socket, raw_message) {
196
+ this.stats.total_received++;
197
+
198
+ let message;
199
+ try {
200
+ message = typeof raw_message === 'string' ? JSON.parse(raw_message) : raw_message;
201
+ } catch (error) {
202
+ this.send_sync_response(socket, null, 'error', 'Invalid JSON message');
203
+ return;
204
+ }
205
+
206
+ const { type, api_key, sequence, operation, collection, data } = message;
207
+
208
+ if (type !== 'operation_sync') {
209
+ this.send_sync_response(socket, sequence, 'error', 'Invalid message type');
210
+ return;
211
+ }
212
+
213
+ // Validate API_KEY
214
+ if (!this.validate_api_key(api_key)) {
215
+ this.stats.auth_failures++;
216
+ this.log.error('Sync authentication failed', {
217
+ sequence,
218
+ operation,
219
+ remote_address: socket.remoteAddress
220
+ });
221
+ this.send_sync_response(socket, sequence, 'auth_failed', 'Invalid API_KEY');
222
+ return;
223
+ }
224
+
225
+ // Apply operation to local database
226
+ try {
227
+ await this.apply_sync_operation(operation, collection, data);
228
+ this.stats.successful_syncs++;
229
+ this.stats.operations_applied++;
230
+
231
+ this.log.debug('Sync operation applied successfully', {
232
+ sequence,
233
+ operation,
234
+ collection
235
+ });
236
+
237
+ this.send_sync_response(socket, sequence, 'success', null);
238
+
239
+ } catch (error) {
240
+ this.stats.failed_syncs++;
241
+
242
+ this.log.error('Failed to apply sync operation', {
243
+ sequence,
244
+ operation,
245
+ collection,
246
+ error: error.message
247
+ });
248
+
249
+ this.send_sync_response(socket, sequence, 'error', error.message);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Validates API_KEY from sync message.
255
+ * @param {string} provided_key - API_KEY from sync message
256
+ * @returns {boolean} True if key is valid
257
+ */
258
+ validate_api_key(provided_key) {
259
+ if (!provided_key || !this.api_key) {
260
+ return false;
261
+ }
262
+
263
+ return provided_key === this.api_key;
264
+ }
265
+
266
+ /**
267
+ * Applies sync operation to local database.
268
+ * @param {string} operation - Operation type
269
+ * @param {string} collection - Collection name
270
+ * @param {Object} data - Operation data
271
+ */
272
+ async apply_sync_operation(operation, collection, data) {
273
+ const database_name = data.database || 'default';
274
+
275
+ switch (operation) {
276
+ case 'insert_one':
277
+ return await insert_one(database_name, collection, data.document, data.options);
278
+
279
+ case 'update_one':
280
+ return await update_one(database_name, collection, data.filter, data.update, data.options);
281
+
282
+ case 'delete_one':
283
+ return await delete_one(database_name, collection, data.filter, data.options);
284
+
285
+ case 'delete_many':
286
+ return await delete_many(database_name, collection, data.filter, data.options);
287
+
288
+ case 'bulk_write':
289
+ return await bulk_write(database_name, collection, data.operations, data.options);
290
+
291
+ case 'create_index':
292
+ return await create_index_operation(database_name, collection, data.field, data.options);
293
+
294
+ case 'drop_index':
295
+ return await drop_index_operation(database_name, collection, data.field);
296
+
297
+ default:
298
+ throw new Error(`Unsupported sync operation: ${operation}`);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Sends sync response back to primary node.
304
+ * @param {net.Socket} socket - Connection socket
305
+ * @param {number|null} sequence - Sequence number
306
+ * @param {string} status - Response status
307
+ * @param {string|null} error - Error message if any
308
+ */
309
+ send_sync_response(socket, sequence, status, error) {
310
+ const response = {
311
+ type: 'sync_acknowledged',
312
+ sequence,
313
+ status,
314
+ timestamp: Date.now()
315
+ };
316
+
317
+ if (error) {
318
+ response.error = error;
319
+ }
320
+
321
+ try {
322
+ const encoded_response = encode_message(response);
323
+ socket.write(encoded_response);
324
+ } catch (write_error) {
325
+ this.log.error('Failed to send sync response', {
326
+ sequence,
327
+ status,
328
+ error: write_error.message
329
+ });
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Checks if a client operation should be blocked on secondary.
335
+ * @param {string} operation_type - Type of operation
336
+ * @returns {boolean} True if operation should be blocked
337
+ */
338
+ should_block_client_operation(operation_type) {
339
+ if (!this.is_secondary) {
340
+ return false;
341
+ }
342
+
343
+ // Allow read operations on secondary
344
+ const read_operations = ['find', 'find_one', 'count_documents', 'get_indexes'];
345
+
346
+ if (read_operations.includes(operation_type)) {
347
+ return false;
348
+ }
349
+
350
+ // Block all write operations for direct clients
351
+ return true;
352
+ }
353
+
354
+ /**
355
+ * Gets current sync receiver status and statistics.
356
+ * @returns {Object} Sync receiver status
357
+ */
358
+ get_sync_status() {
359
+ return {
360
+ is_secondary: this.is_secondary,
361
+ api_key_loaded: !!this.api_key,
362
+ api_key_file: this.api_key_file_path,
363
+ server_running: !!this.server && this.server.listening,
364
+ sync_port: this.sync_port,
365
+ stats: this.stats
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Reloads API_KEY from file (for key rotation).
371
+ */
372
+ async reload_api_key() {
373
+ if (!this.api_key_file_path) {
374
+ throw new Error('No API_KEY file path configured');
375
+ }
376
+
377
+ const old_key_length = this.api_key ? this.api_key.length : 0;
378
+ await this.load_api_key();
379
+
380
+ this.log.info('API_KEY reloaded', {
381
+ old_key_length,
382
+ new_key_length: this.api_key ? this.api_key.length : 0
383
+ });
384
+ }
385
+
386
+ /**
387
+ * Promotes secondary to primary (manual failover support).
388
+ */
389
+ promote_to_primary() {
390
+ if (!this.is_secondary) {
391
+ throw new Error('Node is not configured as secondary');
392
+ }
393
+
394
+ this.log.info('Promoting secondary to primary');
395
+
396
+ // Stop sync receiver server
397
+ if (this.server) {
398
+ this.server.close(() => {
399
+ this.log.info('Sync receiver server stopped for primary promotion');
400
+ });
401
+ this.server = null;
402
+ }
403
+
404
+ this.is_secondary = false;
405
+
406
+ this.log.info('Node promoted to primary - sync receiver disabled');
407
+ }
408
+
409
+ /**
410
+ * Shuts down the sync receiver and closes server.
411
+ * @returns {Promise<void>}
412
+ */
413
+ async shutdown() {
414
+ this.log.info('Shutting down sync receiver');
415
+
416
+ if (this.server) {
417
+ return new Promise((resolve) => {
418
+ this.server.close(() => {
419
+ this.log.info('Sync receiver server closed');
420
+ resolve();
421
+ });
422
+ });
423
+ }
424
+
425
+ this.log.info('Sync receiver shutdown complete');
426
+ }
427
+ }
428
+
429
+ /** @type {SyncReceiver|null} Singleton instance */
430
+ let sync_receiver_instance = null;
431
+
432
+ /**
433
+ * Gets the sync receiver singleton instance.
434
+ * @returns {SyncReceiver} Sync receiver instance
435
+ */
436
+ export const get_sync_receiver = () => {
437
+ if (!sync_receiver_instance) {
438
+ sync_receiver_instance = new SyncReceiver();
439
+ }
440
+ return sync_receiver_instance;
441
+ };
442
+
443
+ /**
444
+ * Initializes the sync receiver singleton.
445
+ * @returns {Promise<void>}
446
+ */
447
+ export const initialize_sync_receiver = async () => {
448
+ const receiver = get_sync_receiver();
449
+ await receiver.initialize();
450
+ };
451
+
452
+ /**
453
+ * Shuts down the sync receiver singleton.
454
+ * @returns {Promise<void>}
455
+ */
456
+ export const shutdown_sync_receiver = async () => {
457
+ if (sync_receiver_instance) {
458
+ await sync_receiver_instance.shutdown();
459
+ sync_receiver_instance = null;
460
+ }
461
+ };
@@ -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};