From 5edbbf0816d36e2d0c8afc573fe450228255ccb9 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:23:20 +0100 Subject: [PATCH] perf: start plugin slot frames without waiting for the plugin's own frame --- .../plugin-host-bridge-origin.test.ts | 47 ++++++++- lib/__tests__/plugin-sandbox-loader.test.ts | 96 ++++++++++++++++++- lib/__tests__/plugin-store.test.ts | 3 + lib/__tests__/server-plugin-list.test.ts | 1 + lib/plugin-loader.ts | 2 +- lib/plugin-sandbox/host-bridge.ts | 19 ++++ lib/plugin-sandbox/loader.ts | 81 +++++++++++++--- stores/plugin-store.ts | 3 +- 8 files changed, 230 insertions(+), 22 deletions(-) diff --git a/lib/__tests__/plugin-host-bridge-origin.test.ts b/lib/__tests__/plugin-host-bridge-origin.test.ts index 9d33c9a91..92e86b232 100644 --- a/lib/__tests__/plugin-host-bridge-origin.test.ts +++ b/lib/__tests__/plugin-host-bridge-origin.test.ts @@ -20,6 +20,12 @@ function post(source: Window | null, origin: string, data: unknown) { } const instances: Array<{ destroy(): void }> = []; + +/** The frame answers init: its hooks and slots are in place. */ +function booted(inst: T): T { + post(inst.iframe.contentWindow, 'null', { type: 'init-done', hooks: [], slots: [], shortcuts: [] }); + return inst; +} afterEach(() => { for (const i of instances.splice(0)) i.destroy(); vi.clearAllMocks(); @@ -131,7 +137,7 @@ describe('slot sizing', () => { describe('shouldShow context', () => { it('leaves host callbacks out instead of failing to post them', async () => { - const inst = createBackgroundInstance({ plugin: plugin(), code: '', locale: 'en', tier: 'untrusted' }); + const inst = booted(createBackgroundInstance({ plugin: plugin(), code: '', locale: 'en', tier: 'untrusted' })); instances.push(inst); const sent = vi.spyOn(inst.iframe.contentWindow!, 'postMessage'); @@ -144,7 +150,7 @@ describe('shouldShow context', () => { describe('host bookkeeping', () => { it('fails a hook call whose arguments cannot be sent, without keeping it pending', async () => { - const inst = createBackgroundInstance({ plugin: plugin(), code: '', locale: 'en', tier: 'untrusted' }); + const inst = booted(createBackgroundInstance({ plugin: plugin(), code: '', locale: 'en', tier: 'untrusted' })); instances.push(inst); vi.spyOn(inst.iframe.contentWindow!, 'postMessage').mockImplementation(() => { throw new DOMException('no clone', 'DataCloneError'); }); @@ -337,3 +343,40 @@ describe('plugin translations in frames', () => { expect(Object.keys(init.payload.manifest.locales).sort()).toEqual(['en', 'ja']); }); }); + +describe('calls before the background frame has booted', () => { + // A plugin offering the slots remembered from its last start is in the + // registry while its frame boots; a message posted before init is lost. + it('holds shouldShow and hook calls until init-done', async () => { + const inst = createBackgroundInstance({ plugin: plugin(), code: '', locale: 'en', tier: 'untrusted' }); + instances.push(inst); + const sent: Array<{ type: string; id?: string }> = []; + vi.spyOn(inst.iframe.contentWindow!, 'postMessage').mockImplementation((m: unknown) => { sent.push(m as { type: string }); }); + + const show = inst.evaluateShouldShow('email-banner', {}); + const hook = inst.invokeHook('onAction', [{ actionId: 'x' }]); + await new Promise((r) => setTimeout(r, 0)); + expect(sent.filter((m) => m.type === 'slot-should-show' || m.type === 'hook-invoke')).toEqual([]); + + booted(inst); + await new Promise((r) => setTimeout(r, 0)); + const ask = sent.find((m) => m.type === 'slot-should-show')!; + const call = sent.find((m) => m.type === 'hook-invoke')!; + post(inst.iframe.contentWindow, 'null', { type: 'slot-should-show-result', id: ask.id, show: true }); + post(inst.iframe.contentWindow, 'null', { type: 'hook-result', id: call.id, ok: true, result: 'done' }); + await expect(show).resolves.toBe(true); + await expect(hook).resolves.toBe('done'); + }); + + it('gives up on them when the frame is destroyed first', async () => { + const inst = createBackgroundInstance({ plugin: plugin(), code: '', locale: 'en', tier: 'untrusted' }); + const show = inst.evaluateShouldShow('email-banner', {}); + const hook = inst.invokeHook('onAction', []); + inst.destroy(); + + await expect(show).resolves.toBe(false); + await expect(hook).rejects.toThrow(/destroyed/); + await expect(inst.initPromise).rejects.toThrow(/destroyed/); + expect(inst.closed).toBe(true); + }); +}); diff --git a/lib/__tests__/plugin-sandbox-loader.test.ts b/lib/__tests__/plugin-sandbox-loader.test.ts index 052e46ca5..8a2715553 100644 --- a/lib/__tests__/plugin-sandbox-loader.test.ts +++ b/lib/__tests__/plugin-sandbox-loader.test.ts @@ -41,7 +41,7 @@ vi.mock('../plugin-sandbox/shortcuts', () => ({ })); import { loadSandboxedPlugin, unloadSandboxedPlugin, setSandboxStoreAccessor, setSandboxLocale } from '../plugin-sandbox/loader'; -import { get as getActive } from '../plugin-sandbox/registry'; +import { get as getActive, offersForSlot } from '../plugin-sandbox/registry'; const CODE = 'export default { activate() {} }'; const HASH = createHash('sha256').update(CODE, 'utf-8').digest('hex'); @@ -67,11 +67,13 @@ function plugin(overrides: Partial = {}): InstalledPlugin { } function fakeBackground() { - return { - initPromise: Promise.resolve({ hooks: [], slots: [], shortcuts: [] }), + const bg = { + initPromise: Promise.resolve({ hooks: [], slots: [], shortcuts: [] }) as Promise, invokeHook: vi.fn(), - destroy: vi.fn(), + closed: false, + destroy: vi.fn(() => { bg.closed = true; }), }; + return bg; } let statuses: Array<{ id: string; status: string; error?: string }>; @@ -260,3 +262,89 @@ describe('loading the same plugin twice', () => { expect(mocks.createBackgroundInstance).toHaveBeenCalledTimes(1); }); }); + +describe('slots remembered from the last start', () => { + const OFFERS = [{ name: 'sidebar-section', hasShouldShow: false, order: 100 }]; + const perms = { permissions: ['ui:sidebar-widget'], grantedPermissions: ['ui:sidebar-widget'] } as Partial; + + function startingBackground() { + let finish!: (info: unknown) => void; + let fail!: (err: Error) => void; + const bg = fakeBackground(); + bg.initPromise = new Promise((res, rej) => { finish = res; fail = rej; }); + mocks.createBackgroundInstance.mockImplementation(() => bg); + return { finish, fail }; + } + + beforeEach(() => { + localStorage.clear(); + mocks.getCode.mockResolvedValue(CODE); + }); + + it('offers them while the background frame is still booting, then the real list', async () => { + // A slot frame used to start only after the plugin's own frame had booted. + localStorage.setItem('plugin-slot-offers:managed-plugin', JSON.stringify({ hash: HASH, slots: OFFERS })); + const { finish } = startingBackground(); + + const load = loadSandboxedPlugin(plugin(perms)); + await vi.waitFor(() => expect(offersForSlot('sidebar-section').map((o) => o.pluginId)).toEqual(['managed-plugin'])); + expect(lastStatus()?.status).not.toBe('running'); + + finish({ hooks: [], slots: [{ name: 'sidebar-section', hasShouldShow: true, order: 5 }], shortcuts: [] }); + await load; + expect(offersForSlot('sidebar-section')).toEqual([{ pluginId: 'managed-plugin', order: 5, hasShouldShow: true }]); + expect(lastStatus()).toMatchObject({ status: 'running' }); + }); + + it('remembers the list for the bundle it came from only', async () => { + mocks.createBackgroundInstance.mockImplementation(() => ({ ...fakeBackground(), initPromise: Promise.resolve({ hooks: [], slots: OFFERS, shortcuts: [] }) })); + await loadSandboxedPlugin(plugin(perms)); + expect(JSON.parse(localStorage.getItem('plugin-slot-offers:managed-plugin')!)).toEqual({ hash: HASH, slots: OFFERS }); + unloadSandboxedPlugin('managed-plugin'); + + // Another bundle: wait for its own list. + localStorage.setItem('plugin-slot-offers:managed-plugin', JSON.stringify({ hash: 'f'.repeat(64), slots: OFFERS })); + const { fail } = startingBackground(); + const load = loadSandboxedPlugin(plugin(perms)); + await new Promise((r) => setTimeout(r, 20)); + expect(offersForSlot('sidebar-section')).toEqual([]); + fail(new Error('done')); + await load; + }); + + it('does not offer a remembered slot the plugin may not use', async () => { + localStorage.setItem('plugin-slot-offers:managed-plugin', JSON.stringify({ hash: HASH, slots: OFFERS })); + const { fail } = startingBackground(); + const load = loadSandboxedPlugin(plugin()); + await new Promise((r) => setTimeout(r, 20)); + expect(offersForSlot('sidebar-section')).toEqual([]); + fail(new Error('done')); + await load; + }); + + it('takes them down when the plugin fails to start', async () => { + localStorage.setItem('plugin-slot-offers:managed-plugin', JSON.stringify({ hash: HASH, slots: OFFERS })); + const { fail } = startingBackground(); + const load = loadSandboxedPlugin(plugin(perms)); + await vi.waitFor(() => expect(offersForSlot('sidebar-section')).toHaveLength(1)); + + fail(new Error('boom')); + await load; + expect(offersForSlot('sidebar-section')).toEqual([]); + expect(getActive('managed-plugin')).toBeUndefined(); + expect(lastStatus()).toMatchObject({ status: 'error' }); + }); + + it('is not marked failed when it was turned off while starting', async () => { + localStorage.setItem('plugin-slot-offers:managed-plugin', JSON.stringify({ hash: HASH, slots: OFFERS })); + const { fail } = startingBackground(); + const load = loadSandboxedPlugin(plugin(perms)); + await vi.waitFor(() => expect(getActive('managed-plugin')).toBeDefined()); + + unloadSandboxedPlugin('managed-plugin'); + // What destroy() does to a starting frame. + fail(new Error('sandbox destroyed')); + await load; + expect(lastStatus()).toMatchObject({ status: 'disabled' }); + }); +}); diff --git a/lib/__tests__/plugin-store.test.ts b/lib/__tests__/plugin-store.test.ts index bd31eeff3..92ec8f130 100644 --- a/lib/__tests__/plugin-store.test.ts +++ b/lib/__tests__/plugin-store.test.ts @@ -28,6 +28,7 @@ vi.mock('@/lib/plugin-loader', () => ({ setPluginStoreAccessor: vi.fn(), setupAutoDisable: vi.fn(), setSandboxLocale: vi.fn(), + forgetSlotOffers: vi.fn(), })); vi.mock('@/lib/plugin-hooks', () => ({ @@ -60,6 +61,7 @@ import { register as registerActive, deregister as deregisterActive, get as getA // Import after mocks import { usePluginStore, PLUGIN_PERSIST_DELAY_MS } from '@/stores/plugin-store'; import { pluginStorage } from '@/lib/plugin-storage'; +import { forgetSlotOffers } from '@/lib/plugin-loader'; function resetStore() { usePluginStore.setState({ @@ -460,6 +462,7 @@ describe('plugins the admin removed', () => { expect(usePluginStore.getState().plugins.some((p) => p.id === 'gone')).toBe(false); expect(localStorage.getItem('plugin:gone:@acct:notes')).toBeNull(); expect(pluginStorage.deletePreview).toHaveBeenCalledWith('gone'); + expect(forgetSlotOffers).toHaveBeenCalledWith('gone'); }); }); diff --git a/lib/__tests__/server-plugin-list.test.ts b/lib/__tests__/server-plugin-list.test.ts index cf8e216a3..a88006a51 100644 --- a/lib/__tests__/server-plugin-list.test.ts +++ b/lib/__tests__/server-plugin-list.test.ts @@ -11,6 +11,7 @@ vi.mock('@/lib/plugin-loader', () => ({ setPluginStoreAccessor: vi.fn(), setupAutoDisable: vi.fn(), setSandboxLocale: vi.fn(), + forgetSlotOffers: vi.fn(), })); import { fetchServerPluginList } from '@/lib/server-plugin-list'; diff --git a/lib/plugin-loader.ts b/lib/plugin-loader.ts index ba80723cf..afcea8463 100644 --- a/lib/plugin-loader.ts +++ b/lib/plugin-loader.ts @@ -17,7 +17,7 @@ import { all as allActive, get as getActive } from './plugin-sandbox/registry'; // Re-export so the plugin store can keep the sandbox locale in step via this // facade, instead of importing lib/plugin-sandbox/loader directly (which would // also pull the hook buses into consumers' module graphs). -export { setSandboxLocale } from './plugin-sandbox/loader'; +export { setSandboxLocale, forgetSlotOffers } from './plugin-sandbox/loader'; export { snapshotRegionalSettings, type RegionalSnapshot } from './plugin-sandbox/host-bridge'; /** diff --git a/lib/plugin-sandbox/host-bridge.ts b/lib/plugin-sandbox/host-bridge.ts index 23970c4b5..35e93a89a 100644 --- a/lib/plugin-sandbox/host-bridge.ts +++ b/lib/plugin-sandbox/host-bridge.ts @@ -290,6 +290,8 @@ export class SandboxInstance { private rejectInit!: (err: Error) => void; private listener: (ev: MessageEvent) => void; private destroyed = false; + /** The frame has answered init-done: its hooks and slots are in place. */ + private initialized = false; private pendingHookInvokes = new Map void; reject: (e: Error) => void }>(); private pendingShouldShow = new Map void>(); @@ -326,6 +328,8 @@ export class SandboxInstance { this.resolveInit = res; this.rejectInit = rej; }); + // Nobody waits on a slot frame's init; its rejection is not an error. + this.initPromise.catch(() => {}); this.iframe = document.createElement('iframe'); // Privileged tier: same-origin in BOTH dev and prod so the iframe gets real @@ -451,6 +455,7 @@ export class SandboxInstance { return; case 'init-done': + this.initialized = true; if (this.mode === 'slot') this.iframe.style.visibility = ''; this.resolveInit({ hooks: msg.hooks, slots: msg.slots, shortcuts: msg.shortcuts ?? [] }); return; @@ -546,6 +551,10 @@ export class SandboxInstance { /** Dispatch a hook handler inside the sandbox; resolves with its return value. */ invokeHook(hookName: string, args: unknown[]): Promise { if (this.destroyed) return Promise.reject(new Error('sandbox destroyed')); + // A plugin offering remembered slots is in the registry before its frame + // has booted; a click on one of its actions waits for it (a message + // posted before init is lost). + if (!this.initialized) return this.initPromise.then(() => this.invokeHook(hookName, args)); const id = uid(); const p = new Promise((resolve, reject) => { // The bus gives up on a slow handler, but the entry used to stay here @@ -571,6 +580,9 @@ export class SandboxInstance { /** Ask the background instance whether a slot should mount for this context. */ evaluateShouldShow(slot: SlotName, context: unknown): Promise { if (this.destroyed) return Promise.resolve(false); + if (!this.initialized) { + return this.initPromise.then(() => this.evaluateShouldShow(slot, context), () => false); + } const id = uid(); const p = new Promise((resolve) => { this.pendingShouldShow.set(id, resolve); @@ -649,9 +661,16 @@ export class SandboxInstance { this.send(msg); } + /** True once destroy() ran. */ + get closed(): boolean { + return this.destroyed; + } + destroy(): void { if (this.destroyed) return; this.destroyed = true; + // Whoever waits for init (the loader, calls held back below) stops waiting. + this.rejectInit(new Error('sandbox destroyed')); liveInstances.delete(this); window.removeEventListener('message', this.listener); this.iframe.remove(); diff --git a/lib/plugin-sandbox/loader.ts b/lib/plugin-sandbox/loader.ts index a3299d46d..a95b2bc47 100644 --- a/lib/plugin-sandbox/loader.ts +++ b/lib/plugin-sandbox/loader.ts @@ -18,9 +18,9 @@ import { useEmailStore } from '@/stores/email-store'; import { emailToReadView } from '../plugin-projection'; import { verifyBundle } from './bundle-integrity'; import { downloadManagedBundle } from './bundle-fetch'; -import { createBackgroundInstance, forEachInstance, snapshotRegionalSettings, type RegionalSnapshot } from './host-bridge'; +import { createBackgroundInstance, forEachInstance, snapshotRegionalSettings, type InitDoneInfo, type RegionalSnapshot } from './host-bridge'; import { resolvePluginTier } from './tier'; -import { register as registerActive, deregister as deregisterActive, all as allActiveEntries, get as getActive } from './registry'; +import { register as registerActive, deregister as deregisterActive, all as allActiveEntries, get as getActive, type SlotOffer } from './registry'; import { cancelPluginDialogs } from './host-api'; import { hasDialogFor, whenNoDialogFor } from './host-dialog'; import { registerShortcuts } from './shortcuts'; @@ -98,7 +98,7 @@ async function readCachedBundle(plugin: InstalledPlugin): Promise } } -async function getBundleCode(plugin: InstalledPlugin): Promise { +async function getBundleCode(plugin: InstalledPlugin): Promise<{ code: string; hash: string }> { // IndexedDB is the fast path: the store's server sync (managed plugins) and // the upload flow (user plugins) both populate it. It is per-browser though, // while a managed plugin's record travels with the server registry to every @@ -111,8 +111,7 @@ async function getBundleCode(plugin: InstalledPlugin): Promise { const cached = await readCachedBundle(plugin); if (cached) { try { - await verifyBundle(cached, plugin.bundleHash); - return cached; + return { code: cached, hash: await verifyBundle(cached, plugin.bundleHash) }; } catch (err) { if (!plugin.managed) throw err; // Stale (sync updated the record but not the bytes) or corrupt copy; the @@ -127,14 +126,60 @@ async function getBundleCode(plugin: InstalledPlugin): Promise { const code = await downloadManagedBundle(plugin.id, plugin.bundleHash, { requireSignature: plugin.tier === 'privileged', }); - await verifyBundle(code, plugin.bundleHash); + const hash = await verifyBundle(code, plugin.bundleHash); try { await pluginStorage.saveCode(plugin.id, code); } catch (err) { // Caching is best effort - the plugin runs from the fetched bytes either way. console.warn(`[plugin-sandbox] Could not cache bundle for "${plugin.id}":`, err); } - return code; + return { code, hash }; +} + +// ─── Slot offers from the last start ───────────────────────── + +// A plugin's slots are fixed exports of its bundle. They used to be known +// only once its background frame had booted and run activate(), so a slot +// frame (a sidebar section, say) started ~1 s after the plugin did, behind +// it. The offers are kept per bundle hash, and a plugin started with the +// same verified bundle offers them right away; its frames then boot next to +// the background one. The real list replaces them when init is done. The +// record only says where to draw: the same permission filter applies and +// the frames run the same verified code. A changed bundle has another hash. +const SLOT_CACHE_PREFIX = 'plugin-slot-offers:'; + +function readSlotOffers(pluginId: string, hash: string): InitDoneInfo['slots'] | null { + try { + const raw = localStorage.getItem(SLOT_CACHE_PREFIX + pluginId); + if (!raw) return null; + const rec = JSON.parse(raw) as { hash?: unknown; slots?: unknown }; + if (rec.hash !== hash || !Array.isArray(rec.slots)) return null; + return rec.slots.filter((o): o is InitDoneInfo['slots'][number] => + !!o && typeof o === 'object' && typeof (o as { name?: unknown }).name === 'string'); + } catch { + return null; + } +} + +function writeSlotOffers(pluginId: string, hash: string, slots: InitDoneInfo['slots']): void { + try { + localStorage.setItem(SLOT_CACHE_PREFIX + pluginId, JSON.stringify({ hash, slots })); + } catch { + // Storage blocked or full: the next start just waits for init again. + } +} + +/** Forget a plugin's remembered slots (it was removed). */ +export function forgetSlotOffers(pluginId: string): void { + try { localStorage.removeItem(SLOT_CACHE_PREFIX + pluginId); } catch { /* ignore */ } +} + +function permittedOffers(plugin: InstalledPlugin, slots: InitDoneInfo['slots'], warn: boolean): SlotOffer[] { + return slots.filter((offer) => { + if (mayOfferSlot(plugin, offer.name)) return true; + if (warn) console.warn(`[plugin-sandbox] Plugin "${plugin.id}" lacks the permission for slot "${offer.name}"; not rendered`); + return false; + }); } // ─── Load ───────────────────────────────────────────────────── @@ -166,9 +211,11 @@ const loading = new Map>(); */ export function loadSandboxedPlugin(plugin: InstalledPlugin): Promise { if (typeof window === 'undefined') return Promise.resolve(); - if (getActive(plugin.id)) return Promise.resolve(); + // Before the registry: a plugin offering remembered slots is registered + // while it is still starting. const pending = loading.get(plugin.id); if (pending) return pending; + if (getActive(plugin.id)) return Promise.resolve(); const run = startSandboxedPlugin(plugin).finally(() => loading.delete(plugin.id)); loading.set(plugin.id, run); return run; @@ -188,7 +235,7 @@ async function startSandboxedPlugin(plugin: InstalledPlugin): Promise { } const tier = resolution.tier; - const code = await getBundleCode(plugin); + const { code, hash } = await getBundleCode(plugin); background = createBackgroundInstance({ plugin, code, @@ -196,6 +243,11 @@ async function startSandboxedPlugin(plugin: InstalledPlugin): Promise { tier, }); + const remembered = readSlotOffers(plugin.id, hash); + if (remembered && remembered.length > 0) { + registerActive({ plugin, code, tier, background, slotOffers: permittedOffers(plugin, remembered, false), hookDisposables: [] }); + } + // Wait for the background runtime to evaluate the bundle, register hooks, // and enumerate slots. Bounded so a stuck iframe doesn't hang activation. const bg = background; @@ -244,11 +296,8 @@ async function startSandboxedPlugin(plugin: InstalledPlugin): Promise { } } - const slotOffers = info.slots.filter((offer) => { - if (mayOfferSlot(plugin, offer.name)) return true; - console.warn(`[plugin-sandbox] Plugin "${plugin.id}" lacks the permission for slot "${offer.name}"; not rendered`); - return false; - }); + const slotOffers = permittedOffers(plugin, info.slots, true); + writeSlotOffers(plugin.id, hash, info.slots); registerActive({ plugin, @@ -270,6 +319,10 @@ async function startSandboxedPlugin(plugin: InstalledPlugin): Promise { } console.info(`[plugin-sandbox] "${plugin.id}" activated (hooks=${info.hooks.length}, slots=${info.slots.length})`); } catch (err) { + // Turned off while it was starting: unload has set the status. + if (background?.closed && getActive(plugin.id)?.background !== background) return; + // Offered from the last start: take the slots back down. + if (background && getActive(plugin.id)?.background === background) deregisterActive(plugin.id); const msg = (err as Error).message ?? String(err); storeAccessor?.setPluginStatus(plugin.id, 'error', msg); console.error(`[plugin-sandbox] Failed to load "${plugin.id}":`, err); diff --git a/stores/plugin-store.ts b/stores/plugin-store.ts index 4cdc4ae7d..4e9f3fad1 100644 --- a/stores/plugin-store.ts +++ b/stores/plugin-store.ts @@ -7,7 +7,7 @@ import { persist, type PersistStorage, type StorageValue } from 'zustand/middlew import type { InstalledPlugin, PluginStatus, PluginTier } from '@/lib/plugin-types'; import { pluginStorage } from '@/lib/plugin-storage'; import { extractPlugin } from '@/lib/plugin-validator'; -import { loadPlugin, deactivatePlugin, setPluginStoreAccessor, setupAutoDisable, setSandboxLocale } from '@/lib/plugin-loader'; +import { loadPlugin, deactivatePlugin, setPluginStoreAccessor, setupAutoDisable, setSandboxLocale, forgetSlotOffers } from '@/lib/plugin-loader'; import { useLocaleStore } from '@/stores/locale-store'; import { useSettingsStore } from '@/stores/settings-store'; import { getEffectiveLocale } from '@/i18n/detect-locale'; @@ -421,6 +421,7 @@ export const usePluginStore = create()( function removePluginData(id: string): void { deactivatePlugin(id); removeAllPluginHooks(id); + forgetSlotOffers(id); void pluginStorage.deleteCode(id).catch(() => {}); void pluginStorage.deletePreview(id).catch(() => {}); // Accounts the user connected for it live on the server (this mail account's).