Runtime: dedupe typing lease logic

This commit is contained in:
Gustavo Madeira Santana
2026-03-30 00:57:33 -04:00
parent 73b128e37d
commit 0e078e8bc0
4 changed files with 134 additions and 55 deletions

View File

@@ -1,29 +1,39 @@
import { afterEach, describe, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createDiscordTypingLease,
type CreateDiscordTypingLeaseParams,
} from "./runtime-discord-typing.js";
import { registerSharedTypingLeaseTests } from "./typing-lease.test-support.js";
const DISCORD_TYPING_INTERVAL_MS = 2_000;
function buildDiscordTypingParams(
pulse: CreateDiscordTypingLeaseParams["pulse"],
): CreateDiscordTypingLeaseParams {
return {
channelId: "123",
intervalMs: DISCORD_TYPING_INTERVAL_MS,
pulse,
};
}
describe("createDiscordTypingLease", () => {
afterEach(() => {
vi.useRealTimers();
});
registerSharedTypingLeaseTests({
createLease: createDiscordTypingLease,
buildParams: buildDiscordTypingParams,
it("uses the Discord default interval and forwards pulse params", async () => {
vi.useFakeTimers();
const pulse: CreateDiscordTypingLeaseParams["pulse"] = vi.fn(async () => undefined);
const cfg = { channels: { discord: { token: "x" } } };
const lease = await createDiscordTypingLease({
channelId: "123",
accountId: "work",
cfg,
intervalMs: Number.NaN,
pulse,
});
expect(pulse).toHaveBeenCalledTimes(1);
expect(pulse).toHaveBeenCalledWith({
channelId: "123",
accountId: "work",
cfg,
});
await vi.advanceTimersByTimeAsync(7_999);
expect(pulse).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
expect(pulse).toHaveBeenCalledTimes(2);
lease.stop();
});
});

View File

@@ -1,4 +1,4 @@
import { logWarn } from "../../logger.js";
import { createTypingLease } from "./typing-lease.js";
export type CreateDiscordTypingLeaseParams = {
channelId: string;
@@ -18,45 +18,15 @@ export async function createDiscordTypingLease(params: CreateDiscordTypingLeaseP
refresh: () => Promise<void>;
stop: () => void;
}> {
const intervalMs =
typeof params.intervalMs === "number" && Number.isFinite(params.intervalMs)
? Math.max(1_000, Math.floor(params.intervalMs))
: DEFAULT_DISCORD_TYPING_INTERVAL_MS;
let stopped = false;
let timer: ReturnType<typeof setInterval> | null = null;
const pulse = async () => {
if (stopped) {
return;
}
await params.pulse({
return await createTypingLease({
defaultIntervalMs: DEFAULT_DISCORD_TYPING_INTERVAL_MS,
errorLabel: "discord",
intervalMs: params.intervalMs,
pulse: params.pulse,
pulseArgs: {
channelId: params.channelId,
accountId: params.accountId,
cfg: params.cfg,
});
};
await pulse();
timer = setInterval(() => {
// Background lease refreshes must never escape as unhandled rejections.
void pulse().catch((err) => {
logWarn(`plugins: discord typing pulse failed: ${String(err)}`);
});
}, intervalMs);
timer.unref?.();
return {
refresh: async () => {
await pulse();
},
stop: () => {
stopped = true;
if (timer) {
clearInterval(timer);
timer = null;
}
},
};
});
}

View File

@@ -0,0 +1,43 @@
import { afterEach, describe, it, vi } from "vitest";
import { createTypingLease } from "./typing-lease.js";
import {
expectDefaultTypingLeaseInterval,
registerSharedTypingLeaseTests,
} from "./typing-lease.test-support.js";
const TEST_TYPING_INTERVAL_MS = 2_000;
const TEST_TYPING_DEFAULT_INTERVAL_MS = 4_000;
function buildTypingLeaseParams(
pulse: (params: { target: string; lane?: string }) => Promise<unknown>,
) {
return {
defaultIntervalMs: TEST_TYPING_DEFAULT_INTERVAL_MS,
errorLabel: "test",
intervalMs: TEST_TYPING_INTERVAL_MS,
pulse,
pulseArgs: {
target: "target-1",
lane: "answer",
},
};
}
describe("createTypingLease", () => {
afterEach(() => {
vi.useRealTimers();
});
registerSharedTypingLeaseTests({
createLease: createTypingLease,
buildParams: buildTypingLeaseParams,
});
it("falls back to the default interval for non-finite values", async () => {
await expectDefaultTypingLeaseInterval({
createLease: createTypingLease,
buildParams: buildTypingLeaseParams,
defaultIntervalMs: TEST_TYPING_DEFAULT_INTERVAL_MS,
});
});
});

View File

@@ -0,0 +1,56 @@
import { logWarn } from "../../logger.js";
export type TypingLease = {
refresh: () => Promise<void>;
stop: () => void;
};
type CreateTypingLeaseParams<TPulseArgs> = {
defaultIntervalMs: number;
errorLabel: string;
intervalMs?: number;
pulse: (params: TPulseArgs) => Promise<unknown>;
pulseArgs: TPulseArgs;
};
export async function createTypingLease<TPulseArgs>(
params: CreateTypingLeaseParams<TPulseArgs>,
): Promise<TypingLease> {
const intervalMs =
typeof params.intervalMs === "number" && Number.isFinite(params.intervalMs)
? Math.max(1_000, Math.floor(params.intervalMs))
: params.defaultIntervalMs;
let stopped = false;
let timer: ReturnType<typeof setInterval> | null = null;
const pulse = async () => {
if (stopped) {
return;
}
await params.pulse(params.pulseArgs);
};
await pulse();
timer = setInterval(() => {
// Background lease refreshes must never escape as unhandled rejections.
void pulse().catch((err) => {
logWarn(`plugins: ${params.errorLabel} typing pulse failed: ${String(err)}`);
});
}, intervalMs);
timer.unref?.();
return {
refresh: async () => {
await pulse();
},
stop: () => {
stopped = true;
if (timer) {
clearInterval(timer);
timer = null;
}
},
};
}