Skip to content

Commit bb0ded6

Browse files
vcarlclaude
andcommitted
Consolidate HMR state into hmrRegistry.ts
Move all HMR-related globals from scattered locations into a single hmrRegistry.ts module: - Login state (isLoginStarted/markLoginStarted) from gateway.ts - Client ready state (isClientReady/setClientReady) from client.server.ts - Scheduled tasks (registerScheduledTask/clearScheduledTasks) from client.server.ts - Listener registry (existing, renamed from listenerRegistry.ts) This centralizes HMR concerns and simplifies client.server.ts to just client instantiation and login. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 8557e11 commit bb0ded6

12 files changed

+111
-107
lines changed

app/discord/activityTracker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getMessageStats } from "#~/helpers/discord.js";
55
import { threadStats } from "#~/helpers/metrics";
66
import { log, trackPerformance } from "#~/helpers/observability";
77

8-
import { registerListener } from "./listenerRegistry";
8+
import { registerListener } from "./hmrRegistry";
99
import { getOrFetchChannel } from "./utils";
1010

1111
export async function startActivityTracking(client: Client) {

app/discord/automod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "#~/models/reportedMessages.server";
1717

1818
import { client } from "./client.server";
19-
import { registerListener } from "./listenerRegistry";
19+
import { registerListener } from "./hmrRegistry";
2020

2121
const AUTO_SPAM_THRESHOLD = 3;
2222

app/discord/client.server.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,6 @@ import { ReacordDiscordJs } from "reacord";
44
import { discordToken } from "#~/helpers/env.server";
55
import { log, trackPerformance } from "#~/helpers/observability";
66

7-
// HMR state helpers - persisted across module reloads
8-
declare global {
9-
var __discordClientReady: boolean | undefined;
10-
var __discordScheduledTasks: ReturnType<typeof setTimeout>[] | undefined;
11-
}
12-
13-
export function isClientReady(): boolean {
14-
return globalThis.__discordClientReady ?? false;
15-
}
16-
17-
export function setClientReady(): void {
18-
globalThis.__discordClientReady = true;
19-
}
20-
21-
export function registerScheduledTask(
22-
timer: ReturnType<typeof setTimeout>,
23-
): void {
24-
globalThis.__discordScheduledTasks ??= [];
25-
globalThis.__discordScheduledTasks.push(timer);
26-
}
27-
28-
export function clearScheduledTasks(): void {
29-
const tasks = globalThis.__discordScheduledTasks ?? [];
30-
if (tasks.length > 0) {
31-
log("info", "Client", `Clearing ${tasks.length} scheduled tasks for HMR`);
32-
}
33-
for (const timer of tasks) {
34-
clearTimeout(timer);
35-
clearInterval(timer);
36-
}
37-
globalThis.__discordScheduledTasks = [];
38-
}
39-
407
export const client = new Client({
418
intents: [
429
GatewayIntentBits.Guilds,

app/discord/deployCommands.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "discord.js";
1111

1212
import { ssrDiscordSdk } from "#~/discord/api";
13-
import { registerListener } from "#~/discord/listenerRegistry";
13+
import { registerListener } from "#~/discord/hmrRegistry";
1414
import {
1515
isMessageComponentCommand,
1616
isMessageContextCommand,

app/discord/escalationResolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from "discord.js";
1010

1111
import { tallyVotes } from "#~/commands/escalate/voting.js";
12-
import { registerScheduledTask } from "#~/discord/client.server";
12+
import { registerScheduledTask } from "#~/discord/hmrRegistry";
1313
import {
1414
humanReadableResolutions,
1515
resolutions,

app/discord/gateway.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@ import { Events } from "discord.js";
22

33
import { startActivityTracking } from "#~/discord/activityTracker";
44
import automod from "#~/discord/automod";
5-
import {
6-
clearScheduledTasks,
7-
client,
8-
isClientReady,
9-
login,
10-
setClientReady,
11-
} from "#~/discord/client.server";
5+
import { client, login } from "#~/discord/client.server";
126
import { deployCommands } from "#~/discord/deployCommands.server";
137
import { startEscalationResolver } from "#~/discord/escalationResolver";
148
import {
9+
clearScheduledTasks,
10+
isClientReady,
11+
isLoginStarted,
12+
markLoginStarted,
1513
registerListener,
1614
removeAllListeners,
17-
} from "#~/discord/listenerRegistry";
15+
setClientReady,
16+
} from "#~/discord/hmrRegistry";
1817
import modActionLogger from "#~/discord/modActionLogger";
1918
import onboardGuild from "#~/discord/onboardGuild";
2019
import { startReactjiChanneler } from "#~/discord/reactjiChanneler";
@@ -24,11 +23,6 @@ import Sentry from "#~/helpers/sentry.server";
2423

2524
import { startHoneypotTracking } from "./honeypotTracker";
2625

27-
// Track if login has been initiated to prevent duplicate logins during HMR
28-
declare global {
29-
var __discordLoginStarted: boolean | undefined;
30-
}
31-
3226
/**
3327
* Initialize all sub-modules that depend on the client being ready.
3428
*/
@@ -71,9 +65,9 @@ async function initializeSubModules() {
7165

7266
export default function init() {
7367
// Login only happens once - persists across HMR
74-
if (!globalThis.__discordLoginStarted) {
68+
if (!isLoginStarted()) {
7569
log("info", "Gateway", "Initializing Discord gateway (first time)");
76-
globalThis.__discordLoginStarted = true;
70+
markLoginStarted();
7771
void login();
7872

7973
// Set ready state when ClientReady fires (only needs to happen once)

app/discord/hmrRegistry.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { Client, ClientEvents } from "discord.js";
2+
3+
import { log } from "#~/helpers/observability";
4+
5+
// All HMR-related global state declarations
6+
declare global {
7+
var __discordListenerRegistry:
8+
| { event: string; listener: (...args: unknown[]) => void }[]
9+
| undefined;
10+
var __discordScheduledTasks: ReturnType<typeof setTimeout>[] | undefined;
11+
var __discordClientReady: boolean | undefined;
12+
var __discordLoginStarted: boolean | undefined;
13+
}
14+
15+
// --- Login state ---
16+
17+
export function isLoginStarted(): boolean {
18+
return globalThis.__discordLoginStarted ?? false;
19+
}
20+
21+
export function markLoginStarted(): void {
22+
globalThis.__discordLoginStarted = true;
23+
}
24+
25+
// --- Client ready state ---
26+
27+
export function isClientReady(): boolean {
28+
return globalThis.__discordClientReady ?? false;
29+
}
30+
31+
export function setClientReady(): void {
32+
globalThis.__discordClientReady = true;
33+
}
34+
35+
// --- Scheduled tasks ---
36+
37+
export function registerScheduledTask(
38+
timer: ReturnType<typeof setTimeout>,
39+
): void {
40+
globalThis.__discordScheduledTasks ??= [];
41+
globalThis.__discordScheduledTasks.push(timer);
42+
}
43+
44+
export function clearScheduledTasks(): void {
45+
const tasks = globalThis.__discordScheduledTasks ?? [];
46+
if (tasks.length > 0) {
47+
log("info", "HMR", `Clearing ${tasks.length} scheduled tasks`);
48+
}
49+
for (const timer of tasks) {
50+
clearTimeout(timer);
51+
clearInterval(timer);
52+
}
53+
globalThis.__discordScheduledTasks = [];
54+
}
55+
56+
// --- Listener registry ---
57+
58+
/**
59+
* Register a listener with the Discord client and track it for HMR cleanup.
60+
*/
61+
export function registerListener<K extends keyof ClientEvents>(
62+
client: Client,
63+
event: K,
64+
listener: (...args: ClientEvents[K]) => void,
65+
): void {
66+
globalThis.__discordListenerRegistry ??= [];
67+
client.on(event, listener);
68+
globalThis.__discordListenerRegistry.push({
69+
event,
70+
listener: listener as (...args: unknown[]) => void,
71+
});
72+
}
73+
74+
/**
75+
* Remove all tracked listeners from the client.
76+
* Call this before rebinding listeners on HMR.
77+
*/
78+
export function removeAllListeners(client: Client): void {
79+
const registry = globalThis.__discordListenerRegistry ?? [];
80+
if (registry.length > 0) {
81+
log("info", "HMR", `Removing ${registry.length} listeners for HMR`);
82+
}
83+
for (const { event, listener } of registry) {
84+
client.off(event, listener);
85+
}
86+
globalThis.__discordListenerRegistry = [];
87+
}
88+
89+
/**
90+
* Get the count of currently registered listeners.
91+
*/
92+
export function getListenerCount(): number {
93+
return globalThis.__discordListenerRegistry?.length ?? 0;
94+
}

app/discord/honeypotTracker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { log } from "#~/helpers/observability";
77
import { fetchSettings, SETTINGS } from "#~/models/guilds.server.js";
88
import { ReportReasons } from "#~/models/reportedMessages.server.js";
99

10-
import { registerListener } from "./listenerRegistry";
10+
import { registerListener } from "./hmrRegistry";
1111

1212
interface HoneypotConfig {
1313
guild_id: string;

app/discord/listenerRegistry.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

app/discord/modActionLogger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { reportModAction, type ModActionReport } from "#~/helpers/modLog";
1414
import { log } from "#~/helpers/observability";
1515

16-
import { registerListener } from "./listenerRegistry";
16+
import { registerListener } from "./hmrRegistry";
1717

1818
// Time window to check audit log for matching entries (5 seconds)
1919
const AUDIT_LOG_WINDOW_MS = 5000;

0 commit comments

Comments
 (0)