/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see .
*/
import type {GuildEmoji, GuildEmojiWithUser} from '~/records/GuildEmojiRecord';
import type {GuildSticker, GuildStickerWithUser} from '~/records/GuildStickerRecord';
import {sortBySnowflakeDesc} from '~/utils/SnowflakeUtils';
type EmojiUpdateListener = (emojis: ReadonlyArray) => void;
type StickerUpdateListener = (stickers: ReadonlyArray) => void;
const emojiCache = new Map>();
const stickerCache = new Map>();
const emojiListeners = new Map>();
const stickerListeners = new Map>();
const freezeList = (items: ReadonlyArray): ReadonlyArray => Object.freeze([...items]);
const notifyListeners = (
listeners: Map) => void>>,
guildId: string,
value: ReadonlyArray,
) => {
const listenersForGuild = listeners.get(guildId);
if (!listenersForGuild) return;
for (const listener of listenersForGuild) {
listener(value);
}
};
const setCache = (
cache: Map>,
listeners: Map) => void>>,
guildId: string,
value: ReadonlyArray,
shouldNotify: boolean,
) => {
const frozen = freezeList(sortBySnowflakeDesc(value));
cache.set(guildId, frozen);
if (shouldNotify) {
notifyListeners(listeners, guildId, frozen);
}
};
export const seedGuildEmojiCache = (guildId: string, emojis: ReadonlyArray): void => {
setCache(emojiCache, emojiListeners, guildId, emojis, false);
};
export const seedGuildStickerCache = (guildId: string, stickers: ReadonlyArray): void => {
setCache(stickerCache, stickerListeners, guildId, stickers, false);
};
export const subscribeToGuildEmojiUpdates = (guildId: string, listener: EmojiUpdateListener): (() => void) => {
let listenersForGuild = emojiListeners.get(guildId);
if (!listenersForGuild) {
listenersForGuild = new Set();
emojiListeners.set(guildId, listenersForGuild);
}
listenersForGuild.add(listener);
return () => {
listenersForGuild?.delete(listener);
if (listenersForGuild && listenersForGuild.size === 0) {
emojiListeners.delete(guildId);
}
};
};
export const subscribeToGuildStickerUpdates = (guildId: string, listener: StickerUpdateListener): (() => void) => {
let listenersForGuild = stickerListeners.get(guildId);
if (!listenersForGuild) {
listenersForGuild = new Set();
stickerListeners.set(guildId, listenersForGuild);
}
listenersForGuild.add(listener);
return () => {
listenersForGuild?.delete(listener);
if (listenersForGuild && listenersForGuild.size === 0) {
stickerListeners.delete(guildId);
}
};
};
export const patchGuildEmojiCacheFromGateway = (guildId: string, updates: ReadonlyArray) => {
const previous = emojiCache.get(guildId) ?? [];
const previousUserById = new Map(previous.map((emoji) => [emoji.id, emoji.user]));
const next = updates.map((emoji) => ({
...emoji,
user: emoji.user ?? previousUserById.get(emoji.id),
}));
setCache(emojiCache, emojiListeners, guildId, next, true);
};
export const patchGuildStickerCacheFromGateway = (guildId: string, updates: ReadonlyArray) => {
const previous = stickerCache.get(guildId) ?? [];
const previousUserById = new Map(previous.map((sticker) => [sticker.id, sticker.user]));
const next = updates.map((sticker) => ({
...sticker,
user: sticker.user ?? previousUserById.get(sticker.id),
}));
setCache(stickerCache, stickerListeners, guildId, next, true);
};