fix(api)!: correct pagination and locking, drop toggle routes (#2505)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-06 15:31:13 +02:00
1 parent 632f4c7b6c
commit d07f520b13
10 files changed
+302 -626

No files matched your search

@@ -7,6 +7,7 @@ import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError';
import {ResourceLockedError} from '@fluxer/errors/src/domains/core/ResourceLockedError';
import {UnknownGuildEmojiError} from '@fluxer/errors/src/domains/guild/UnknownGuildEmojiError';
import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError';
import {UnknownGuildStickerError} from '@fluxer/errors/src/domains/guild/UnknownGuildStickerError';
@@ -17,6 +18,7 @@ import type {
import type {GuildUpdateRequest} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
import type {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import type {ICacheService} from '@pkgs/cache/src/ICacheService';
import type {IpInfoService} from '@pkgs/geoip/src/IpInfoService';
import type {ApiContext} from '../../ApiContext';
import type {EmojiID, GuildID, RoleID, StickerID, UserID} from '../../BrandedTypes';
@@ -95,6 +97,10 @@ interface GuildAuth {
canManageRoles: (targetUserId: UserID, targetRoleId: RoleID) => Promise<boolean>;
}
const GUILD_UPDATE_LOCK_TTL_SECONDS = 10;
const GUILD_UPDATE_LOCK_RETRY_DELAY_MS = 50;
const GUILD_UPDATE_LOCK_MAX_WAIT_MS = 5000;
export class GuildService {
public readonly data: GuildDataService;
public readonly members: GuildMemberService;
@@ -104,6 +110,7 @@ export class GuildService {
public readonly channels: GuildChannelService;
public readonly search: GuildSearchService;
private readonly guildRepository: IGuildRepositoryAggregate;
private readonly cacheService: ICacheService;
private readonly userCacheService: UserCacheService;
private readonly webhookRepository: IWebhookRepository;
private readonly guildAuditLogService: GuildAuditLogService;
@@ -135,6 +142,7 @@ export class GuildService {
} = apiContext.services;
this.gatewayService = gatewayService;
this.guildRepository = guildRepository;
this.cacheService = cacheService;
this.userCacheService = userCacheService;
this.webhookRepository = webhookRepository;
this.guildAuditLogService = guildAuditLogService;
@@ -212,12 +220,6 @@ export class GuildService {
);
}
async getGuildFeaturesForToggle(guildId: GuildID): Promise<Set<string>> {
const guild = await this.guildRepository.findUnique(guildId);
if (!guild) throw new UnknownGuildError();
return new Set(guild.features);
}
async updateGuild(
params: {
userId: UserID;
@@ -228,7 +230,9 @@ export class GuildService {
auditLogReason?: string | null,
): Promise<GuildResponse> {
const {guildId, requestCache} = params;
const {guild, previousFeatures, updatedFeatures} = await this.data.updateGuild(params, auditLogReason);
const {guild, previousFeatures, updatedFeatures} = await this.withGuildUpdateLock(guildId, requestCache, () =>
this.data.updateGuild(params, auditLogReason),
);
if (
previousFeatures.has(GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES) &&
!updatedFeatures.has(GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES)
@@ -238,6 +242,32 @@ export class GuildService {
return guild;
}
private async withGuildUpdateLock<T>(guildId: GuildID, requestCache: RequestCache, fn: () => Promise<T>): Promise<T> {
const lockKey = `guild:${guildId}:update`;
const lockToken = await this.acquireGuildUpdateLock(lockKey);
if (!lockToken) {
throw new ResourceLockedError();
}
try {
requestCache.guilds.delete(guildId);
return await fn();
} finally {
await this.cacheService.releaseLock(lockKey, lockToken);
}
}
private async acquireGuildUpdateLock(lockKey: string): Promise<string | null> {
const startTime = Date.now();
while (Date.now() - startTime < GUILD_UPDATE_LOCK_MAX_WAIT_MS) {
const token = await this.cacheService.acquireLock(lockKey, GUILD_UPDATE_LOCK_TTL_SECONDS);
if (token) {
return token;
}
await new Promise((resolve) => setTimeout(resolve, GUILD_UPDATE_LOCK_RETRY_DELAY_MS));
}
return null;
}
async getEmojiMetadata(emojiId: EmojiID): Promise<GuildEmojiMetadataResponse> {
const emoji = await this.guildRepository.getEmojiById(emojiId);
if (!emoji) throw new UnknownGuildEmojiError();
@@ -318,10 +348,7 @@ export class GuildService {
let processedLogs: Array<GuildAuditLog> = [];
let currentBeforeLogId = beforeLogId;
let currentAfterLogId = afterLogId;
const maxIterations = 5;
let iterations = 0;
while (processedLogs.length < effectiveLimit && iterations < maxIterations) {
iterations++;
while (processedLogs.length < effectiveLimit) {
const fetchLimit = Math.min(effectiveLimit * 2, 200);
const logs = await this.guildRepository.listAuditLogs({
guildId,