diff --git a/tests/persona-matrix-mouth.test.mjs b/tests/persona-matrix-mouth.test.mjs index 094095a..9245f9d 100644 --- a/tests/persona-matrix-mouth.test.mjs +++ b/tests/persona-matrix-mouth.test.mjs @@ -1035,6 +1035,7 @@ test("presence follows the brake: online while released, offline and one PUT on await mouth.syncOnce(); await mouth.syncOnce(); await mouth.syncOnce(); assert.deepEqual(syncs().slice(2), ["offline", "offline", "offline"]); assert.deepEqual(hs.presencePuts().map(r => [r.path, r.body]), [[`/_matrix/client/v3/presence/${SELF}/status`, { presence: "offline" }]], "exactly one PUT"); + assert.equal(hs.presencePuts()[0].rawPath, `/_matrix/client/v3/presence/${encodeURIComponent(SELF)}/status`, "the user id is percent-encoded on the wire"); open = true; await mouth.syncOnce(); assert.equal(syncs().at(-1), "online"); @@ -1151,3 +1152,18 @@ test("a full backlog still sets presence from the brake", async () => { assert.equal(hs.seen.filter(r => r.path.endsWith("/sync")).length, syncsBefore); assert.deepEqual(hs.presencePuts().map(r => r.body), [{ presence: "offline" }]); }); + +test("a brake placed during the first of two joins stops the second join and its m.direct write", async () => { + const DM2 = "!newdm2:gamecult.test"; + const invites2 = { ...invite(`@meta:${HS_NAME}`), [DM2]: invite(`@meta:${HS_NAME}`)[DMROOM] }; + const hs = await fakeHomeserver({ sync: since => since === null ? { next_batch: "b0" } : since === "b0" ? { next_batch: "b1", rooms: { invite: invites2 } } : { next_batch: since } }); + let open = true; + const fetch = async (url, init) => { const res = await hs.fetch(url, init); if (init?.method === "POST" && String(url).includes("/join/")) open = false; return res; }; // the brake lands during the first join + const mouth = mouthFor(hs, { fetch, brake: async () => open }); + await mouth.syncOnce(); await mouth.syncOnce(); + assert.deepEqual(joinsOf(hs), [DMROOM], "only the join already started"); + assert.equal(hs.seen.filter(r => r.method === "PUT" && r.path.endsWith("/account_data/m.direct")).length, 1, "one m.direct, for the first join"); + open = true; + await mouth.syncOnce(); + assert.deepEqual(joinsOf(hs), [DMROOM, DM2], "the second waits for the release"); +}); diff --git a/tools/persona-matrix-mouth.mjs b/tools/persona-matrix-mouth.mjs index 5b7d4ef..21f1c7c 100644 --- a/tools/persona-matrix-mouth.mjs +++ b/tools/persona-matrix-mouth.mjs @@ -164,7 +164,7 @@ export function createMouth({ // A DM invitation from an account on our own homeserver is accepted, and only while the brake is // released; one received while braked waits for the release. An invitation from any other server is // declined. A same-server invitation to a room that is not a DM is not ours to accept and is dropped. - async function answerInvites(free) { + async function answerInvites() { for (const [roomId, invite] of [...invites]) { try { const room = encodeURIComponent(roomId); @@ -174,7 +174,7 @@ export function createMouth({ } else if (!invite.direct) { invites.delete(roomId); } else { - if (!free) continue; + if (!(await released())) continue; // read fresh before every join: a brake placed mid-pass stops the next one await matrix("POST", `/join/${room}`, {}); invites.delete(roomId); directRooms.add(roomId); @@ -299,7 +299,7 @@ export function createMouth({ const baseline = cursor === null; // first ever sync: history is context, not mail for (const event of reply.account_data?.events ?? []) if (event.type === "m.direct") noteDirect(event.content); noteInvites(reply.rooms?.invite); - await answerInvites(await released()); // a brake placed during the long-poll stops the join, as send re-reads it + await answerInvites(); // each join re-reads the brake, as send does const made = []; const round = new Map(); let truncated = false; // the pending cap was reached inside this round