refactor: unify monitor abort lifecycle handling

This commit is contained in:
Peter Steinberger
2026-02-26 04:36:00 +01:00
parent 02c731826a
commit e915b4c64a
13 changed files with 319 additions and 103 deletions

View File

@@ -0,0 +1,33 @@
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
import { attachEarlyGatewayErrorGuard } from "./gateway-error-guard.js";
describe("attachEarlyGatewayErrorGuard", () => {
it("captures gateway errors until released", () => {
const emitter = new EventEmitter();
const fallbackErrorListener = vi.fn();
emitter.on("error", fallbackErrorListener);
const client = {
getPlugin: vi.fn(() => ({ emitter })),
};
const guard = attachEarlyGatewayErrorGuard(client as never);
emitter.emit("error", new Error("Fatal Gateway error: 4014"));
expect(guard.pendingErrors).toHaveLength(1);
guard.release();
emitter.emit("error", new Error("Fatal Gateway error: 4000"));
expect(guard.pendingErrors).toHaveLength(1);
expect(fallbackErrorListener).toHaveBeenCalledTimes(2);
});
it("returns noop guard when gateway emitter is unavailable", () => {
const client = {
getPlugin: vi.fn(() => undefined),
};
const guard = attachEarlyGatewayErrorGuard(client as never);
expect(guard.pendingErrors).toEqual([]);
expect(() => guard.release()).not.toThrow();
});
});

View File

@@ -0,0 +1,36 @@
import type { Client } from "@buape/carbon";
import { getDiscordGatewayEmitter } from "../monitor.gateway.js";
export type EarlyGatewayErrorGuard = {
pendingErrors: unknown[];
release: () => void;
};
export function attachEarlyGatewayErrorGuard(client: Client): EarlyGatewayErrorGuard {
const pendingErrors: unknown[] = [];
const gateway = client.getPlugin("gateway");
const emitter = getDiscordGatewayEmitter(gateway);
if (!emitter) {
return {
pendingErrors,
release: () => {},
};
}
let released = false;
const onGatewayError = (err: unknown) => {
pendingErrors.push(err);
};
emitter.on("error", onGatewayError);
return {
pendingErrors,
release: () => {
if (released) {
return;
}
released = true;
emitter.removeListener("error", onGatewayError);
},
};
}

View File

@@ -34,7 +34,6 @@ import { createDiscordRetryRunner } from "../../infra/retry-policy.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js";
import { resolveDiscordAccount } from "../accounts.js";
import { getDiscordGatewayEmitter } from "../monitor.gateway.js";
import { fetchDiscordApplicationId } from "../probe.js";
import { normalizeDiscordToken } from "../token.js";
import { createDiscordVoiceCommand } from "../voice/command.js";
@@ -52,6 +51,7 @@ import {
} from "./agent-components.js";
import { resolveDiscordSlashCommandConfig } from "./commands.js";
import { createExecApprovalButton, DiscordExecApprovalHandler } from "./exec-approvals.js";
import { attachEarlyGatewayErrorGuard } from "./gateway-error-guard.js";
import { createDiscordGatewayPlugin } from "./gateway-plugin.js";
import {
DiscordMessageListener,
@@ -230,33 +230,6 @@ function isDiscordDisallowedIntentsError(err: unknown): boolean {
return message.includes(String(DISCORD_DISALLOWED_INTENTS_CODE));
}
type EarlyGatewayErrorGuard = {
pendingErrors: unknown[];
release: () => void;
};
function attachEarlyGatewayErrorGuard(client: Client): EarlyGatewayErrorGuard {
const pendingErrors: unknown[] = [];
const gateway = client.getPlugin<GatewayPlugin>("gateway");
const emitter = getDiscordGatewayEmitter(gateway);
if (!emitter) {
return {
pendingErrors,
release: () => {},
};
}
const onGatewayError = (err: unknown) => {
pendingErrors.push(err);
};
emitter.on("error", onGatewayError);
return {
pendingErrors,
release: () => {
emitter.removeListener("error", onGatewayError);
},
};
}
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const cfg = opts.config ?? loadConfig();
const account = resolveDiscordAccount({