mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(api)!: correct pagination and locking, drop toggle routes (#2505)
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user