refactor(matrix,tests): extract helpers and inject send-queue timing

This commit is contained in:
Peter Steinberger
2026-02-24 23:37:45 +00:00
parent a2529c25ff
commit 58309fd8d9
5 changed files with 108 additions and 46 deletions

View File

@@ -5,6 +5,32 @@ import { sendReadReceiptMatrix } from "../send.js";
import type { MatrixRawEvent } from "./types.js";
import { EventType } from "./types.js";
function createSelfUserIdResolver(client: Pick<MatrixClient, "getUserId">) {
let selfUserId: string | undefined;
let selfUserIdLookup: Promise<string | undefined> | undefined;
return async (): Promise<string | undefined> => {
if (selfUserId) {
return selfUserId;
}
if (!selfUserIdLookup) {
selfUserIdLookup = client
.getUserId()
.then((userId) => {
selfUserId = userId;
return userId;
})
.catch(() => undefined)
.finally(() => {
if (!selfUserId) {
selfUserIdLookup = undefined;
}
});
}
return await selfUserIdLookup;
};
}
export function registerMatrixMonitorEvents(params: {
client: MatrixClient;
auth: MatrixAuth;
@@ -26,28 +52,7 @@ export function registerMatrixMonitorEvents(params: {
onRoomMessage,
} = params;
let selfUserId: string | undefined;
let selfUserIdLookup: Promise<string | undefined> | undefined;
const resolveSelfUserId = async (): Promise<string | undefined> => {
if (selfUserId) {
return selfUserId;
}
if (!selfUserIdLookup) {
selfUserIdLookup = client
.getUserId()
.then((userId) => {
selfUserId = userId;
return userId;
})
.catch(() => undefined)
.finally(() => {
if (!selfUserId) {
selfUserIdLookup = undefined;
}
});
}
return await selfUserIdLookup;
};
const resolveSelfUserId = createSelfUserIdResolver(client);
client.on("room.message", (roomId: string, event: MatrixRawEvent) => {
const eventId = event?.event_id;
const senderId = event?.sender;

View File

@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { enqueueSend } from "./send-queue.js";
import { DEFAULT_SEND_GAP_MS, enqueueSend } from "./send-queue.js";
function deferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
@@ -36,15 +36,15 @@ describe("enqueueSend", () => {
return "two";
});
await vi.advanceTimersByTimeAsync(150);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS);
expect(events).toEqual(["start1"]);
await vi.advanceTimersByTimeAsync(300);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS * 2);
expect(events).toEqual(["start1"]);
gate.resolve();
await first;
await vi.advanceTimersByTimeAsync(149);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS - 1);
expect(events).toEqual(["start1", "end1"]);
await vi.advanceTimersByTimeAsync(1);
await second;
@@ -63,7 +63,7 @@ describe("enqueueSend", () => {
return "b";
});
await vi.advanceTimersByTimeAsync(150);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS);
await Promise.all([a, b]);
expect(events.sort()).toEqual(["a", "b"]);
});
@@ -76,14 +76,14 @@ describe("enqueueSend", () => {
(error) => ({ ok: false as const, error }),
);
await vi.advanceTimersByTimeAsync(150);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS);
const firstResult = await first;
expect(firstResult.ok).toBe(false);
expect(firstResult.error).toBeInstanceOf(Error);
expect((firstResult.error as Error).message).toBe("boom");
const second = enqueueSend("!room:example.org", async () => "ok");
await vi.advanceTimersByTimeAsync(150);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS);
await expect(second).resolves.toBe("ok");
});
@@ -104,7 +104,7 @@ describe("enqueueSend", () => {
return "two";
});
await vi.advanceTimersByTimeAsync(150);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS);
expect(events).toEqual(["start1"]);
gate.resolve();
@@ -112,8 +112,37 @@ describe("enqueueSend", () => {
expect(firstResult.ok).toBe(false);
expect(firstResult.error).toBeInstanceOf(Error);
await vi.advanceTimersByTimeAsync(150);
await vi.advanceTimersByTimeAsync(DEFAULT_SEND_GAP_MS);
await expect(second).resolves.toBe("two");
expect(events).toEqual(["start1", "start2"]);
});
it("supports custom gap and delay injection", async () => {
const events: string[] = [];
const delayFn = vi.fn(async (_ms: number) => {});
const first = enqueueSend(
"!room:example.org",
async () => {
events.push("first");
return "one";
},
{ gapMs: 7, delayFn },
);
const second = enqueueSend(
"!room:example.org",
async () => {
events.push("second");
return "two";
},
{ gapMs: 7, delayFn },
);
await expect(first).resolves.toBe("one");
await expect(second).resolves.toBe("two");
expect(events).toEqual(["first", "second"]);
expect(delayFn).toHaveBeenCalledTimes(2);
expect(delayFn).toHaveBeenNthCalledWith(1, 7);
expect(delayFn).toHaveBeenNthCalledWith(2, 7);
});
});

View File

@@ -1,15 +1,26 @@
const SEND_GAP_MS = 150;
export const DEFAULT_SEND_GAP_MS = 150;
type MatrixSendQueueOptions = {
gapMs?: number;
delayFn?: (ms: number) => Promise<void>;
};
// Serialize sends per room to preserve Matrix delivery order.
const roomQueues = new Map<string, Promise<void>>();
export async function enqueueSend<T>(roomId: string, fn: () => Promise<T>): Promise<T> {
export async function enqueueSend<T>(
roomId: string,
fn: () => Promise<T>,
options?: MatrixSendQueueOptions,
): Promise<T> {
const gapMs = options?.gapMs ?? DEFAULT_SEND_GAP_MS;
const delayFn = options?.delayFn ?? delay;
const previous = roomQueues.get(roomId) ?? Promise.resolve();
const next = previous
.catch(() => {})
.then(async () => {
await delay(SEND_GAP_MS);
await delayFn(gapMs);
return await fn();
});