Files
webmail/lib/__tests__/push-bindings.test.ts
T
lucamzanon 2d5756bff7 perf(push): reconcile push bindings instead of rebuilding them
The push effect re-runs every time a login connects, which while a browser
with many logins restores means once per account. Each run closed every
client's push and set it up again: the live streams were torn down and
reopened and every slow poll re-primed its baseline with a full state read
(Mailbox, Email, Calendar, CalendarEvent, SieveScript) - quadratic in the
number of accounts. With ten logins that was 21 stream openings and about
180 state-poll methods in the first seconds.

Reconcile instead: bind the clients that are new, unbind the ones that are
gone, and on an account switch re-bind only the two logins whose role
changed, handing the live stream to the newly active one when it has
none. Handlers are read through a ref at event time, so a binding never has
to be re-created because a callback changed identity.

The reconciliation is a pure function (lib/push-bindings.ts) with its own
tests; the effect only supplies the callbacks.
2026-09-30 19:03:34 +02:00

109 lines
4.1 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { reconcilePushBindings, releasePushBindings, type PushBinding, type PushRole } from '../push-bindings';
/** A client that counts what the reconciliation does to it. */
class FakeClient {
setups = 0;
closes = 0;
sse = false;
roles: PushRole[] = [];
constructor(readonly name: string, private readonly slots: { free: number }) {}
setupPushNotifications() {
this.setups++;
if (this.slots.free > 0) { this.slots.free--; this.sse = true; }
}
closePushNotifications() {
this.closes++;
if (this.sse) { this.slots.free++; this.sse = false; }
}
hasSSEStream() { return this.sse; }
}
function world(names: string[], freeSlots = 2) {
const slots = { free: freeSlots };
const clients = new Map(names.map((n) => [n, new FakeClient(n, slots)] as const));
const bindings = new Map<FakeClient, PushBinding>();
const run = (connected: string[], active: string | null) => reconcilePushBindings({
bindings,
clients: new Map(connected.map((n) => [n, clients.get(n)!] as const)),
activeAccountId: active,
listen: (c, role) => { c.roles.push(role); },
});
return { clients, bindings, run, slots };
}
describe('reconcilePushBindings', () => {
it('binds each login once however many times the effect re-runs while logins connect', () => {
const { clients, run } = world(['a', 'b', 'c', 'd']);
// A restore: the effect runs once per login that connects.
run(['a'], 'a');
run(['a', 'b'], 'a');
run(['a', 'b', 'c'], 'a');
run(['a', 'b', 'c', 'd'], 'a');
run(['a', 'b', 'c', 'd'], 'a');
for (const c of clients.values()) {
expect(c.setups, c.name).toBe(1);
expect(c.closes, c.name).toBe(0);
}
});
it('gives the active login a live stream before the background ones', () => {
const { clients, run } = world(['a', 'b', 'c'], 1);
run(['b', 'c', 'a'], 'a');
expect(clients.get('a')!.hasSSEStream()).toBe(true);
expect(clients.get('b')!.hasSSEStream()).toBe(false);
});
it('unbinds only the login that went away', () => {
const { clients, bindings, run } = world(['a', 'b', 'c']);
run(['a', 'b', 'c'], 'a');
run(['a', 'c'], 'a');
expect(clients.get('b')!.closes).toBe(1);
expect(clients.get('a')!.closes).toBe(0);
expect(clients.get('c')!.closes).toBe(0);
expect([...bindings.values()].map((b) => b.accountId).sort()).toEqual(['a', 'c']);
});
it('on an account switch touches only the two logins whose role changed', () => {
const { clients, run } = world(['a', 'b', 'c', 'd'], 1);
run(['a', 'b', 'c', 'd'], 'a');
const setupsBefore = [...clients.values()].map((c) => c.setups);
run(['a', 'b', 'c', 'd'], 'c');
// c had no stream: it takes a's, and a is re-bound as background.
expect(clients.get('c')!.hasSSEStream()).toBe(true);
expect(clients.get('c')!.roles.at(-1)).toBe('active');
expect(clients.get('a')!.roles.at(-1)).toBe('background');
expect(clients.get('b')!.setups).toBe(setupsBefore[1]);
expect(clients.get('d')!.setups).toBe(setupsBefore[3]);
});
it('only swaps the callback when the newly active login already has a stream', () => {
const { clients, run } = world(['a', 'b'], 2);
run(['a', 'b'], 'a');
run(['a', 'b'], 'b');
expect(clients.get('b')!.setups).toBe(1);
expect(clients.get('b')!.roles).toEqual(['background', 'active']);
expect(clients.get('a')!.roles).toEqual(['active', 'background']);
});
it('rebinds a login whose client was replaced', () => {
const { bindings, run, clients } = world(['a']);
run(['a'], 'a');
const old = clients.get('a')!;
const fresh = new FakeClient('a', { free: 2 });
reconcilePushBindings({
bindings, clients: new Map([['a', fresh]]), activeAccountId: 'a', listen: vi.fn(),
});
expect(old.closes).toBe(1);
expect(fresh.setups).toBe(1);
});
it('releases everything on logout', () => {
const { clients, bindings, run } = world(['a', 'b']);
run(['a', 'b'], 'a');
releasePushBindings(bindings);
expect(bindings.size).toBe(0);
for (const c of clients.values()) expect(c.closes).toBe(1);
});
});