@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,444 @@
1
+ /**
2
+ * @fileoverview Simple sync manager for primary nodes in JoystickDB.
3
+ * Provides basic TCP synchronization of write operations to secondary nodes
4
+ * with API_KEY authentication and simple retry logic.
5
+ */
6
+
7
+ import net from 'net';
8
+ import { get_settings } from './load_settings.js';
9
+ import { encode_message } from './tcp_protocol.js';
10
+ import create_logger from './logger.js';
11
+
12
+ const { create_context_logger } = create_logger('simple_sync');
13
+
14
+ /**
15
+ * Simple sync manager that syncs write operations to secondary nodes.
16
+ * Uses API_KEY authentication and basic TCP connections with retry logic.
17
+ */
18
+ class SimpleSyncManager {
19
+ /**
20
+ * Creates a new SimpleSyncManager instance.
21
+ */
22
+ constructor() {
23
+ /** @type {boolean} Whether this node is configured as primary */
24
+ this.is_primary = false;
25
+
26
+ /** @type {Array<Object>} List of secondary nodes to sync to */
27
+ this.secondary_nodes = [];
28
+
29
+ /** @type {Map<string, Object>} Active connections to secondary nodes */
30
+ this.connections = new Map();
31
+
32
+ /** @type {number} TCP port for sync operations */
33
+ this.sync_port = 1985;
34
+
35
+ /** @type {number} Timeout for sync operations in milliseconds */
36
+ this.sync_timeout_ms = 5000;
37
+
38
+ /** @type {number} Number of retry attempts */
39
+ this.sync_retries = 2;
40
+
41
+ /** @type {number} Sequence number for operations */
42
+ this.sequence_number = 0;
43
+
44
+ /** @type {Object} Logger instance */
45
+ this.log = create_context_logger();
46
+
47
+ /** @type {Object} Sync statistics */
48
+ this.stats = {
49
+ total_synced: 0,
50
+ successful_syncs: 0,
51
+ failed_syncs: 0,
52
+ auth_failures: 0
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Initializes the sync manager with settings configuration.
58
+ */
59
+ initialize() {
60
+ try {
61
+ const settings = get_settings();
62
+
63
+ if (!settings.primary) {
64
+ this.log.info('Node not configured as primary - sync disabled');
65
+ return;
66
+ }
67
+
68
+ this.is_primary = settings.primary;
69
+ this.secondary_nodes = settings.secondary_nodes || [];
70
+ this.sync_port = settings.sync_port || 1985;
71
+ this.sync_timeout_ms = settings.sync_timeout_ms || 5000;
72
+ this.sync_retries = settings.sync_retries || 2;
73
+
74
+ this.log.info('Initializing simple sync manager', {
75
+ is_primary: this.is_primary,
76
+ secondary_count: this.secondary_nodes.length,
77
+ sync_port: this.sync_port,
78
+ timeout_ms: this.sync_timeout_ms,
79
+ retries: this.sync_retries
80
+ });
81
+
82
+ this.connect_to_secondaries();
83
+
84
+ } catch (error) {
85
+ this.log.warn('Could not initialize sync manager - settings not loaded', {
86
+ error: error.message
87
+ });
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Establishes connections to all secondary nodes.
93
+ */
94
+ connect_to_secondaries() {
95
+ for (const secondary of this.secondary_nodes) {
96
+ this.connect_to_secondary(secondary);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Connects to a single secondary node.
102
+ * @param {Object} secondary - Secondary node configuration
103
+ */
104
+ connect_to_secondary(secondary) {
105
+ const { ip } = secondary;
106
+ const connection_id = `${ip}:${this.sync_port}`;
107
+
108
+ if (this.connections.has(connection_id)) {
109
+ return;
110
+ }
111
+
112
+ this.log.info('Connecting to secondary node', { ip, port: this.sync_port });
113
+
114
+ try {
115
+ const socket = new net.Socket();
116
+
117
+ socket.connect(this.sync_port, ip, () => {
118
+ this.log.info('Connected to secondary node', { ip, port: this.sync_port });
119
+ });
120
+
121
+ socket.on('error', (error) => {
122
+ this.log.error('Secondary connection error', {
123
+ ip,
124
+ error: error.message
125
+ });
126
+
127
+ this.connections.delete(connection_id);
128
+
129
+ // Retry connection after delay
130
+ setTimeout(() => {
131
+ try {
132
+ this.connect_to_secondary(secondary);
133
+ } catch (retry_error) {
134
+ this.log.error('Failed to retry secondary connection', {
135
+ ip,
136
+ error: retry_error.message
137
+ });
138
+ }
139
+ }, 5000);
140
+ });
141
+
142
+ socket.on('close', () => {
143
+ this.log.warn('Secondary connection closed', { ip });
144
+ this.connections.delete(connection_id);
145
+
146
+ // Retry connection after delay
147
+ setTimeout(() => {
148
+ try {
149
+ this.connect_to_secondary(secondary);
150
+ } catch (retry_error) {
151
+ this.log.error('Failed to retry secondary connection', {
152
+ ip,
153
+ error: retry_error.message
154
+ });
155
+ }
156
+ }, 5000);
157
+ });
158
+
159
+ socket.on('data', (data) => {
160
+ try {
161
+ const message = JSON.parse(data.toString());
162
+ this.handle_sync_response(connection_id, message);
163
+ } catch (error) {
164
+ this.log.error('Failed to parse sync response', {
165
+ connection_id,
166
+ error: error.message
167
+ });
168
+ }
169
+ });
170
+
171
+ this.connections.set(connection_id, {
172
+ socket,
173
+ ip,
174
+ connected: true,
175
+ last_sync: null
176
+ });
177
+
178
+ } catch (error) {
179
+ this.log.error('Failed to connect to secondary', {
180
+ ip,
181
+ error: error.message
182
+ });
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Handles sync response from secondary node.
188
+ * @param {string} connection_id - Connection identifier
189
+ * @param {Object} message - Response message from secondary
190
+ */
191
+ handle_sync_response(connection_id, message) {
192
+ const { type, status, sequence, error } = message;
193
+
194
+ if (type !== 'sync_acknowledged') {
195
+ return;
196
+ }
197
+
198
+ if (status === 'success') {
199
+ this.stats.successful_syncs++;
200
+ this.log.debug('Sync acknowledged', { connection_id, sequence });
201
+ } else if (status === 'auth_failed') {
202
+ this.stats.auth_failures++;
203
+ this.log.error('Sync authentication failed', { connection_id, sequence, error });
204
+ } else {
205
+ this.stats.failed_syncs++;
206
+ this.log.error('Sync failed', { connection_id, sequence, error });
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Queues an operation for sync to secondary nodes.
212
+ * @param {string} operation - Operation type
213
+ * @param {string} collection - Collection name
214
+ * @param {Object} data - Operation data
215
+ */
216
+ queue_sync(operation, collection, data) {
217
+ if (!this.is_primary || this.connections.size === 0) {
218
+ return;
219
+ }
220
+
221
+ const sequence = ++this.sequence_number;
222
+
223
+ this.log.debug('Queuing sync operation', {
224
+ operation,
225
+ collection,
226
+ sequence,
227
+ secondary_count: this.connections.size
228
+ });
229
+
230
+ this.stats.total_synced++;
231
+
232
+ // Send immediately to all connected secondaries
233
+ this.send_sync_to_secondaries(operation, collection, data, sequence);
234
+ }
235
+
236
+ /**
237
+ * Sends sync message to all connected secondary nodes.
238
+ * @param {string} operation - Operation type
239
+ * @param {string} collection - Collection name
240
+ * @param {Object} data - Operation data
241
+ * @param {number} sequence - Sequence number
242
+ */
243
+ send_sync_to_secondaries(operation, collection, data, sequence) {
244
+ try {
245
+ const settings = get_settings();
246
+
247
+ if (!settings.api_key) {
248
+ this.log.error('No API_KEY configured for sync operations');
249
+ return;
250
+ }
251
+
252
+ const sync_message = {
253
+ type: 'operation_sync',
254
+ api_key: settings.api_key,
255
+ sequence,
256
+ timestamp: Date.now(),
257
+ operation,
258
+ collection,
259
+ data
260
+ };
261
+
262
+ const encoded_message = encode_message(sync_message);
263
+
264
+ for (const [connection_id, connection] of this.connections) {
265
+ if (!connection.connected || !connection.socket) {
266
+ continue;
267
+ }
268
+
269
+ try {
270
+ connection.socket.write(encoded_message);
271
+ connection.last_sync = Date.now();
272
+
273
+ this.log.debug('Sent sync to secondary', {
274
+ connection_id,
275
+ operation,
276
+ sequence
277
+ });
278
+
279
+ } catch (error) {
280
+ this.log.error('Failed to send sync to secondary', {
281
+ connection_id,
282
+ error: error.message
283
+ });
284
+ }
285
+ }
286
+ } catch (error) {
287
+ this.log.error('Failed to send sync to secondaries', {
288
+ operation,
289
+ sequence,
290
+ error: error.message
291
+ });
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Updates the secondary nodes configuration at runtime.
297
+ * @param {Array<Object>} new_secondary_nodes - New secondary nodes list
298
+ */
299
+ update_secondary_nodes(new_secondary_nodes) {
300
+ this.log.info('Updating secondary nodes configuration', {
301
+ old_count: this.secondary_nodes.length,
302
+ new_count: new_secondary_nodes.length
303
+ });
304
+
305
+ // Close existing connections
306
+ for (const [connection_id, connection] of this.connections) {
307
+ try {
308
+ connection.socket.end();
309
+ } catch (error) {
310
+ this.log.warn('Error closing secondary connection', {
311
+ connection_id,
312
+ error: error.message
313
+ });
314
+ }
315
+ }
316
+
317
+ this.connections.clear();
318
+ this.secondary_nodes = new_secondary_nodes;
319
+
320
+ // Establish new connections
321
+ this.connect_to_secondaries();
322
+ }
323
+
324
+ /**
325
+ * Forces sync of all connected secondaries (health check).
326
+ * @returns {Promise<Object>} Sync status for each secondary
327
+ */
328
+ async force_sync() {
329
+ if (!this.is_primary) {
330
+ throw new Error('Node is not configured as primary');
331
+ }
332
+
333
+ const results = [];
334
+
335
+ for (const [connection_id, connection] of this.connections) {
336
+ try {
337
+ if (connection.connected) {
338
+ results.push({
339
+ connection_id,
340
+ status: 'sync_initiated'
341
+ });
342
+ } else {
343
+ results.push({
344
+ connection_id,
345
+ status: 'not_connected'
346
+ });
347
+ }
348
+ } catch (error) {
349
+ results.push({
350
+ connection_id,
351
+ status: 'error',
352
+ error: error.message
353
+ });
354
+ }
355
+ }
356
+
357
+ return {
358
+ success: true,
359
+ message: 'Force sync initiated',
360
+ results
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Gets current sync status and statistics.
366
+ * @returns {Object} Sync status information
367
+ */
368
+ get_sync_status() {
369
+ const secondary_status = [];
370
+
371
+ for (const [connection_id, connection] of this.connections) {
372
+ secondary_status.push({
373
+ connection_id,
374
+ ip: connection.ip,
375
+ connected: connection.connected,
376
+ last_sync: connection.last_sync
377
+ });
378
+ }
379
+
380
+ return {
381
+ is_primary: this.is_primary,
382
+ secondary_count: this.connections.size,
383
+ stats: this.stats,
384
+ secondaries: secondary_status
385
+ };
386
+ }
387
+
388
+ /**
389
+ * Shuts down the sync manager and closes all connections.
390
+ * @returns {Promise<void>}
391
+ */
392
+ async shutdown() {
393
+ this.log.info('Shutting down simple sync manager');
394
+
395
+ for (const [connection_id, connection] of this.connections) {
396
+ try {
397
+ connection.socket.end();
398
+ } catch (error) {
399
+ this.log.warn('Error closing secondary connection during shutdown', {
400
+ connection_id,
401
+ error: error.message
402
+ });
403
+ }
404
+ }
405
+
406
+ this.connections.clear();
407
+ this.is_primary = false;
408
+
409
+ this.log.info('Simple sync manager shutdown complete');
410
+ }
411
+ }
412
+
413
+ /** @type {SimpleSyncManager|null} Singleton instance */
414
+ let sync_manager_instance = null;
415
+
416
+ /**
417
+ * Gets the sync manager singleton instance.
418
+ * @returns {SimpleSyncManager} Sync manager instance
419
+ */
420
+ export const get_simple_sync_manager = () => {
421
+ if (!sync_manager_instance) {
422
+ sync_manager_instance = new SimpleSyncManager();
423
+ }
424
+ return sync_manager_instance;
425
+ };
426
+
427
+ /**
428
+ * Initializes the sync manager singleton.
429
+ */
430
+ export const initialize_simple_sync_manager = () => {
431
+ const manager = get_simple_sync_manager();
432
+ manager.initialize();
433
+ };
434
+
435
+ /**
436
+ * Shuts down the sync manager singleton.
437
+ * @returns {Promise<void>}
438
+ */
439
+ export const shutdown_simple_sync_manager = async () => {
440
+ if (sync_manager_instance) {
441
+ await sync_manager_instance.shutdown();
442
+ sync_manager_instance = null;
443
+ }
444
+ };