CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 3m55s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 27s
CI / Playwright smoke (e2e) (pull_request) Successful in 33m26s
Synapse allows one presence write per 10 s per user (shared by every device). The heartbeat wrote presence at startup and on every tab switch, so a status save right after usually got a 429 and waited 7-25 s, with the whole status section (quick statuses, input, Clear) locked and the Save spinner going. - One shared presence queue (heartbeat + status save), newest write wins: changes made while a write waits out a 429 replace it, so only the latest is sent, as soon as the server allows. - The status UI no longer waits on the server: the status applies locally, Save switches off (it compared with the server echo, so it stayed lit and invited a second, guaranteed-429 write), the controls stay usable, and a short note shows until the write lands. - The heartbeat skips writes the server already reflects (e.g. "online" at startup), so it stops spending the budget for nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { IPresenceOpts, MatrixError } from 'matrix-js-sdk';
|
|
import {
|
|
PresenceDeduper,
|
|
describePresenceError,
|
|
isPresenceWritePending,
|
|
queuePresenceWrite,
|
|
setPresenceWithRetry,
|
|
} from './presenceWrite';
|
|
|
|
const limited = (retryMs = 3000) =>
|
|
new MatrixError(
|
|
{ errcode: 'M_LIMIT_EXCEEDED', error: 'Too Many Requests', retry_after_ms: retryMs },
|
|
429,
|
|
);
|
|
|
|
test('429 is retried after Retry-After, then succeeds', async () => {
|
|
let n = 0;
|
|
const waits: number[] = [];
|
|
const mx = {
|
|
setPresence: async () => {
|
|
n += 1;
|
|
if (n < 3) throw limited(3000);
|
|
return {};
|
|
},
|
|
};
|
|
await setPresenceWithRetry(
|
|
mx,
|
|
{ presence: 'online', status_msg: 'hi' },
|
|
{
|
|
sleepFn: async (ms) => {
|
|
waits.push(ms);
|
|
},
|
|
},
|
|
);
|
|
assert.equal(n, 3);
|
|
assert.deepEqual(waits, [3000, 3000]);
|
|
});
|
|
|
|
test('gives up once the total wait would exceed the cap; other errors are not retried', async () => {
|
|
const mx = {
|
|
setPresence: async () => {
|
|
throw limited(20_000);
|
|
},
|
|
};
|
|
await assert.rejects(
|
|
() =>
|
|
setPresenceWithRetry(
|
|
mx,
|
|
{ presence: 'online' },
|
|
{ maxWaitMs: 25_000, sleepFn: async () => undefined },
|
|
),
|
|
/Too Many/,
|
|
);
|
|
let calls = 0;
|
|
const forbidden = {
|
|
setPresence: async () => {
|
|
calls += 1;
|
|
throw new MatrixError({ errcode: 'M_FORBIDDEN', error: 'nope' }, 403);
|
|
},
|
|
};
|
|
await assert.rejects(() => setPresenceWithRetry(forbidden, { presence: 'online' }));
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
test('deduper skips identical writes and notices changes', () => {
|
|
const d = new PresenceDeduper();
|
|
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'a' }), true);
|
|
d.sent({ presence: 'online', status_msg: 'a' });
|
|
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'a' }), false);
|
|
assert.equal(d.shouldSend({ presence: 'unavailable', status_msg: 'a' }), true);
|
|
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'b' }), true);
|
|
d.reset();
|
|
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'a' }), true);
|
|
});
|
|
|
|
test('error wording', () => {
|
|
assert.match(describePresenceError(limited()), /once every 10 seconds/);
|
|
assert.equal(
|
|
describePresenceError(
|
|
new MatrixError({ errcode: 'M_FORBIDDEN', error: 'Presence is disabled' }, 403),
|
|
),
|
|
'Failed to save status: Presence is disabled',
|
|
);
|
|
assert.match(describePresenceError(new TypeError('Failed to fetch')), /couldn't reach/);
|
|
});
|
|
|
|
test('[status save] queuePresenceWrite: newest wins, waits out 429 once, all callers settle', async () => {
|
|
const sent: string[] = [];
|
|
let calls = 0;
|
|
const sleeps: number[] = [];
|
|
const mx = {
|
|
setPresence: async (w: IPresenceOpts) => {
|
|
calls += 1;
|
|
if (calls === 1) {
|
|
sent.push(`429:${w.status_msg}`);
|
|
throw new MatrixError({ errcode: 'M_LIMIT_EXCEEDED', retry_after_ms: 7000 }, 429);
|
|
}
|
|
sent.push(`ok:${w.status_msg}`);
|
|
},
|
|
};
|
|
const sleepFn = async (ms: number) => {
|
|
sleeps.push(ms);
|
|
};
|
|
// 'a' goes out at once; 'b' and 'c' arrive while it waits out the limit.
|
|
const first = queuePresenceWrite(mx, { presence: 'online', status_msg: 'a' }, { sleepFn });
|
|
const second = queuePresenceWrite(mx, { presence: 'online', status_msg: 'b' }, { sleepFn });
|
|
const third = queuePresenceWrite(mx, { presence: 'online', status_msg: 'c' }, { sleepFn });
|
|
assert.equal(isPresenceWritePending(mx), true);
|
|
await Promise.all([first, second, third]);
|
|
// 'a' was 429'd; after the wait only the newest ('c') goes out — 'a' isn't
|
|
// retried and 'b' is never sent.
|
|
assert.deepEqual(sent, ['429:a', 'ok:c']);
|
|
assert.deepEqual(sleeps, [7000]);
|
|
assert.equal(isPresenceWritePending(mx), false);
|
|
});
|
|
|
|
test('[status save] queuePresenceWrite: a failure rejects the callers of that write', async () => {
|
|
const mx = {
|
|
setPresence: async () => {
|
|
throw new MatrixError({ errcode: 'M_FORBIDDEN' }, 403);
|
|
},
|
|
};
|
|
await assert.rejects(queuePresenceWrite(mx, { presence: 'online', status_msg: 'x' }));
|
|
assert.equal(isPresenceWritePending(mx), false);
|
|
});
|