Skip to content

Commit 9b7fdc5

Browse files
authored
fix(worker): reconnect IMAP IDLE after socket close (#444)
Co-authored-by: divrajbajwa <22200467+divrajbajwa@users.noreply.github.com>
1 parent c79279c commit 9b7fdc5

2 files changed

Lines changed: 53 additions & 3 deletions

File tree

.changeset/swift-paws-jog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@kurrier/worker": patch
3+
"@kurrier/repo": patch
4+
---
5+
6+
Reconnect IMAP IDLE after socket close

apps/worker/lib/imap/imap-idle-sync.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import type { ImapFlow } from "imapflow";
55
import type { FlagsEvent } from "imapflow";
66
import { deltaFetch } from "../../lib/imap/imap-delta-fetch";
77

8+
const MAX_RECONNECT_ATTEMPTS = 5;
9+
const RECONNECT_BACKOFFS_MS = [5000, 10000, 20000, 40000, 80000];
10+
const reconnectAttempts = new Map<string, number>();
11+
812
async function handleFlagsUpdate(
913
identityId: string,
1014
uid: number,
@@ -247,27 +251,62 @@ function attachRealtimeEventHandlers(
247251
});
248252
}
249253

250-
async function idleForever(identityId: string, client: ImapFlow) {
254+
async function idleForever(
255+
identityId: string,
256+
client: ImapFlow,
257+
idleImapInstances: Map<string, ImapFlow>,
258+
imapInstances: Map<string, ImapFlow>,
259+
) {
251260
console.log(`[realtime:${identityId}] entering idle loop...`);
252261
while (client.authenticated && client.usable) {
253262
try {
254263
await client.idle();
264+
reconnectAttempts.set(identityId, 0);
255265
} catch (err) {
256266
console.error(`[realtime:${identityId}] idle error`, err);
257267
}
258268
}
259269
console.warn(`[realtime:${identityId}] idle loop ended (client closed)`);
270+
271+
const attempts = reconnectAttempts.get(identityId) ?? 0;
272+
if (attempts >= MAX_RECONNECT_ATTEMPTS) {
273+
console.error(`[realtime:${identityId}] reconnect cap reached, giving up`);
274+
return;
275+
}
276+
277+
const backoffMs = RECONNECT_BACKOFFS_MS[attempts];
278+
reconnectAttempts.set(identityId, attempts + 1);
279+
280+
try {
281+
await client.logout();
282+
} catch {}
283+
idleImapInstances.delete(identityId);
284+
285+
console.log(
286+
`[realtime:${identityId}] reconnecting in ${backoffMs / 1000}s (attempt ${attempts + 1}/${MAX_RECONNECT_ATTEMPTS})`,
287+
);
288+
289+
setTimeout(() => {
290+
startRealtimeForIdentity(
291+
identityId,
292+
idleImapInstances,
293+
imapInstances,
294+
).catch((err) =>
295+
console.error(`[realtime:${identityId}] reconnect failed`, err),
296+
);
297+
}, backoffMs);
260298
}
261299

262300
async function startRealtimeSyncForIdentity(
263301
identityId: string,
264302
client: ImapFlow,
303+
idleImapInstances: Map<string, ImapFlow>,
265304
imapInstances: Map<string, ImapFlow>,
266305
) {
267306
try {
268307
await client.getMailboxLock("INBOX");
269308
attachRealtimeEventHandlers(identityId, client, imapInstances);
270-
idleForever(identityId, client);
309+
idleForever(identityId, client, idleImapInstances, imapInstances);
271310
} catch (err) {
272311
console.error(
273312
`[realtime:${identityId}] failed to start realtime sync`,
@@ -295,7 +334,12 @@ export async function startRealtimeForIdentity(
295334
}
296335

297336
(client as any).__kurrierRealtimeStarted = true;
298-
await startRealtimeSyncForIdentity(identityId, client, imapInstances);
337+
await startRealtimeSyncForIdentity(
338+
identityId,
339+
client,
340+
idleImapInstances,
341+
imapInstances,
342+
);
299343
}
300344

301345
export async function stopRealtimeForIdentity(

0 commit comments

Comments
 (0)