inline email resync instead of job queue, refresh list on completion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 17:07:06 +00:00
co-authored by Claude Opus 4.6
parent ea8113e61d
commit b3f1d10bc1
7 changed files with 367 additions and 83 deletions
+9 -63
View File
@@ -1,74 +1,28 @@
import type { Job, EnqueueParams } from '../../queue/types';
import { getAllSyncedAccounts, getUserById, getUserIntegration } from 'officerdb';
import { getAllSyncedAccounts, getUserById } from 'officerdb';
import { performResync } from '../../api/email/resync';
const INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
let timer: ReturnType<typeof setInterval> | null = null;
let enqueueFn: ((params: EnqueueParams) => Promise<Job>) | null = null;
let listJobsFn: (() => Promise<Job[]>) | null = null;
async function tick() {
if (!enqueueFn || !listJobsFn) return;
try {
const accounts = await getAllSyncedAccounts();
if (accounts.length === 0) return;
const allJobs = await listJobsFn();
const activeEmailSyncIds = new Set(
allJobs
.filter((j) => j.type === 'email-sync' && (j.status === 'queued' || j.status === 'running'))
.map((j) => (j.meta as Record<string, unknown> | undefined)?.emailAccountId),
);
for (const account of accounts) {
if (activeEmailSyncIds.has(account.id)) continue;
const user = await getUserById(account.userId);
if (!user) continue;
// Resolve IMAP auth
const imapAuth: Record<string, unknown> = { user: account.email };
if (account.authType === 'oauth') {
const integration = await getUserIntegration(account.userId, 'google');
const config = integration?.config as Record<string, unknown> | undefined;
const accessToken = config?.accessToken as string | undefined;
if (!accessToken) {
console.log(`[email-cron] Skipping ${account.email}: no OAuth access token`);
continue;
}
imapAuth.accessToken = accessToken;
} else {
const creds = account.credentials as Record<string, unknown>;
imapAuth.pass = creds.password;
}
try {
await enqueueFn({
lane: 'email',
type: 'email-sync',
userId: user.email,
meta: {
emailAccountId: account.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,
},
});
console.log(`[email-cron] Enqueued incremental sync for ${account.email}`);
console.log(`[email-cron] Resyncing ${account.email}`);
const result = await performResync({ accountId: account.id, userEmail: user.email, userId: user.id });
if (result.saved > 0) {
console.log(`[email-cron] ${account.email}: ${result.saved} new emails`);
}
} catch (err) {
console.error(
`[email-cron] Failed to enqueue sync for ${account.email}:`,
`[email-cron] Failed to resync ${account.email}:`,
err instanceof Error ? err.message : err,
);
}
@@ -78,18 +32,10 @@ async function tick() {
}
}
type EmailCronDeps = {
enqueue: (params: EnqueueParams) => Promise<Job>;
listJobs: () => Promise<Job[]>;
};
export function initEmailCron(deps: EmailCronDeps) {
export function initEmailCron() {
if (timer) return;
enqueueFn = deps.enqueue;
listJobsFn = deps.listJobs;
console.log(`[email-cron] Starting email sync cron (every ${INTERVAL_MS / 60_000} min)`);
timer = setInterval(tick, INTERVAL_MS);
// Run first tick after a short delay
setTimeout(tick, 30_000);
}
+1 -1
View File
@@ -83,7 +83,7 @@ const connection = createSidecarConnector({
},
onConnected() {
// Start email cron once connected (so queue commands can reach API server)
// initEmailCron({ enqueue: enqueueViaWs, listJobs: listJobsViaWs }); // TODO: re-enable after testing
initEmailCron();
},
});