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:
ITOH
2021-03-11 14:09:59 +00:00
committed by GitHub
parent 906fba7763
commit 372dc9988b
7 changed files with 134 additions and 72 deletions
+104
View File
@@ -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,
);
}),
);
}
}
}
}
+14 -3
View File
@@ -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());
} }
+2 -64
View File
@@ -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,
+1 -1
View File
@@ -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
View File
@@ -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 */
+4
View File
@@ -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
View File
@@ -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);