/** * Worker entry point — runs the entire mailx service layer off the main thread. * * Hosts: wa-sqlite DB, sync manager, providers, service, settings. * Main thread only handles UI rendering and proxies TCP/native bridge calls. */ import { initWorkerTcp, handleTcpMessage, WorkerTcpTransport } from "./worker-tcp-transport.js"; import { WebMailxDB } from "./db.js"; import { WebMessageStore } from "./web-message-store.js"; import { WebMailxService } from "./web-service.js"; import { SyncManager } from "./sync-manager.js"; import { dispatch } from "./web-jsonrpc.js"; import type { AccountConfig } from "@bobfrankston/mailx-types"; import { loadAccounts, loadAccountsFromCloud, loadSettings, clearSettings, setGDriveTokenProvider, setGDriveFolderId } from "./web-settings.js"; // ── Globals ── let db: WebMailxDB; let bodyStore: WebMessageStore; let syncManager: SyncManager; let service: WebMailxService; const post = (msg: any) => (self as any).postMessage(msg); // ── Event emission → main thread ── function emitEvent(event: any): void { post({ type: "event", event }); } function vlog(msg: string): void { // Route through main thread for logit (fetch works in workers but keep consistent) try { fetch(`https://rmf39.aaz.lt/logit/${encodeURIComponent("V/" + msg.substring(0, 800))}?log=mailx-android&silent=true`).catch(() => {}); } catch { /* ignore */ } } // ── Native bridge proxy ── let nativeReqCounter = 0; const pendingNative = new Map void; reject: (e: Error) => void }>(); function nativeRequest(op: string, args?: any[]): Promise { const reqId = ++nativeReqCounter; return new Promise((resolve, reject) => { pendingNative.set(reqId, { resolve, reject }); post({ type: "native", reqId, op, args }); setTimeout(() => { if (pendingNative.has(reqId)) { pendingNative.delete(reqId); reject(new Error(`Native ${op} timeout`)); } }, 60000); }); } function handleNativeResponse(msg: any): void { const pending = pendingNative.get(msg.reqId); if (pending) { pendingNative.delete(msg.reqId); if (msg.error) pending.reject(new Error(msg.error)); else pending.resolve(msg.result); } } // ── Token provider (proxied through main thread → C# OAuth) ── function createTokenProvider(email: string): () => Promise { return async () => { console.log(`[oauth] Refreshing token for ${email}`); return nativeRequest("refreshToken", [email]); }; } // ── GDrive folder lookup ── async function findGDriveMailxFolder(tokenProvider: () => Promise): Promise<{ id: string; name: string; path: string; ownerEmail?: string } | null> { const token = await tokenProvider(); const headers = { "Authorization": `Bearer ${token}` }; // Two-step path traversal: My Drive root → home/ → .rmfmail const homeQ = encodeURIComponent("name='home' and mimeType='application/vnd.google-apps.folder' and 'root' in parents and trashed=false"); const homeRes = await fetch( `https://www.googleapis.com/drive/v3/files?q=${homeQ}&fields=files(id,name)&spaces=drive`, { headers } ); if (!homeRes.ok) return null; const home = (await homeRes.json() as any).files?.[0]; if (!home?.id) return null; const q = encodeURIComponent(`name='.rmfmail' and mimeType='application/vnd.google-apps.folder' and '${home.id}' in parents and trashed=false`); const r = await fetch( `https://www.googleapis.com/drive/v3/files?q=${q}&fields=files(id,name,owners(emailAddress))&spaces=drive`, { headers } ); if (!r.ok) return null; const data = await r.json(); const folder = data.files?.[0]; if (!folder?.id) return null; const homeName = home.name || "home"; const folderName = folder.name || ".rmfmail"; return { id: folder.id, name: folderName, path: `My Drive/${homeName}/${folderName}`, ownerEmail: folder.owners?.[0]?.emailAddress, }; } // ── Message handler ── (self as any).onmessage = async (e: MessageEvent) => { const msg = e.data; if (msg.type === "tcp-response" || msg.type === "tcp-data" || msg.type === "tcp-close" || msg.type === "tcp-error") { handleTcpMessage(msg); return; } if (msg.type === "native-response") { handleNativeResponse(msg); return; } if (msg.type === "rpc") { try { const result = await dispatch(service as any, { _action: msg.action, _cbid: String(msg.id), ...msg.params }); post({ type: "rpc-response", id: msg.id, result: result.result, error: result.error }); } catch (e: any) { post({ type: "rpc-response", id: msg.id, error: e.message }); } return; } if (msg.type === "init") { try { await initialize(); post({ type: "init-complete" }); } catch (e: any) { console.error(`[worker] Init failed: ${e.message}\n${e.stack}`); post({ type: "init-error", error: e.message }); } return; } }; // ── Initialization ── async function initialize(): Promise { console.log("[worker] Initializing service layer..."); // Wire up TCP proxy for IMAP/SMTP initWorkerTcp(post); // Initialize DB (wa-sqlite runs in this Worker thread — no main thread blocking) db = new WebMailxDB("mailx"); await db.waitReady(); bodyStore = new WebMessageStore(); // Create sync manager with injected deps syncManager = new SyncManager(db, bodyStore, { emitEvent, vlog, createTcpTransport: () => new WorkerTcpTransport(), }); service = new WebMailxService(db, bodyStore, syncManager); // Load accounts let accounts = await loadAccounts(); console.log(`[worker] ${accounts.length} account(s) found`); let gmailTokenProvider: (() => Promise) | null = null; for (const account of accounts) { if (!account.enabled) continue; const domain = account.email?.split("@")[1]?.toLowerCase() || ""; if (domain === "gmail.com" || domain === "googlemail.com") { const tp = createTokenProvider(account.email); syncManager.setTokenProvider(account.id, tp); if (!gmailTokenProvider) gmailTokenProvider = tp; } await syncManager.addAccount(account); } // GDrive setup — read shared accounts from cloud if (gmailTokenProvider) { setGDriveTokenProvider(gmailTokenProvider); try { console.log("[worker] Looking up GDrive .rmfmail folder..."); const folder = await findGDriveMailxFolder(gmailTokenProvider); if (!folder) { emitEvent({ type: "fatal", key: "gdrive-folder-missing", message: "GDrive folder '.rmfmail' not found — app cannot start. Create it or sign in with the correct Google account.", }); } else { setGDriveFolderId(folder.id, folder.name, folder.ownerEmail, folder.path); console.log(`[worker] GDrive ${folder.path} folder: ${folder.id} (owner=${folder.ownerEmail || "?"})`); // Load accounts from cloud const cloudAccounts = await loadAccountsFromCloud(); if (cloudAccounts && cloudAccounts.length > 0) { console.log(`[worker] GDrive returned ${cloudAccounts.length} accounts`); for (const account of cloudAccounts) { if (!account.enabled) continue; const domain = account.email?.split("@")[1]?.toLowerCase() || ""; if (domain === "gmail.com" || domain === "googlemail.com") { const tp = createTokenProvider(account.email); syncManager.setTokenProvider(account.id, tp); if (!gmailTokenProvider) gmailTokenProvider = tp; } await syncManager.addAccount(account); } } } } catch (e: any) { emitEvent({ type: "fatal", key: "gdrive-setup-failed", message: `GDrive setup failed — app cannot start: ${e.message}`, }); } } // Periodic sync (runs in Worker — never blocks UI) setInterval(() => { console.log("[sync] periodic poll"); syncManager.syncAll().catch((e: any) => console.error(`[worker] Periodic sync error: ${e.message}`)); }, 2 * 60 * 1000); // Initial sync after 1 second setTimeout(() => { syncManager.syncAll().catch((e: any) => console.error(`[worker] Initial sync error: ${e.message}`)); }, 1000); console.log("[worker] Service layer ready"); emitEvent({ type: "connected" }); }