email sync: API passes full config at enqueue, auto-reconnect on IMAP drops
Sidecar/queue runner no longer needs job-specific context. API server resolves account details, IMAP auth, and user email at enqueue time — all persisted in the job file. Handler reads directly from job meta. Removed "Load account" step. Sync step auto-reconnects up to 10 times when Gmail drops the connection, resuming from saved UIDs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,39 @@ export const accountsRouter = createRouter();
|
||||
accountsRouter.get('/', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const accounts = await getEmailAccounts(user.id);
|
||||
|
||||
// Check for stale syncing/queued accounts with no active job
|
||||
const staleIds: number[] = [];
|
||||
const hasActiveAccounts = accounts.some((a) => a.status === 'syncing' || a.status === 'queued');
|
||||
let activeJobAccountIds = new Set<number>();
|
||||
|
||||
if (hasActiveAccounts) {
|
||||
try {
|
||||
const jobs = await sidecar.listJobs();
|
||||
activeJobAccountIds = new Set(
|
||||
jobs
|
||||
.filter((j) => j.type === 'email-sync' && (j.status === 'queued' || j.status === 'running'))
|
||||
.map((j) => (j.meta as Record<string, unknown> | undefined)?.emailAccountId as number)
|
||||
.filter(Boolean),
|
||||
);
|
||||
} catch {
|
||||
// Sidecar unavailable — all syncing/queued accounts are stale
|
||||
}
|
||||
|
||||
for (const a of accounts) {
|
||||
if ((a.status === 'syncing' || a.status === 'queued') && !activeJobAccountIds.has(a.id)) {
|
||||
staleIds.push(a.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset stale accounts in background
|
||||
if (staleIds.length > 0) {
|
||||
for (const id of staleIds) {
|
||||
updateEmailAccountStatus(id, 'connected').catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.json(
|
||||
accounts.map((a) => ({
|
||||
id: a.id,
|
||||
@@ -43,7 +76,7 @@ accountsRouter.get('/', async (ctx) => {
|
||||
email: a.email,
|
||||
displayName: a.displayName,
|
||||
enabled: a.enabled,
|
||||
status: a.status,
|
||||
status: staleIds.includes(a.id) ? 'connected' : a.status,
|
||||
createdAt: a.createdAt,
|
||||
})),
|
||||
);
|
||||
@@ -104,6 +137,10 @@ accountsRouter.post('/:id/sync', async (ctx) => {
|
||||
if (account.status === 'syncing') throw BAD_REQUEST('Account is already syncing');
|
||||
if (account.status === 'synced') throw BAD_REQUEST('Account is already synced — incremental syncs run automatically');
|
||||
|
||||
// Resolve auth before enqueueing
|
||||
const authResult = await resolveAuth(user.id, account.authType, account.email, account.credentials as Record<string, unknown>);
|
||||
if (!authResult.ok) throw BAD_REQUEST(authResult.error);
|
||||
|
||||
// Set status immediately so the UI reflects the queued state
|
||||
await updateEmailAccountStatus(id, 'queued');
|
||||
|
||||
@@ -111,7 +148,22 @@ accountsRouter.post('/:id/sync', async (ctx) => {
|
||||
lane: 'email',
|
||||
type: 'email-sync',
|
||||
userId: user.email,
|
||||
meta: { emailAccountId: id },
|
||||
meta: {
|
||||
emailAccountId: id,
|
||||
userEmail: user.email,
|
||||
account: {
|
||||
id: account.id,
|
||||
userId: account.userId,
|
||||
email: account.email,
|
||||
imapHost: account.imapHost,
|
||||
imapPort: account.imapPort,
|
||||
imapSecure: account.imapSecure,
|
||||
provider: account.provider,
|
||||
authType: account.authType,
|
||||
credentials: account.credentials,
|
||||
},
|
||||
imapAuth: { user: account.email, ...authResult.auth },
|
||||
},
|
||||
});
|
||||
|
||||
return ctx.json({ ok: true, jobId: job.id }, 201);
|
||||
|
||||
Reference in New Issue
Block a user