Skip to content

Commit a105b2e

Browse files
committed
fix: memory leak when open a app on the browser too long
1 parent ccce48c commit a105b2e

File tree

8 files changed

+49
-4
lines changed

8 files changed

+49
-4
lines changed

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## v0.1.2
4+
5+
- Fix memory leak: `sync_log` table growing unbounded — add periodic pruning of entries older than 30 days
6+
- Fix memory leak: WebSocket client `connect()` not clearing pending reconnect timer, causing duplicate connections
7+
- Fix memory leak: `SyncEngine.stop()` not removing event listeners, clearing `wsClients`, or clearing `ignoreMatcherCache`
8+
- Fix memory leak: debounce timers not cleaned up when individual repo/service watchers are stopped
9+
310
## v0.1.1
411

512
- Fix excessive git commits in data repo on every server startup (only `lastSeen` timestamp change in `machines.json`)

landing/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
AI Sync
113113
<span
114114
class="rounded-full bg-muted px-1.5 py-0.5 text-xs h-fit font-medium text-muted-foreground"
115-
>v0.1.1</span
115+
>v0.1.2</span
116116
>
117117
</a>
118118
<div class="flex items-center gap-3">

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ai-sync",
33
"author": "Anh-Thi Dinh",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@ai-sync/server",
33
"author": "Anh-Thi Dinh",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",

packages/server/src/services/file-watcher.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ export class FileWatcherService extends EventEmitter {
5656
return true;
5757
}
5858

59+
private clearDebounceTimersForPrefix(prefix: string): void {
60+
for (const [key, timer] of this.debounceTimers) {
61+
if (key.startsWith(prefix)) {
62+
clearTimeout(timer);
63+
this.debounceTimers.delete(key);
64+
}
65+
}
66+
}
67+
5968
private debounce(key: string, fn: () => void): void {
6069
const existing = this.debounceTimers.get(key);
6170
if (existing) clearTimeout(existing);
@@ -125,6 +134,7 @@ export class FileWatcherService extends EventEmitter {
125134
if (watcher) {
126135
await watcher.close();
127136
this.targetWatchers.delete(repoId);
137+
this.clearDebounceTimersForPrefix(`target:${repoId}:`);
128138
}
129139
}
130140

@@ -185,6 +195,7 @@ export class FileWatcherService extends EventEmitter {
185195
if (watcher) {
186196
await watcher.close();
187197
this.serviceTargetWatchers.delete(serviceId);
198+
this.clearDebounceTimersForPrefix(`serviceTarget:${serviceId}:`);
188199
}
189200
}
190201

packages/server/src/services/sync-engine.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class SyncEngine {
8787
private pollingTimer: NodeJS.Timeout | null = null;
8888
private wsClients: Set<{ send: (data: string) => void }> = new Set();
8989
private ignoreMatcherCache = new Map<string, picomatch.Matcher>();
90+
private lastLogCleanup = 0;
9091

9192
constructor(db: Database.Database) {
9293
this.db = db;
@@ -191,6 +192,7 @@ export class SyncEngine {
191192
await this.scanAllServicesForNewFiles();
192193
await this.syncAllRepos();
193194
await this.syncAllServices();
195+
this.pruneOldSyncLogs();
194196
} catch (err) {
195197
console.error('Polling error:', err);
196198
}
@@ -200,12 +202,27 @@ export class SyncEngine {
200202
}, interval);
201203
}
202204

205+
private pruneOldSyncLogs(): void {
206+
const now = Date.now();
207+
// Run cleanup at most once per hour
208+
if (now - this.lastLogCleanup < 3_600_000) return;
209+
this.lastLogCleanup = now;
210+
try {
211+
this.db.prepare("DELETE FROM sync_log WHERE created_at < datetime('now', '-30 days')").run();
212+
} catch (err) {
213+
console.error('Failed to prune sync_log:', err);
214+
}
215+
}
216+
203217
async stop(): Promise<void> {
204218
if (this.pollingTimer) {
205219
clearTimeout(this.pollingTimer);
206220
this.pollingTimer = null;
207221
}
222+
this.watcher.removeAllListeners();
208223
await this.watcher.stopAll();
224+
this.wsClients.clear();
225+
this.ignoreMatcherCache.clear();
209226
console.log('Sync engine stopped');
210227
}
211228

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@ai-sync/ui",
33
"author": "Anh-Thi Dinh",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",

packages/ui/src/lib/ws.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ class WebSocketClient {
66
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
77

88
connect(): void {
9+
if (this.reconnectTimer) {
10+
clearTimeout(this.reconnectTimer);
11+
this.reconnectTimer = null;
12+
}
13+
if (this.ws) {
14+
this.ws.onclose = null;
15+
this.ws.onerror = null;
16+
this.ws.onmessage = null;
17+
this.ws.close();
18+
}
919
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1020
const wsUrl = `${protocol}//${window.location.host}/ws`;
1121
this.ws = new WebSocket(wsUrl);

0 commit comments

Comments
 (0)