-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.ts
More file actions
752 lines (662 loc) · 23.4 KB
/
main.ts
File metadata and controls
752 lines (662 loc) · 23.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
/**
* Main plugin file - Obsidian integration
*/
import {
Plugin,
PluginSettingTab,
Setting,
Vault,
TFile,
TFolder,
Notice,
MarkdownView,
WorkspaceLeaf,
} from 'obsidian';
import { HyperswarmSyncSettingTab } from './src/settings-tab';
import {
HyperswarmSyncSettings,
DEFAULT_SETTINGS,
} from './src/settings';
import { FileManager } from './src/file-manager';
import { SyncEngine } from './src/sync-engine';
import { SyncStatus, MessageType } from './src/types';
import { ConflictManager } from './src/conflict-manager';
// Set up module resolution for plugin's node_modules
// This must be done before importing HyperswarmManager
(function setupModuleResolution() {
try {
const path = require('path');
const fs = require('fs');
// Try multiple methods to find plugin directory
let pluginDir: string | null = null;
// Method 1: Try __dirname (available in CommonJS/Electron)
// @ts-ignore
if (typeof __dirname !== 'undefined') {
// @ts-ignore
pluginDir = __dirname;
}
// Method 2: Try to get from require.cache (if main.js is already loaded)
if (!pluginDir) {
const mainModule = require.cache[require.resolve('./main')];
if (mainModule && mainModule.filename) {
pluginDir = path.dirname(mainModule.filename);
}
}
// Method 3: Try to infer from process.argv or other Electron globals
if (!pluginDir && typeof process !== 'undefined' && process.argv) {
for (const arg of process.argv) {
if (arg.includes('hypersync') && arg.includes('plugins')) {
pluginDir = path.dirname(arg);
break;
}
}
}
// Method 4: Try common Obsidian plugin paths
if (!pluginDir) {
const possiblePaths = [
path.join(require('os').homedir(), '.obsidian', 'plugins', 'hypersync'),
path.join(require('os').homedir(), 'Documents', 'Obsidian', '.obsidian', 'plugins', 'hypersync'),
];
for (const possiblePath of possiblePaths) {
if (fs.existsSync(possiblePath)) {
pluginDir = possiblePath;
break;
}
}
}
if (pluginDir) {
const nodeModulesPath = path.join(pluginDir, 'node_modules');
// Check if node_modules exists
if (fs.existsSync(nodeModulesPath)) {
// Add to module.paths for require resolution
if (require.main && require.main.paths) {
// Add to the beginning so it's checked first
if (!require.main.paths.includes(nodeModulesPath)) {
require.main.paths.unshift(nodeModulesPath);
}
}
// Also modify Module._nodeModulePaths for better resolution
const Module = require('module');
const originalResolveFilename = Module._resolveFilename;
Module._resolveFilename = function(request: string, parent: any, isMain: boolean, options: any) {
// Try plugin's node_modules first
if (!request.startsWith('.') && !path.isAbsolute(request)) {
try {
const pluginModulePath = path.join(nodeModulesPath, request);
if (fs.existsSync(pluginModulePath) || fs.existsSync(pluginModulePath + '.js')) {
return originalResolveFilename.call(this, pluginModulePath, parent, isMain, options);
}
} catch (e) {
// Fall through to default resolution
}
}
return originalResolveFilename.call(this, request, parent, isMain, options);
};
console.log('Hyperswarm Sync: Module resolution set up for', nodeModulesPath);
} else {
console.warn('Hyperswarm Sync: node_modules not found at', nodeModulesPath);
}
} else {
console.warn('Hyperswarm Sync: Could not determine plugin directory');
}
} catch (e) {
console.error('Hyperswarm Sync: Error setting up module resolution:', e);
}
})();
// Import HyperswarmManager after setting up module resolution
// We'll pass the plugin instance to HyperswarmManager so it can find node_modules
import { HyperswarmManager } from './src/hyperswarm-manager';
export default class HyperswarmSyncPlugin extends Plugin {
settings: HyperswarmSyncSettings;
public swarmManager: HyperswarmManager | null = null;
private fileManager: FileManager | null = null;
private syncEngine: SyncEngine | null = null;
private statusBarItem: HTMLElement | null = null;
private syncInterval: NodeJS.Timeout | null = null;
private editorChangeTimer: NodeJS.Timeout | null = null;
public conflictManager: ConflictManager | null = null;
async onload() {
console.log('[HyperswarmSyncPlugin] Plugin loading...');
// CRITICAL: Set up process signal handlers for force-close scenarios
// These handlers will attempt cleanup even if onunload() isn't called
if (typeof process !== 'undefined') {
const cleanup = async () => {
console.log('[HyperswarmSyncPlugin] Process signal received, attempting cleanup...');
try {
// Quick cleanup with very short timeout (process is being killed)
await Promise.race([
this.stopSync(),
new Promise(resolve => setTimeout(resolve, 500)) // 500ms max
]);
} catch (error) {
// Ignore - process is being killed
}
};
// Handle SIGTERM (graceful shutdown request)
process.once('SIGTERM', cleanup);
// Handle SIGINT (Ctrl+C)
process.once('SIGINT', cleanup);
// Handle uncaught exceptions (last resort)
process.once('uncaughtException', (error) => {
console.error('[HyperswarmSyncPlugin] Uncaught exception, cleaning up:', error);
cleanup();
});
}
await this.loadSettings();
console.log('[HyperswarmSyncPlugin] Settings loaded');
// Initialize file manager
this.fileManager = new FileManager(this.app.vault);
console.log('[HyperswarmSyncPlugin] File manager initialized');
// Initialize conflict manager
this.conflictManager = new ConflictManager(this.app);
console.log('[HyperswarmSyncPlugin] Conflict manager initialized');
// Generate or load vault ID
if (!this.settings.vaultId) {
console.log('[HyperswarmSyncPlugin] Generating new Vault ID...');
this.settings.vaultId = await this.generateVaultId();
await this.saveSettings();
console.log(`[HyperswarmSyncPlugin] Vault ID generated: ${this.settings.vaultId.substring(0, 16)}...`);
} else {
console.log(`[HyperswarmSyncPlugin] Using existing Vault ID: ${this.settings.vaultId.substring(0, 16)}...`);
}
// Create status bar
if (this.settings.showStatusBar) {
this.createStatusBar();
}
// Set up file watcher for real-time sync
this.registerEvent(
this.app.vault.on('modify', (file) => {
if (this.settings.enabled && this.settings.autoSync && this.syncEngine) {
if (file instanceof TFile && this.fileManager?.shouldSyncFile(file)) {
// Real-time: send file immediately instead of full sync
this.syncEngine.sendFileImmediately(file.path);
}
}
})
);
// REAL-TIME: Watch active editor changes - send IMMEDIATELY on every keystroke
// No debounce - true real-time sync!
this.registerEvent(
this.app.workspace.on('editor-change', (editor) => {
if (this.settings.enabled && this.settings.autoSync && this.syncEngine) {
const file = this.app.workspace.getActiveFile();
if (file && this.fileManager?.shouldSyncFile(file)) {
// REAL-TIME: Send immediately, no debounce!
this.syncEngine?.sendFileImmediately(file.path);
}
}
})
);
this.registerEvent(
this.app.vault.on('create', (file) => {
if (this.settings.enabled && this.settings.autoSync && this.syncEngine) {
if (file instanceof TFile && this.fileManager?.shouldSyncFile(file)) {
// Real-time: send file immediately instead of full sync
this.syncEngine.sendFileImmediately(file.path);
} else if (file instanceof TFolder) {
// Real-time: broadcast updated file list to include new folder
this.syncEngine.broadcastFileList();
}
}
})
);
this.registerEvent(
this.app.vault.on('delete', (file) => {
if (this.settings.enabled && this.settings.autoSync && this.syncEngine && this.swarmManager) {
if (file instanceof TFile && this.fileManager?.shouldSyncFile(file)) {
// Real-time: immediately broadcast file deletion
console.log(`[HyperswarmSyncPlugin] File deleted: ${file.path}, broadcasting deletion`);
this.swarmManager.broadcast(MessageType.FILE_DELETE, {
path: file.path,
isFolder: false,
});
} else if (file instanceof TFolder) {
// Real-time: immediately broadcast folder deletion
console.log(`[HyperswarmSyncPlugin] Folder deleted: ${file.path}, broadcasting deletion`);
this.swarmManager.broadcast(MessageType.FILE_DELETE, {
path: file.path,
isFolder: true,
});
// Also broadcast updated file list to reflect folder removal
this.syncEngine?.broadcastFileList();
}
}
})
);
// Handle folder rename (rename triggers delete + create)
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => {
if (this.settings.enabled && this.settings.autoSync && this.syncEngine && this.swarmManager) {
if (file instanceof TFolder) {
// Broadcast deletion of old path and updated file list
console.log(`[HyperswarmSyncPlugin] Folder renamed: ${oldPath} -> ${file.path}`);
this.swarmManager.broadcast(MessageType.FILE_DELETE, {
path: oldPath,
isFolder: true,
});
// Broadcast updated file list with new folder
this.syncEngine.broadcastFileList();
}
}
})
);
// Add settings tab
this.addSettingTab(new HyperswarmSyncSettingTab(this.app, this));
// Set up network change detection (ISSUE #6: No Network Change Detection)
this.setupNetworkChangeDetection();
// Start sync if enabled (deferred to not block Obsidian startup)
if (this.settings.enabled) {
console.log('[HyperswarmSyncPlugin] Sync enabled, starting in background...');
// Start in background but ensure it completes initialization
setTimeout(() => {
this.startSync().catch(err => {
console.error('[HyperswarmSyncPlugin] Background sync start failed:', err);
});
}, 100);
} else {
console.log('[HyperswarmSyncPlugin] Sync disabled');
}
// Periodic sync (optional fallback - disabled for real-time sync)
// Real-time sync via file watchers is more efficient
// Uncomment below if you want periodic sync as a fallback
// if (this.settings.autoSync) {
// console.log(`[HyperswarmSyncPlugin] Auto-sync enabled (interval: ${this.settings.syncInterval}ms)`);
// this.startPeriodicSync();
// }
console.log('[HyperswarmSyncPlugin] Plugin loaded successfully');
new Notice('Hyperswarm Sync loaded');
}
async onunload() {
console.log('[HyperswarmSyncPlugin] Unloading plugin...');
// CRITICAL: Use timeout to ensure cleanup completes even if Obsidian closes abruptly
// Don't wait more than 2 seconds for cleanup
try {
await Promise.race([
this.stopSync(),
new Promise(resolve => setTimeout(resolve, 2000)) // 2 second timeout
]);
console.log('[HyperswarmSyncPlugin] Cleanup completed');
} catch (error) {
console.error('[HyperswarmSyncPlugin] Cleanup error (continuing anyway):', error);
}
// Synchronous cleanup (always runs)
if (this.syncInterval) {
clearInterval(this.syncInterval);
this.syncInterval = null;
}
if (this.statusBarItem) {
this.statusBarItem.remove();
this.statusBarItem = null;
}
// Clean up network change handlers (ISSUE #6)
if (typeof window !== 'undefined' && (this as any)._networkHandlers) {
const handlers = (this as any)._networkHandlers;
window.removeEventListener('online', handlers.handleOnline);
window.removeEventListener('offline', handlers.handleOffline);
delete (this as any)._networkHandlers;
}
// Force cleanup if async didn't complete
if (this.swarmManager) {
console.warn('[HyperswarmSyncPlugin] Force cleaning swarm manager...');
try {
// Force destroy without waiting
this.swarmManager.removeAllListeners();
// Call stop but don't wait
this.swarmManager.stop().catch(() => {});
} catch (error) {
// Ignore - we're shutting down
}
this.swarmManager = null;
}
if (this.syncEngine) {
this.syncEngine = null;
}
console.log('[HyperswarmSyncPlugin] Plugin unloaded');
}
/**
* Load settings
*/
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
/**
* Save settings
*/
async saveSettings() {
await this.saveData(this.settings);
}
/**
* Generate unique vault ID
*/
private async generateVaultId(): Promise<string> {
// Get vault path - basePath exists at runtime but not in type definitions
const vaultPath = (this.app.vault.adapter as any).basePath || this.app.vault.getName();
const machineId = this.getMachineId();
const combined = `${vaultPath}-${machineId}-${Date.now()}`;
// Use Web Crypto API
const encoder = new TextEncoder();
const data = encoder.encode(combined);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
/**
* Generate a new vault ID (public method for settings)
*/
async generateNewVaultId(): Promise<void> {
this.settings.vaultId = await this.generateVaultId();
await this.saveSettings();
// Restart sync with new vault ID
await this.stopSync();
if (this.settings.enabled) {
await this.startSync();
}
new Notice('New Vault ID generated. Share this ID with other devices to sync.');
}
/**
* Get machine ID (simple implementation)
*/
private getMachineId(): string {
// In a real implementation, you might want to use a more persistent ID
// For now, we'll use a combination of available info
return `${navigator.platform}-${navigator.userAgent}`;
}
/**
* Generate or load peer seed for persistent peer ID (CRITICAL ISSUE #2)
*/
private async getOrGeneratePeerSeed(): Promise<Buffer> {
if (this.settings.peerSeed) {
// Load existing seed from settings
try {
const seedBytes = Buffer.from(this.settings.peerSeed, 'base64');
if (seedBytes.length === 32) {
console.log('[HyperswarmSyncPlugin] Using existing peer seed');
return seedBytes;
}
} catch (error) {
console.warn('[HyperswarmSyncPlugin] Invalid peer seed, generating new one:', error);
}
}
// Generate new seed
console.log('[HyperswarmSyncPlugin] Generating new peer seed...');
const crypto = require('crypto');
const seed = crypto.randomBytes(32);
this.settings.peerSeed = seed.toString('base64');
await this.saveSettings();
console.log('[HyperswarmSyncPlugin] New peer seed generated and saved');
return seed;
}
/**
* Set up network change detection (ISSUE #6: No Network Change Detection)
*/
private setupNetworkChangeDetection(): void {
// Listen for online/offline events
if (typeof window !== 'undefined') {
const handleOnline = async () => {
console.log('[HyperswarmSyncPlugin] Network came online, refreshing discovery...');
if (this.swarmManager && this.settings.enabled) {
try {
await this.swarmManager.restartDiscovery();
new Notice('Network reconnected, refreshing sync...');
} catch (error) {
console.error('[HyperswarmSyncPlugin] Failed to refresh discovery on network change:', error);
}
}
};
const handleOffline = () => {
console.log('[HyperswarmSyncPlugin] Network went offline');
new Notice('Network disconnected');
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// Store handlers for cleanup
(this as any)._networkHandlers = { handleOnline, handleOffline };
}
// Also listen for Electron-specific network events if available
if (typeof process !== 'undefined' && (process as any).type === 'renderer') {
// Electron renderer process - network status might be available via IPC
// This is a fallback if window events don't work
console.log('[HyperswarmSyncPlugin] Network change detection set up (Electron renderer)');
}
}
/**
* Start sync
*/
async startSync(): Promise<void> {
console.log('[HyperswarmSyncPlugin] Starting sync...');
// Check if actually running, not just if object exists
if (this.swarmManager && this.swarmManager.connected()) {
console.log('[HyperswarmSyncPlugin] Sync already started');
return; // Already started
}
// Clean up any stale managers before starting
if (this.swarmManager) {
await this.stopSync();
}
try {
// Get plugin directory for module resolution
// Try multiple methods to get the plugin path
let pluginPath: string | undefined;
console.log('[HyperswarmSyncPlugin] Determining plugin path...');
// Method 1: Try Obsidian's plugin manifest
try {
// @ts-ignore - accessing internal plugin path
const manifestDir = (this as any).app?.plugins?.plugins?.['hypersync']?.manifest?.dir;
if (manifestDir) {
// Convert to absolute path if relative
const path = require('path');
// @ts-ignore - basePath exists at runtime
const vaultBasePath = (this.app.vault.adapter as any).basePath;
pluginPath = path.isAbsolute(manifestDir)
? manifestDir
: path.resolve(vaultBasePath || process.cwd(), manifestDir);
}
} catch (e) {
// Ignore
}
// Method 2: Try to find from require.cache (limit iteration for speed)
if (!pluginPath) {
try {
const path = require('path');
const cache = require.cache;
const keys = Object.keys(cache);
// Limit search to first 50 entries for speed
for (let i = 0; i < Math.min(50, keys.length); i++) {
const key = cache[keys[i]]?.filename;
if (key && key.includes('hypersync') && key.endsWith('main.js')) {
if (!key.includes('electron.asar')) {
pluginPath = path.dirname(key);
if (!path.isAbsolute(pluginPath)) {
pluginPath = path.resolve(process.cwd(), pluginPath);
}
break;
}
}
}
} catch (e) {
// Ignore
}
}
// Method 3: Try __dirname if available
if (!pluginPath) {
// @ts-ignore
if (typeof __dirname !== 'undefined') {
// @ts-ignore
pluginPath = __dirname;
}
}
// Method 4: Try to construct from vault path
if (!pluginPath) {
try {
const path = require('path');
const vaultPath = (this.app.vault.adapter as any).basePath;
if (vaultPath) {
pluginPath = path.join(vaultPath, '.obsidian', 'plugins', 'hypersync');
}
} catch (e) {
// Ignore
}
}
console.log('[HyperswarmSyncPlugin] Plugin path:', pluginPath);
if (pluginPath) {
const path = require('path');
console.log('[HyperswarmSyncPlugin] Plugin path (absolute):', path.isAbsolute(pluginPath) ? pluginPath : path.resolve(process.cwd(), pluginPath));
}
// Get or generate peer seed for persistent peer ID (CRITICAL ISSUE #2)
const peerSeed = await this.getOrGeneratePeerSeed();
// Initialize swarm manager
console.log('[HyperswarmSyncPlugin] Initializing HyperswarmManager...');
this.swarmManager = new HyperswarmManager(this.settings.vaultId, peerSeed, pluginPath);
// Set up event handlers
this.swarmManager.on('peer-connected', (peerInfo) => {
console.log(`[HyperswarmSyncPlugin] Peer connected event: ${peerInfo.publicKey.substring(0, 16)}...`);
new Notice(`Peer connected: ${peerInfo.publicKey.substring(0, 8)}...`);
this.updateStatusBar();
});
this.swarmManager.on('peer-disconnected', (peerId) => {
console.log(`[HyperswarmSyncPlugin] Peer disconnected event: ${peerId.substring(0, 16)}...`);
new Notice(`Peer disconnected: ${peerId.substring(0, 8)}...`);
this.updateStatusBar();
});
// Initialize sync engine
if (this.fileManager && this.conflictManager) {
console.log('[HyperswarmSyncPlugin] Initializing SyncEngine...');
this.syncEngine = new SyncEngine(
this.swarmManager,
this.fileManager,
this.app,
this.conflictManager
);
// Set up sync engine events
this.syncEngine.on('status-changed', (status) => {
console.log(`[HyperswarmSyncPlugin] Status changed: ${status.peers} peers, ${status.pendingFiles} pending, syncing: ${status.syncing}`);
this.updateStatusBar();
});
this.syncEngine.on('file-synced', (filePath) => {
console.log(`[HyperswarmSyncPlugin] File synced: ${filePath}`);
});
this.syncEngine.on('conflict', (filePath, localHash, remoteHash) => {
console.warn(`[HyperswarmSyncPlugin] Conflict detected: ${filePath} (local: ${localHash.substring(0, 8)}..., remote: ${remoteHash.substring(0, 8)}...)`);
new Notice(`⚠️ Conflict detected in ${filePath}. Choose which version to keep.`);
});
this.syncEngine.on('error', (error) => {
console.error('[HyperswarmSyncPlugin] Sync error:', error);
new Notice(`Sync error: ${error.message}`);
});
// Start sync engine
const syncEngineStart = Date.now();
console.log(`[HyperswarmSyncPlugin] Starting sync engine... (T+${Date.now() - syncEngineStart}ms)`);
await this.syncEngine.start();
console.log(`[HyperswarmSyncPlugin] Sync started successfully (T+${Date.now() - syncEngineStart}ms)`);
}
} catch (error) {
console.error('[HyperswarmSyncPlugin] Failed to start sync:', error);
new Notice(`Failed to start sync: ${error.message}`);
}
}
/**
* Stop sync
*/
async stopSync(): Promise<void> {
console.log('[HyperswarmSyncPlugin] Stopping sync...');
// Stop sync engine with timeout
if (this.syncEngine) {
try {
await Promise.race([
this.syncEngine.stop(),
new Promise(resolve => setTimeout(resolve, 1000)) // 1s timeout
]);
} catch (error) {
console.warn('[HyperswarmSyncPlugin] Sync engine stop error:', error);
}
this.syncEngine = null;
}
// Stop swarm manager with timeout (most critical for cleanup)
if (this.swarmManager) {
// Remove event listeners to prevent leaks
this.swarmManager.removeAllListeners();
try {
await Promise.race([
this.swarmManager.stop(),
new Promise(resolve => setTimeout(resolve, 1500)) // 1.5s timeout
]);
} catch (error) {
console.warn('[HyperswarmSyncPlugin] Swarm manager stop error:', error);
}
this.swarmManager = null;
}
this.updateStatusBar();
console.log('[HyperswarmSyncPlugin] Sync stopped');
}
/**
* Trigger sync
*/
async triggerSync(): Promise<void> {
console.log('[HyperswarmSyncPlugin] Manual sync triggered');
if (this.syncEngine) {
this.syncEngine.triggerSync();
} else {
console.warn('[HyperswarmSyncPlugin] Cannot trigger sync - sync engine not initialized');
}
}
/**
* Get sync status
*/
getSyncStatus(): SyncStatus {
if (this.syncEngine) {
return this.syncEngine.getStatus();
}
return {
connected: false,
peers: 0,
syncing: false,
lastSync: null,
pendingFiles: 0,
};
}
/**
* Create status bar item
*/
private createStatusBar(): void {
this.statusBarItem = this.addStatusBarItem();
this.updateStatusBar();
}
/**
* Update status bar
*/
updateStatusBar(): void {
if (!this.statusBarItem || !this.settings.showStatusBar) {
return;
}
const status = this.getSyncStatus();
let text = '🔄 Sync: ';
if (!status.connected) {
text += 'Disconnected';
} else if (status.syncing) {
text += `Syncing (${status.peers} peers)`;
} else {
text += `Connected (${status.peers} peers)`;
}
if (status.pendingFiles > 0) {
text += ` [${status.pendingFiles} pending]`;
}
this.statusBarItem.setText(text);
}
/**
* Start periodic sync
*/
private startPeriodicSync(): void {
if (this.syncInterval) {
clearInterval(this.syncInterval);
}
this.syncInterval = setInterval(() => {
if (this.settings.enabled && this.settings.autoSync) {
this.triggerSync();
}
}, this.settings.syncInterval);
}
}