mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): retry hydrating message authors after a reconnect (#1849)
This commit is contained in:
1 parent
3e267b8104
commit
8c431c7562
1 file changed
+120
-32
@@ -17,8 +17,9 @@ interface PendingMemberRequest {
|
|||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
members: Array<GuildMember>;
|
members: Array<GuildMember>;
|
||||||
receivedChunks: number;
|
receivedChunks: number;
|
||||||
expectedChunks: number;
|
|
||||||
requestedUserIds?: Set<string>;
|
requestedUserIds?: Set<string>;
|
||||||
|
markMissingAsNonMembers: boolean;
|
||||||
|
timeoutId: ReturnType<typeof setTimeout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEMBER_REQUEST_TIMEOUT = 30000;
|
const MEMBER_REQUEST_TIMEOUT = 30000;
|
||||||
@@ -83,12 +84,19 @@ class GuildMembers {
|
|||||||
pendingRequests: Map<string, PendingMemberRequest> = new Map();
|
pendingRequests: Map<string, PendingMemberRequest> = new Map();
|
||||||
loadedGuilds: Set<string> = new Set();
|
loadedGuilds: Set<string> = new Set();
|
||||||
private pendingMessageMemberHydration: Map<string, Set<string>> = new Map();
|
private pendingMessageMemberHydration: Map<string, Set<string>> = new Map();
|
||||||
|
private inFlightMessageMembers: Map<string, Set<string>> = new Map();
|
||||||
|
private messageMemberRequestGeneration: number = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable<this, 'pendingMessageMemberHydration'>(
|
makeAutoObservable<
|
||||||
|
this,
|
||||||
|
'pendingMessageMemberHydration' | 'inFlightMessageMembers' | 'messageMemberRequestGeneration'
|
||||||
|
>(
|
||||||
this,
|
this,
|
||||||
{
|
{
|
||||||
pendingMessageMemberHydration: false,
|
pendingMessageMemberHydration: false,
|
||||||
|
inFlightMessageMembers: false,
|
||||||
|
messageMemberRequestGeneration: false,
|
||||||
},
|
},
|
||||||
{autoBind: true},
|
{autoBind: true},
|
||||||
);
|
);
|
||||||
@@ -127,8 +135,14 @@ class GuildMembers {
|
|||||||
handleConnectionOpen(guilds: Array<GuildReadyData>): void {
|
handleConnectionOpen(guilds: Array<GuildReadyData>): void {
|
||||||
this.members = {};
|
this.members = {};
|
||||||
this.nonMembers = {};
|
this.nonMembers = {};
|
||||||
this.pendingMessageMemberHydration.clear();
|
|
||||||
this.loadedGuilds.clear();
|
this.loadedGuilds.clear();
|
||||||
|
this.resetMessageMemberRequests();
|
||||||
|
const availableGuildIds = new Set(guilds.map((guild) => guild.id));
|
||||||
|
for (const guildId of Array.from(this.pendingMessageMemberHydration.keys())) {
|
||||||
|
if (!availableGuildIds.has(guildId)) {
|
||||||
|
this.pendingMessageMemberHydration.delete(guildId);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const guild of guilds) {
|
for (const guild of guilds) {
|
||||||
this.handleGuildCreate(guild);
|
this.handleGuildCreate(guild);
|
||||||
}
|
}
|
||||||
@@ -138,14 +152,16 @@ class GuildMembers {
|
|||||||
if (guild.unavailable) {
|
if (guild.unavailable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const members: Members = {};
|
if (!this.members[guild.id]) {
|
||||||
|
this.members[guild.id] = {};
|
||||||
|
}
|
||||||
|
const members = this.members[guild.id];
|
||||||
cacheGuildMemberUsers(guild.members);
|
cacheGuildMemberUsers(guild.members);
|
||||||
for (const member of guild.members) {
|
for (const member of guild.members) {
|
||||||
members[member.user.id] = new GuildMember(guild.id, member, {cacheUser: false});
|
members[member.user.id] = new GuildMember(guild.id, member, {cacheUser: false});
|
||||||
}
|
}
|
||||||
addVoiceStateMembers(guild, members);
|
addVoiceStateMembers(guild, members);
|
||||||
const missingVoiceStateMemberUserIds = getMissingVoiceStateMemberUserIds(guild, members);
|
const missingVoiceStateMemberUserIds = getMissingVoiceStateMemberUserIds(guild, members);
|
||||||
this.members[guild.id] = members;
|
|
||||||
if (missingVoiceStateMemberUserIds.length > 0 && GatewayConnection.socket) {
|
if (missingVoiceStateMemberUserIds.length > 0 && GatewayConnection.socket) {
|
||||||
void this.ensureMembersLoaded(guild.id, missingVoiceStateMemberUserIds).catch((error: unknown) => {
|
void this.ensureMembersLoaded(guild.id, missingVoiceStateMemberUserIds).catch((error: unknown) => {
|
||||||
logger.warn('Failed to fetch missing voice members after guild create', {
|
logger.warn('Failed to fetch missing voice members after guild create', {
|
||||||
@@ -157,7 +173,7 @@ class GuildMembers {
|
|||||||
}
|
}
|
||||||
if (options?.synced || GatewayConnection.hasCompletedGuildSync(guild.id)) {
|
if (options?.synced || GatewayConnection.hasCompletedGuildSync(guild.id)) {
|
||||||
this.loadedGuilds.add(guild.id);
|
this.loadedGuilds.add(guild.id);
|
||||||
this.flushPendingMessageMemberHydration(guild.id);
|
void this.requestPendingMessageMembers(guild.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +181,7 @@ class GuildMembers {
|
|||||||
delete this.members[guildId];
|
delete this.members[guildId];
|
||||||
delete this.nonMembers[guildId];
|
delete this.nonMembers[guildId];
|
||||||
this.pendingMessageMemberHydration.delete(guildId);
|
this.pendingMessageMemberHydration.delete(guildId);
|
||||||
|
this.inFlightMessageMembers.delete(guildId);
|
||||||
this.loadedGuilds.delete(guildId);
|
this.loadedGuilds.delete(guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +259,7 @@ class GuildMembers {
|
|||||||
pending.members.push(...newMembers);
|
pending.members.push(...newMembers);
|
||||||
pending.receivedChunks++;
|
pending.receivedChunks++;
|
||||||
if (pending.receivedChunks >= chunkCount) {
|
if (pending.receivedChunks >= chunkCount) {
|
||||||
|
clearTimeout(pending.timeoutId);
|
||||||
this.markNotFoundAsNonMembers(pending);
|
this.markNotFoundAsNonMembers(pending);
|
||||||
pending.resolve(pending.members);
|
pending.resolve(pending.members);
|
||||||
this.pendingRequests.delete(nonce);
|
this.pendingRequests.delete(nonce);
|
||||||
@@ -251,6 +269,9 @@ class GuildMembers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private markNotFoundAsNonMembers(pending: PendingMemberRequest): void {
|
private markNotFoundAsNonMembers(pending: PendingMemberRequest): void {
|
||||||
|
if (!pending.markMissingAsNonMembers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const requested = pending.requestedUserIds;
|
const requested = pending.requestedUserIds;
|
||||||
if (!requested || requested.size === 0) {
|
if (!requested || requested.size === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -281,28 +302,36 @@ class GuildMembers {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
userIds?: Array<string>;
|
userIds?: Array<string>;
|
||||||
presences?: boolean;
|
presences?: boolean;
|
||||||
|
markMissingAsNonMembers?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<Array<GuildMember>> {
|
): Promise<Array<GuildMember>> {
|
||||||
const userIds = options?.userIds;
|
const userIds = options?.userIds;
|
||||||
if (userIds && userIds.length > MAX_USER_IDS_PER_REQUEST) {
|
if (userIds && userIds.length > MAX_USER_IDS_PER_REQUEST) {
|
||||||
const all: Array<GuildMember> = [];
|
const batches: Array<Promise<Array<GuildMember>>> = [];
|
||||||
for (let i = 0; i < userIds.length; i += MAX_USER_IDS_PER_REQUEST) {
|
for (let i = 0; i < userIds.length; i += MAX_USER_IDS_PER_REQUEST) {
|
||||||
const slice = userIds.slice(i, i + MAX_USER_IDS_PER_REQUEST);
|
const slice = userIds.slice(i, i + MAX_USER_IDS_PER_REQUEST);
|
||||||
const batch = await this.fetchMembers(guildId, {...options, userIds: slice});
|
batches.push(this.fetchMembers(guildId, {...options, userIds: slice}));
|
||||||
all.push(...batch);
|
|
||||||
}
|
}
|
||||||
return all;
|
const results = await Promise.all(batches);
|
||||||
|
return results.flat();
|
||||||
}
|
}
|
||||||
const nonce = generateMemberNonce();
|
const nonce = generateMemberNonce();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (this.pendingRequests.has(nonce)) {
|
||||||
|
this.pendingRequests.delete(nonce);
|
||||||
|
reject(new Error('Request timed out'));
|
||||||
|
}
|
||||||
|
}, MEMBER_REQUEST_TIMEOUT);
|
||||||
this.pendingRequests.set(nonce, {
|
this.pendingRequests.set(nonce, {
|
||||||
guildId,
|
guildId,
|
||||||
resolve,
|
resolve,
|
||||||
reject,
|
reject,
|
||||||
members: [],
|
members: [],
|
||||||
receivedChunks: 0,
|
receivedChunks: 0,
|
||||||
expectedChunks: 1,
|
|
||||||
requestedUserIds: userIds && userIds.length > 0 ? new Set(userIds) : undefined,
|
requestedUserIds: userIds && userIds.length > 0 ? new Set(userIds) : undefined,
|
||||||
|
markMissingAsNonMembers: options?.markMissingAsNonMembers ?? false,
|
||||||
|
timeoutId,
|
||||||
});
|
});
|
||||||
const socket = GatewayConnection.socket;
|
const socket = GatewayConnection.socket;
|
||||||
const requestOptions: {
|
const requestOptions: {
|
||||||
@@ -327,12 +356,6 @@ class GuildMembers {
|
|||||||
requestOptions.userIds = userIds;
|
requestOptions.userIds = userIds;
|
||||||
}
|
}
|
||||||
socket?.requestGuildMembers(requestOptions);
|
socket?.requestGuildMembers(requestOptions);
|
||||||
setTimeout(() => {
|
|
||||||
if (this.pendingRequests.has(nonce)) {
|
|
||||||
this.pendingRequests.delete(nonce);
|
|
||||||
reject(new Error('Request timed out'));
|
|
||||||
}
|
|
||||||
}, MEMBER_REQUEST_TIMEOUT);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +363,6 @@ class GuildMembers {
|
|||||||
guildIds: Array<string>;
|
guildIds: Array<string>;
|
||||||
query?: string;
|
query?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
userIds?: Array<string>;
|
|
||||||
presences?: boolean;
|
presences?: boolean;
|
||||||
}): void {
|
}): void {
|
||||||
const socket = GatewayConnection.socket;
|
const socket = GatewayConnection.socket;
|
||||||
@@ -355,7 +377,6 @@ class GuildMembers {
|
|||||||
guildIds,
|
guildIds,
|
||||||
...(options.query !== undefined && {query: options.query}),
|
...(options.query !== undefined && {query: options.query}),
|
||||||
...(options.limit !== undefined && {limit: options.limit}),
|
...(options.limit !== undefined && {limit: options.limit}),
|
||||||
...(options.userIds !== undefined && {userIds: options.userIds}),
|
|
||||||
...(options.presences !== undefined && {presences: options.presences}),
|
...(options.presences !== undefined && {presences: options.presences}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -365,7 +386,7 @@ class GuildMembers {
|
|||||||
if (missingIds.length === 0) {
|
if (missingIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.fetchMembers(guildId, {userIds: missingIds});
|
await this.fetchMembers(guildId, {userIds: missingIds, markMissingAsNonMembers: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureMembersLoadedForMessages(guildId: string, userIds: Array<string>): Promise<void> {
|
async ensureMembersLoadedForMessages(guildId: string, userIds: Array<string>): Promise<void> {
|
||||||
@@ -373,14 +394,23 @@ class GuildMembers {
|
|||||||
if (missingIds.length === 0) {
|
if (missingIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (GatewayConnection.hasCompletedGuildSync(guildId)) {
|
|
||||||
await this.ensureMembersLoaded(guildId, missingIds);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.queuePendingMessageMemberHydration(guildId, missingIds);
|
this.queuePendingMessageMemberHydration(guildId, missingIds);
|
||||||
if (SelectedGuild.selectedGuildId === guildId) {
|
if (!GatewayConnection.hasCompletedGuildSync(guildId) && SelectedGuild.selectedGuildId === guildId) {
|
||||||
GatewayConnection.syncGuildIfNeeded(guildId, 'message-member-hydration');
|
GatewayConnection.syncGuildIfNeeded(guildId, 'message-member-hydration');
|
||||||
}
|
}
|
||||||
|
await this.requestPendingMessageMembers(guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnectionResumed(): void {
|
||||||
|
this.resetMessageMemberRequests();
|
||||||
|
for (const guildId of Array.from(this.pendingMessageMemberHydration.keys())) {
|
||||||
|
void this.requestPendingMessageMembers(guildId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetMessageMemberRequests(): void {
|
||||||
|
this.messageMemberRequestGeneration += 1;
|
||||||
|
this.inFlightMessageMembers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMissingMemberIds(guildId: string, userIds: Array<string>): Array<string> {
|
private getMissingMemberIds(guildId: string, userIds: Array<string>): Array<string> {
|
||||||
@@ -400,21 +430,79 @@ class GuildMembers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private flushPendingMessageMemberHydration(guildId: string): void {
|
private async requestPendingMessageMembers(guildId: string): Promise<void> {
|
||||||
|
if (!GatewayConnection.hasCompletedGuildSync(guildId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pending = this.pendingMessageMemberHydration.get(guildId);
|
const pending = this.pendingMessageMemberHydration.get(guildId);
|
||||||
if (!pending || pending.size === 0) {
|
if (!pending || pending.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const stillMissing = new Set(this.getMissingMemberIds(guildId, Array.from(pending)));
|
||||||
|
for (const userId of Array.from(pending)) {
|
||||||
|
if (!stillMissing.has(userId)) {
|
||||||
|
pending.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pending.size === 0) {
|
||||||
this.pendingMessageMemberHydration.delete(guildId);
|
this.pendingMessageMemberHydration.delete(guildId);
|
||||||
const missingIds = this.getMissingMemberIds(guildId, Array.from(pending));
|
|
||||||
if (missingIds.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.requestMembersInBackground({
|
let inFlight = this.inFlightMessageMembers.get(guildId);
|
||||||
guildIds: [guildId],
|
if (!inFlight) {
|
||||||
userIds: missingIds,
|
inFlight = new Set();
|
||||||
presences: true,
|
this.inFlightMessageMembers.set(guildId, inFlight);
|
||||||
|
}
|
||||||
|
const userIds = Array.from(pending).filter((userId) => !inFlight.has(userId));
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const userId of userIds) {
|
||||||
|
inFlight.add(userId);
|
||||||
|
}
|
||||||
|
const generation = this.messageMemberRequestGeneration;
|
||||||
|
try {
|
||||||
|
await this.fetchMembers(guildId, {userIds, markMissingAsNonMembers: false});
|
||||||
|
if (generation === this.messageMemberRequestGeneration) {
|
||||||
|
this.forgetPendingMessageMembers(guildId, userIds);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to hydrate message author members, will retry on the next load or reconnect', {
|
||||||
|
guildId,
|
||||||
|
userIds,
|
||||||
|
error,
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
if (generation === this.messageMemberRequestGeneration) {
|
||||||
|
this.releaseInFlightMessageMembers(guildId, userIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private forgetPendingMessageMembers(guildId: string, userIds: Array<string>): void {
|
||||||
|
const pending = this.pendingMessageMemberHydration.get(guildId);
|
||||||
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const userId of userIds) {
|
||||||
|
pending.delete(userId);
|
||||||
|
}
|
||||||
|
if (pending.size === 0) {
|
||||||
|
this.pendingMessageMemberHydration.delete(guildId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private releaseInFlightMessageMembers(guildId: string, userIds: Array<string>): void {
|
||||||
|
const inFlight = this.inFlightMessageMembers.get(guildId);
|
||||||
|
if (!inFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const userId of userIds) {
|
||||||
|
inFlight.delete(userId);
|
||||||
|
}
|
||||||
|
if (inFlight.size === 0) {
|
||||||
|
this.inFlightMessageMembers.delete(guildId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isGuildFullyLoaded(guildId: string): boolean {
|
isGuildFullyLoaded(guildId: string): boolean {
|
||||||
|
|||||||
Reference in new issue
Block a user