mirror of
https://github.com/discordeno/discordeno.git
synced 2026-06-16 11:28:15 +00:00
fix(controllers/READY): reimplement guild cache mechanism (#647)
* add lastShardID * fix ready event controller * forgot to push this file * move ready to its own file * some changes * Update READY.ts * some changes idk if they are good * Update options.ts * Update READY.ts * Update guilds.ts
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
eventHandlers,
|
||||||
|
lastShardID,
|
||||||
|
setApplicationID,
|
||||||
|
setBotID,
|
||||||
|
} from "../../bot.ts";
|
||||||
|
import { DiscordPayload, ReadyPayload } from "../../types/discord.ts";
|
||||||
|
import { cache } from "../../util/cache.ts";
|
||||||
|
import { delay } from "../../util/utils.ts";
|
||||||
|
import { allowNextShard, basicShards } from "../../ws/mod.ts";
|
||||||
|
import { initialMemberLoadQueue } from "../structures/guild.ts";
|
||||||
|
import { structures } from "../structures/mod.ts";
|
||||||
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
|
/** This function is the internal handler for the ready event. Users can override this with controllers if desired. */
|
||||||
|
export async function handleInternalReady(
|
||||||
|
data: DiscordPayload,
|
||||||
|
shardID: number,
|
||||||
|
) {
|
||||||
|
// The bot has already started, the last shard is resumed, however.
|
||||||
|
if (cache.isReady) return;
|
||||||
|
|
||||||
|
const payload = data.d as ReadyPayload;
|
||||||
|
setBotID(payload.user.id);
|
||||||
|
setApplicationID(payload.application.id);
|
||||||
|
|
||||||
|
// Triggered on each shard
|
||||||
|
eventHandlers.shardReady?.(shardID);
|
||||||
|
// Save when the READY event was received to prevent infinite load loops
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const shard = basicShards.get(shardID);
|
||||||
|
if (!shard) return;
|
||||||
|
|
||||||
|
// Set ready to false just to go sure
|
||||||
|
shard.ready = false;
|
||||||
|
// All guilds are unavailable at first
|
||||||
|
shard.unavailableGuildIDs = new Set(payload.guilds.map((g) => g.id));
|
||||||
|
|
||||||
|
// Start ready check in 2 seconds
|
||||||
|
setTimeout(() => checkReady(payload, shardID, now), 2000);
|
||||||
|
|
||||||
|
// Wait 5 seconds to spawn next shard
|
||||||
|
await delay(5000);
|
||||||
|
allowNextShard();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't pass the shard itself because unavailableGuilds won't be updated by the GUILD_CREATE event
|
||||||
|
/** This function checks if the shard is fully loaded */
|
||||||
|
function checkReady(payload: ReadyPayload, shardID: number, now: number) {
|
||||||
|
const shard = basicShards.get(shardID);
|
||||||
|
if (!shard) return;
|
||||||
|
|
||||||
|
// Check if all guilds were loaded
|
||||||
|
if (shard.unavailableGuildIDs.size) {
|
||||||
|
if (Date.now() - now > 10000) {
|
||||||
|
eventHandlers.shardFailedToLoad?.(shardID, shard.unavailableGuildIDs);
|
||||||
|
// Force execute the loaded function to prevent infinite loop
|
||||||
|
loaded(shardID);
|
||||||
|
} else {
|
||||||
|
// Not all guilds were loaded but 10 seconds haven't passed so check again
|
||||||
|
setTimeout(() => checkReady(payload, shardID, now), 2000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// All guilds were loaded
|
||||||
|
loaded(shardID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loaded(shardID: number) {
|
||||||
|
const shard = basicShards.get(shardID);
|
||||||
|
if (!shard) return;
|
||||||
|
|
||||||
|
shard.ready = true;
|
||||||
|
|
||||||
|
// If it is the last shard we can go full ready
|
||||||
|
if (shardID === lastShardID - 1) {
|
||||||
|
// Still some shards are loading so wait another 2 seconds for them
|
||||||
|
if (basicShards.some((shard) => !shard.ready)) {
|
||||||
|
setTimeout(() => loaded(shardID), 2000);
|
||||||
|
} else {
|
||||||
|
cache.isReady = true;
|
||||||
|
eventHandlers.ready?.();
|
||||||
|
|
||||||
|
// All the members that came in on guild creates should now be processed 1 by 1
|
||||||
|
for (const [guildID, members] of initialMemberLoadQueue.entries()) {
|
||||||
|
await Promise.allSettled(
|
||||||
|
members.map(async (member) => {
|
||||||
|
const memberStruct = await structures.createMemberStruct(
|
||||||
|
member,
|
||||||
|
guildID,
|
||||||
|
);
|
||||||
|
|
||||||
|
return cacheHandlers.set(
|
||||||
|
"members",
|
||||||
|
memberStruct.id,
|
||||||
|
memberStruct,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
} from "../../types/mod.ts";
|
} from "../../types/mod.ts";
|
||||||
import { cache } from "../../util/cache.ts";
|
import { cache } from "../../util/cache.ts";
|
||||||
import { Collection } from "../../util/collection.ts";
|
import { Collection } from "../../util/collection.ts";
|
||||||
import { Member, structures } from "../structures/mod.ts";
|
import { basicShards } from "../../ws/mod.ts";
|
||||||
|
import { structures } from "../structures/mod.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
export async function handleInternalGuildCreate(
|
export async function handleInternalGuildCreate(
|
||||||
@@ -26,15 +27,22 @@ export async function handleInternalGuildCreate(
|
|||||||
);
|
);
|
||||||
await cacheHandlers.set("guilds", guildStruct.id, guildStruct);
|
await cacheHandlers.set("guilds", guildStruct.id, guildStruct);
|
||||||
|
|
||||||
if (await cacheHandlers.has("unavailableGuilds", payload.id)) {
|
const shard = basicShards.get(shardID);
|
||||||
|
|
||||||
|
if (shard?.unavailableGuildIDs.has(payload.id)) {
|
||||||
await cacheHandlers.delete("unavailableGuilds", payload.id);
|
await cacheHandlers.delete("unavailableGuilds", payload.id);
|
||||||
|
|
||||||
|
shard.unavailableGuildIDs.delete(payload.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cache.isReady) return eventHandlers.guildLoaded?.(guildStruct);
|
if (!cache.isReady) return eventHandlers.guildLoaded?.(guildStruct);
|
||||||
eventHandlers.guildCreate?.(guildStruct);
|
eventHandlers.guildCreate?.(guildStruct);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleInternalGuildDelete(data: DiscordPayload) {
|
export async function handleInternalGuildDelete(
|
||||||
|
data: DiscordPayload,
|
||||||
|
shardID: number,
|
||||||
|
) {
|
||||||
const payload = data.d as GuildDeletePayload;
|
const payload = data.d as GuildDeletePayload;
|
||||||
cacheHandlers.forEach("messages", (message) => {
|
cacheHandlers.forEach("messages", (message) => {
|
||||||
if (message.guildID === payload.id) {
|
if (message.guildID === payload.id) {
|
||||||
@@ -62,6 +70,9 @@ export async function handleInternalGuildDelete(data: DiscordPayload) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (payload.unavailable) {
|
if (payload.unavailable) {
|
||||||
|
const shard = basicShards.get(shardID);
|
||||||
|
if (shard) shard.unavailableGuildIDs.add(payload.id);
|
||||||
|
|
||||||
return cacheHandlers.set("unavailableGuilds", payload.id, Date.now());
|
return cacheHandlers.set("unavailableGuilds", payload.id, Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { eventHandlers, setApplicationID, setBotID } from "../../bot.ts";
|
import { eventHandlers } from "../../bot.ts";
|
||||||
import {
|
import {
|
||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
IntegrationCreateUpdateEvent,
|
IntegrationCreateUpdateEvent,
|
||||||
@@ -6,76 +6,14 @@ import {
|
|||||||
InviteCreateEvent,
|
InviteCreateEvent,
|
||||||
InviteDeleteEvent,
|
InviteDeleteEvent,
|
||||||
PresenceUpdatePayload,
|
PresenceUpdatePayload,
|
||||||
ReadyPayload,
|
|
||||||
TypingStartPayload,
|
TypingStartPayload,
|
||||||
UserPayload,
|
UserPayload,
|
||||||
VoiceStateUpdatePayload,
|
VoiceStateUpdatePayload,
|
||||||
WebhookUpdatePayload,
|
WebhookUpdatePayload,
|
||||||
} from "../../types/mod.ts";
|
} from "../../types/mod.ts";
|
||||||
import { cache } from "../../util/cache.ts";
|
|
||||||
import { delay } from "../../util/utils.ts";
|
|
||||||
import { allowNextShard } from "../../ws/shard_manager.ts";
|
|
||||||
import { initialMemberLoadQueue } from "../structures/guild.ts";
|
|
||||||
import { structures } from "../structures/mod.ts";
|
import { structures } from "../structures/mod.ts";
|
||||||
import { cacheHandlers } from "./cache.ts";
|
import { cacheHandlers } from "./cache.ts";
|
||||||
|
|
||||||
/** This function is the internal handler for the ready event. Users can override this with controllers if desired. */
|
|
||||||
export async function handleInternalReady(
|
|
||||||
data: DiscordPayload,
|
|
||||||
shardID: number,
|
|
||||||
) {
|
|
||||||
const payload = data.d as ReadyPayload;
|
|
||||||
setBotID(payload.user.id);
|
|
||||||
setApplicationID(payload.application.id);
|
|
||||||
|
|
||||||
// Triggered on each shard
|
|
||||||
eventHandlers.shardReady?.(shardID);
|
|
||||||
if (payload.shard && shardID === payload.shard[1] - 1) {
|
|
||||||
const loadedAllGuilds = async () => {
|
|
||||||
const guildsMissing = async () => {
|
|
||||||
for (const g of payload.guilds) {
|
|
||||||
if (!(await cacheHandlers.has("guilds", g.id))) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (await guildsMissing()) {
|
|
||||||
setTimeout(loadedAllGuilds, 2000);
|
|
||||||
} else {
|
|
||||||
// The bot has already started, the last shard is resumed, however.
|
|
||||||
if (cache.isReady) return;
|
|
||||||
|
|
||||||
cache.isReady = true;
|
|
||||||
eventHandlers.ready?.();
|
|
||||||
|
|
||||||
// All the members that came in on guild creates should now be processed 1 by 1
|
|
||||||
for (const [guildID, members] of initialMemberLoadQueue.entries()) {
|
|
||||||
await Promise.all(
|
|
||||||
members.map(async (member) => {
|
|
||||||
const memberStruct = await structures.createMemberStruct(
|
|
||||||
member,
|
|
||||||
guildID,
|
|
||||||
);
|
|
||||||
|
|
||||||
return cacheHandlers.set(
|
|
||||||
"members",
|
|
||||||
memberStruct.id,
|
|
||||||
memberStruct,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(loadedAllGuilds, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait 5 seconds to spawn next shard
|
|
||||||
await delay(5000);
|
|
||||||
allowNextShard();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This function is the internal handler for the presence update event. Users can override this with controllers if desired. */
|
/** This function is the internal handler for the presence update event. Users can override this with controllers if desired. */
|
||||||
export async function handleInternalPresenceUpdate(data: DiscordPayload) {
|
export async function handleInternalPresenceUpdate(data: DiscordPayload) {
|
||||||
const payload = data.d as PresenceUpdatePayload;
|
const payload = data.d as PresenceUpdatePayload;
|
||||||
@@ -233,7 +171,7 @@ export function handleInternalIntegrationDelete(data: DiscordPayload) {
|
|||||||
|
|
||||||
export function handleInternalInviteCreate(payload: DiscordPayload) {
|
export function handleInternalInviteCreate(payload: DiscordPayload) {
|
||||||
if (payload.t !== "INVITE_CREATE") return;
|
if (payload.t !== "INVITE_CREATE") return;
|
||||||
|
//TODO: replace with tocamelcase
|
||||||
const {
|
const {
|
||||||
channel_id: channelID,
|
channel_id: channelID,
|
||||||
created_at: createdAt,
|
created_at: createdAt,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import {
|
|||||||
handleInternalInviteCreate,
|
handleInternalInviteCreate,
|
||||||
handleInternalInviteDelete,
|
handleInternalInviteDelete,
|
||||||
handleInternalPresenceUpdate,
|
handleInternalPresenceUpdate,
|
||||||
handleInternalReady,
|
|
||||||
handleInternalTypingStart,
|
handleInternalTypingStart,
|
||||||
handleInternalUserUpdate,
|
handleInternalUserUpdate,
|
||||||
handleInternalVoiceStateUpdate,
|
handleInternalVoiceStateUpdate,
|
||||||
@@ -52,6 +51,7 @@ import {
|
|||||||
handleInternalMessageReactionRemoveAll,
|
handleInternalMessageReactionRemoveAll,
|
||||||
handleInternalMessageReactionRemoveEmoji,
|
handleInternalMessageReactionRemoveEmoji,
|
||||||
} from "./reactions.ts";
|
} from "./reactions.ts";
|
||||||
|
import { handleInternalReady } from "./READY.ts";
|
||||||
import {
|
import {
|
||||||
handleInternalGuildRoleCreate,
|
handleInternalGuildRoleCreate,
|
||||||
handleInternalGuildRoleDelete,
|
handleInternalGuildRoleDelete,
|
||||||
|
|||||||
+4
-2
@@ -18,6 +18,7 @@ export let eventHandlers: EventHandlers = {};
|
|||||||
|
|
||||||
export let botGatewayData: DiscordBotGatewayData;
|
export let botGatewayData: DiscordBotGatewayData;
|
||||||
export let proxyWSURL = `wss://gateway.discord.gg`;
|
export let proxyWSURL = `wss://gateway.discord.gg`;
|
||||||
|
export let lastShardID = 0;
|
||||||
|
|
||||||
export const identifyPayload: DiscordIdentify = {
|
export const identifyPayload: DiscordIdentify = {
|
||||||
token: "",
|
token: "",
|
||||||
@@ -60,9 +61,10 @@ export async function startBot(config: BotConfig) {
|
|||||||
(bits, next) => (bits |= typeof next === "string" ? Intents[next] : next),
|
(bits, next) => (bits |= typeof next === "string" ? Intents[next] : next),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
identifyPayload.shard = [0, botGatewayData.shards];
|
lastShardID = botGatewayData.shards;
|
||||||
|
identifyPayload.shard = [0, lastShardID];
|
||||||
|
|
||||||
await spawnShards(botGatewayData, identifyPayload, 0, botGatewayData.shards);
|
await spawnShards(botGatewayData, identifyPayload, 0, lastShardID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Allows you to dynamically update the event handlers by passing in new eventHandlers */
|
/** Allows you to dynamically update the event handlers by passing in new eventHandlers */
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ export interface EventHandlers {
|
|||||||
roleGained?: (guild: Guild, member: Member, roleID: string) => unknown;
|
roleGained?: (guild: Guild, member: Member, roleID: string) => unknown;
|
||||||
roleLost?: (guild: Guild, member: Member, roleID: string) => unknown;
|
roleLost?: (guild: Guild, member: Member, roleID: string) => unknown;
|
||||||
shardReady?: (shardID: number) => unknown;
|
shardReady?: (shardID: number) => unknown;
|
||||||
|
shardFailedToLoad?: (
|
||||||
|
shardID: number,
|
||||||
|
guildIDs: Set<string>,
|
||||||
|
) => unknown;
|
||||||
/** Sent when a user starts typing in a channel. */
|
/** Sent when a user starts typing in a channel. */
|
||||||
typingStart?: (data: TypingStartPayload) => unknown;
|
typingStart?: (data: TypingStartPayload) => unknown;
|
||||||
voiceChannelJoin?: (member: Member, channelID: string) => unknown;
|
voiceChannelJoin?: (member: Member, channelID: string) => unknown;
|
||||||
|
|||||||
+5
-2
@@ -6,13 +6,12 @@ import {
|
|||||||
DiscordPayload,
|
DiscordPayload,
|
||||||
FetchMembersOptions,
|
FetchMembersOptions,
|
||||||
GatewayOpcode,
|
GatewayOpcode,
|
||||||
GatewayStatusUpdatePayload,
|
|
||||||
ReadyPayload,
|
ReadyPayload,
|
||||||
} from "../types/mod.ts";
|
} from "../types/mod.ts";
|
||||||
|
import { Collection } from "../util/collection.ts";
|
||||||
import { delay } from "../util/utils.ts";
|
import { delay } from "../util/utils.ts";
|
||||||
import { decompressWith } from "./deps.ts";
|
import { decompressWith } from "./deps.ts";
|
||||||
import { handleDiscordPayload } from "./shard_manager.ts";
|
import { handleDiscordPayload } from "./shard_manager.ts";
|
||||||
import { Collection } from "../util/collection.ts";
|
|
||||||
|
|
||||||
export const basicShards = new Collection<number, BasicShard>();
|
export const basicShards = new Collection<number, BasicShard>();
|
||||||
const heartbeating = new Map<number, boolean>();
|
const heartbeating = new Map<number, boolean>();
|
||||||
@@ -27,6 +26,8 @@ export interface BasicShard {
|
|||||||
sessionID: string;
|
sessionID: string;
|
||||||
previousSequenceNumber: number | null;
|
previousSequenceNumber: number | null;
|
||||||
needToResume: boolean;
|
needToResume: boolean;
|
||||||
|
ready: boolean;
|
||||||
|
unavailableGuildIDs: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestMemberQueuedRequest {
|
interface RequestMemberQueuedRequest {
|
||||||
@@ -53,6 +54,8 @@ export function createShard(
|
|||||||
sessionID: oldShard?.sessionID || "",
|
sessionID: oldShard?.sessionID || "",
|
||||||
previousSequenceNumber: oldShard?.previousSequenceNumber || 0,
|
previousSequenceNumber: oldShard?.previousSequenceNumber || 0,
|
||||||
needToResume: false,
|
needToResume: false,
|
||||||
|
ready: false,
|
||||||
|
unavailableGuildIDs: new Set<string>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
basicShards.set(basicShard.id, basicShard);
|
basicShards.set(basicShard.id, basicShard);
|
||||||
|
|||||||
Reference in New Issue
Block a user