diff --git a/src/bot/models/guild/guildChannelModel.ts b/src/bot/models/guild/guildChannelModel.ts index 18fda7e..aebf929 100644 --- a/src/bot/models/guild/guildChannelModel.ts +++ b/src/bot/models/guild/guildChannelModel.ts @@ -1,39 +1,41 @@ import type { Guild, GuildBasedChannel } from 'discord.js'; import shardDb from '../../../models/shardDb/shardDb.js'; import mysql from 'promise-mysql'; -import type { guildChannel, textMessage, voiceMinute } from 'models/types/shard.js'; - -const promises = new Map>(); +import type { + GuildChannelSchema, + TextMessageSchema, + VoiceMinuteSchema, +} from 'models/types/shard.js'; +import guildModel from './guildModel.js'; const cachedFields = ['noXp', 'noCommand'] as const; -let defaultCache: CachedGuildChannel | null = null; -let defaultAll: guildChannel | null = null; +let defaultCache: CachedDbFields | null = null; +let defaultAll: GuildChannelSchema | null = null; -export type CachedGuildChannel = Pick; +type CachedDbFields = Pick; -export const cache = { - load: (channel: GuildBasedChannel) => { - if (!channel.appData) { - if (promises.has(channel.id)) { - return promises.get(channel.id)!; - } - - promises.set( - channel.id, - buildCache(channel).finally(() => promises.delete(channel.id)), - ); - - return promises.get(channel.id); - } +interface CachedGuildChannel { + db: CachedDbFields; +} + +const channelCache = new WeakMap(); - return new Promise(async (resolve) => resolve()); +export const cache = { + get: async function (channel: GuildBasedChannel): Promise { + if (channelCache.has(channel)) return channelCache.get(channel)!; + return await buildCache(channel); }, }; +function isCachableDbKey(key: keyof GuildChannelSchema): key is keyof CachedDbFields { + return cachedFields.includes(key as keyof CachedDbFields); +} + export const storage = { - get: async (guild: Guild, channelId: string): Promise => { - const res = await shardDb.query( - guild.appData.dbHost, + get: async (guild: Guild, channelId: string): Promise => { + const { dbHost } = await guildModel.cache.get(guild); + const res = await shardDb.query( + dbHost, `SELECT * FROM guildChannel WHERE guildId = ${guild.id} && channelId = ${mysql.escape( channelId, )}`, @@ -42,8 +44,8 @@ export const storage = { if (res.length == 0) { if (!defaultAll) defaultAll = ( - await shardDb.query( - guild.appData.dbHost, + await shardDb.query( + dbHost, `SELECT * FROM guildChannel WHERE guildId = 0 AND channelId = 0`, ) )[0]; @@ -51,32 +53,36 @@ export const storage = { } else return res[0]; }, - set: async ( + set: async ( guild: Guild, channelId: string, field: K, - value: guildChannel[K], + value: GuildChannelSchema[K], ) => { + const { dbHost } = await guildModel.cache.get(guild); await shardDb.query( - guild.appData.dbHost, + dbHost, `INSERT INTO guildChannel (guildId,channelId,${field}) VALUES (${guild.id},${mysql.escape( channelId, )},${mysql.escape(value)}) ON DUPLICATE KEY UPDATE ${field} = ${mysql.escape(value)}`, ); const channel = guild.channels.cache.get(channelId); - if (channel && channel.appData && (cachedFields as readonly string[]).includes(field)) - channel.appData[field] = value; + if (channel && isCachableDbKey(field)) { + const cachedChannel = await cache.get(channel); + Object.defineProperty(cachedChannel.db, field, { value }); + } }, }; export const getRankedChannelIds = async (guild: Guild) => { - const textmessageUserIds = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + const textmessageUserIds = await shardDb.query( + dbHost, `SELECT DISTINCT channelId FROM textMessage WHERE guildId = ${guild.id} AND alltime != 0`, ); - const voiceMinuteUserIds = await shardDb.query( - guild.appData.dbHost, + const voiceMinuteUserIds = await shardDb.query( + dbHost, `SELECT DISTINCT channelId FROM voiceMinute WHERE guildId = ${guild.id} AND alltime != 0`, ); @@ -91,8 +97,9 @@ export const getRankedChannelIds = async (guild: Guild) => { }; export const getNoXpChannelIds = async (guild: Guild) => { - const res = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + const res = await shardDb.query[]>( + dbHost, `SELECT channelId FROM guildChannel WHERE guildId = ${guild.id} AND noXp = 1`, ); @@ -100,35 +107,31 @@ export const getNoXpChannelIds = async (guild: Guild) => { }; export const getNoCommandChannelIds = async (guild: Guild) => { - const res = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + const res = await shardDb.query[]>( + dbHost, `SELECT channelId FROM guildChannel WHERE guildId = ${guild.id} AND noCommand = 1`, ); return res.map((i) => i.channelId); }; -const buildCache = async (channel: GuildBasedChannel) => { - let foundCache = await shardDb.query( - channel.guild.appData.dbHost, +async function buildCache(channel: GuildBasedChannel): Promise { + const { dbHost } = await guildModel.cache.get(channel.guild); + let foundCache = await shardDb.query( + dbHost, `SELECT ${cachedFields.join(',')} FROM guildChannel WHERE guildId = ${ channel.guild.id } AND channelId = ${channel.id}`, ); - let cache; + const db = foundCache.length > 0 ? foundCache[0] : { ...(await loadDefaultCache(dbHost)) }; - if (foundCache.length > 0) cache = foundCache[0]; - else { - if (!defaultCache) await loadDefaultCache(channel.guild.appData.dbHost); - cache = Object.assign({}, defaultCache); - } - - channel.appData = cache; -}; + return { db }; +} const loadDefaultCache = async (dbHost: string) => { - let res = await shardDb.query( + let res = await shardDb.query( dbHost, `SELECT ${cachedFields.join(',')} FROM guildChannel WHERE guildId = 0 AND channelId = 0`, ); @@ -141,7 +144,8 @@ const loadDefaultCache = async (dbHost: string) => { `SELECT ${cachedFields.join(',')} FROM guildChannel WHERE guildId = 0 AND channelId = 0`, ); - defaultCache = res[0]; + defaultCache = res[0]!; + return defaultCache; }; export default { diff --git a/src/bot/models/guild/guildMemberModel.ts b/src/bot/models/guild/guildMemberModel.ts index 58f351e..a8fbc39 100644 --- a/src/bot/models/guild/guildMemberModel.ts +++ b/src/bot/models/guild/guildMemberModel.ts @@ -1,65 +1,55 @@ import shardDb from '../../../models/shardDb/shardDb.js'; import mysql from 'promise-mysql'; -import rankModel from '../rankModel.js'; import type { Guild, GuildMember } from 'discord.js'; -import type { guildMember } from 'models/types/shard.js'; +import type { GuildMemberSchema } from 'models/types/shard.js'; import type { PropertiesOfType } from 'models/types/generics.js'; - -const promises = new Map>(); +import guildModel from './guildModel.js'; const cachedFields = ['notifyLevelupDm', 'reactionVote'] as const; -let defaultCache: CachedGuildMember | null = null; -let defaultAll: guildMember | null = null; - -export type CachedGuildMember = Pick & { - totalXp: number; - totalScore: number; - lastVoteDate: Date | null; - lastTextMessageDate: Date | null; - lastMessageChannelId: string | null; -}; +let defaultCache: CachedDbFields | null = null; +let defaultAll: GuildMemberSchema | null = null; -export const cache = { - load: (member: GuildMember) => { - if (!member.appData) { - const promiseKey = `${member.guild.id}.${member.id}`; - if (promises.has(promiseKey)) { - return promises.get(promiseKey)!; - } +type CachedDbFields = Pick; - promises.set( - `${member.guild.id}.${member.id}`, - new Promise(async (resolve, reject) => { - try { - await buildCache(member); - promises.delete(promiseKey); - resolve(); - } catch (e) { - promises.delete(promiseKey); - reject(e); - } - }), - ); - - return promises.get(promiseKey)!; - } +interface MemberCacheStorage { + totalXp?: number; + totalScore?: number; + lastVoteDate?: Date | null; + lastTextMessageDate?: Date | null; + lastMessageChannelId?: string | null; +} + +interface CachedGuildMember { + db: CachedDbFields; + cache: MemberCacheStorage; +} - return new Promise(async (resolve) => resolve()); +const memberCache = new WeakMap(); + +export const cache = { + get: async function (member: GuildMember): Promise { + if (memberCache.has(member)) return memberCache.get(member)!; + return await buildCache(member); }, }; +function isCachableDbKey(key: keyof GuildMemberSchema): key is keyof CachedDbFields { + return cachedFields.includes(key as keyof CachedDbFields); +} + export const storage = { get: async (guild: Guild, userId: string) => { - const res = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + const res = await shardDb.query( + dbHost, `SELECT * FROM guildMember WHERE guildId = ${guild.id} && userId = ${mysql.escape(userId)}`, ); if (res.length == 0) { if (!defaultAll) defaultAll = ( - await shardDb.query( - guild.appData.dbHost, + await shardDb.query( + dbHost, `SELECT * FROM guildMember WHERE guildId = 0 AND userId = 0`, ) )[0]; @@ -67,32 +57,40 @@ export const storage = { } else return res[0]; }, - set: async >( + set: async >( guild: Guild, userId: string, field: K, - value: CachedGuildMember[K], + value: GuildMemberSchema[K], ) => { + const { dbHost } = await guildModel.cache.get(guild); + await shardDb.query( - guild.appData.dbHost, + dbHost, `INSERT INTO guildMember (guildId,userId,${field}) VALUES (${guild.id},${mysql.escape( userId, )},${mysql.escape(value)}) ON DUPLICATE KEY UPDATE ${field} = ${mysql.escape(value)}`, ); const member = guild.members.cache.get(userId); - if (member && member.appData && (cachedFields as readonly string[]).indexOf(field) > -1) - member.appData[field] = value; + if (member) { + const cachedMember = await cache.get(member); + if (isCachableDbKey(field)) { + Object.defineProperty(cachedMember.db, field, { value }); + } + } }, - increment: async >( + increment: async >( guild: Guild, userId: string, field: K, value: number, ) => { + const { dbHost } = await guildModel.cache.get(guild); + await shardDb.query( - guild.appData.dbHost, + dbHost, `INSERT INTO guildMember (guildId,userId,${field}) VALUES (${guild.id},${mysql.escape( userId, )},${mysql.escape(value)}) ON DUPLICATE KEY UPDATE ${field} = ${field} + ${mysql.escape( @@ -101,26 +99,30 @@ export const storage = { ); const member = guild.members.cache.get(userId); - if (member && member.appData && (cachedFields as readonly string[]).indexOf(field) > -1) - member.appData[field] += value * 1; + if (member && isCachableDbKey(field)) { + const cachedMember = await cache.get(member); + cachedMember.db[field] += value; + } }, }; export async function getRankedUserIds(guild: Guild) { + const { dbHost } = await guildModel.cache.get(guild); + const textmessageUserIds = await shardDb.query<{ userId: string }[]>( - guild.appData.dbHost, + dbHost, `SELECT DISTINCT userId FROM textMessage WHERE guildId = ${guild.id} AND alltime != 0`, ); const voiceMinuteUserIds = await shardDb.query<{ userId: string }[]>( - guild.appData.dbHost, + dbHost, `SELECT DISTINCT userId FROM voiceMinute WHERE guildId = ${guild.id} AND alltime != 0`, ); const voteUserIds = await shardDb.query<{ userId: string }[]>( - guild.appData.dbHost, + dbHost, `SELECT DISTINCT userId FROM vote WHERE guildId = ${guild.id} AND alltime != 0`, ); const bonusUserIds = await shardDb.query<{ userId: string }[]>( - guild.appData.dbHost, + dbHost, `SELECT DISTINCT userId FROM bonus WHERE guildId = ${guild.id} AND alltime != 0`, ); @@ -136,30 +138,24 @@ export async function getRankedUserIds(guild: Guild) { return userIds; } -const buildCache = async (member: GuildMember) => { - let foundCache = await shardDb.query( - member.guild.appData.dbHost, +async function buildCache(member: GuildMember): Promise { + const { dbHost } = await guildModel.cache.get(member.guild); + let foundCache = await shardDb.query( + dbHost, `SELECT ${cachedFields.join(',')} FROM guildMember WHERE guildId = ${ member.guild.id } AND userId = ${member.id}`, ); - let cache; - - if (foundCache.length > 0) cache = foundCache[0]; - else { - if (!defaultCache) await loadDefaultCache(member.guild.appData.dbHost); - cache = Object.assign({}, defaultCache); - } - - cache.totalXp = (await rankModel.getGuildMemberTotalScore(member.guild, member.id)) ?? 0; - cache.lastTextMessageDate = null; + const db = foundCache.length > 0 ? foundCache[0] : { ...(await loadDefaultCache(dbHost)) }; - member.appData = cache; -}; + return { db, cache: {} }; +} const loadDefaultCache = async (dbHost: string) => { - let res = await shardDb.query( + if (defaultCache) return defaultCache; + + let res = await shardDb.query( dbHost, `SELECT ${cachedFields.join(',')} FROM guildMember WHERE guildId = 0 AND userId = 0`, ); @@ -173,6 +169,7 @@ const loadDefaultCache = async (dbHost: string) => { ); defaultCache = res[0]; + return defaultCache; }; export default { diff --git a/src/bot/models/guild/guildModel.ts b/src/bot/models/guild/guildModel.ts index 4baf78d..113003c 100644 --- a/src/bot/models/guild/guildModel.ts +++ b/src/bot/models/guild/guildModel.ts @@ -2,11 +2,9 @@ import shardDb from '../../../models/shardDb/shardDb.js'; import managerDb from '../../../models/managerDb/managerDb.js'; import mysql from 'promise-mysql'; import type { Guild } from 'discord.js'; -import type { guild } from 'models/types/shard.js'; +import type { GuildSchema } from 'models/types/shard.js'; import type { PropertiesOfType } from 'models/types/generics.js'; -const promises = new Map>(); - const hostField = process.env.NODE_ENV == 'production' ? 'hostIntern' : 'hostExtern'; const cachedFields = [ //'tokens', @@ -54,68 +52,78 @@ const cachedFields = [ 'isBanned', ] as const; -export const cache = { - load: (guild: Guild) => { - if (!guild.appData) { - if (promises.has(guild.id)) return promises.get(guild.id)!; - - promises.set( - guild.id, - buildCache(guild).finally(() => promises.delete(guild.id)), - ); +type CachedDbFields = Pick; - return promises.get(guild.id)!; - } +// TODO convert to dates +interface GuildCacheStorage { + lastAskForPremiumDate?: number; + lastResetServer?: number; +} +interface CachedGuild { + db: CachedDbFields; + dbHost: string; + cache: GuildCacheStorage; +} +const guildCache = new WeakMap(); - return new Promise(async (resolve) => resolve()); +export const cache = { + get: async function (guild: Guild): Promise { + if (guildCache.has(guild)) return guildCache.get(guild)!; + else return await buildCache(guild); }, }; +function isCachableDbKey(key: keyof GuildSchema): key is keyof CachedDbFields { + return cachedFields.includes(key as keyof CachedDbFields); +} + export const storage = { - set: async >( + set: async >( guild: Guild, field: K, - value: guild[K], + value: GuildSchema[K], ) => { + const cachedGuild = await cache.get(guild); await shardDb.query( - guild.appData.dbHost, + cachedGuild.dbHost, `UPDATE guild SET ${field} = ${mysql.escape(value)} WHERE guildId = ${guild.id}`, ); - if ((cachedFields as readonly string[]).includes(field)) guild.appData[field] = value; + if (isCachableDbKey(field)) { + Object.defineProperty(cachedGuild.db, field, { value }); + } }, - increment: async >( + increment: async >( guild: Guild, field: K, - value: CachedGuild[K], + value: GuildSchema[K], ) => { + const cachedGuild = await cache.get(guild); await shardDb.query( - guild.appData.dbHost, + cachedGuild.dbHost, `UPDATE guild SET ${field} = ${field} + ${mysql.escape(value)} WHERE guildId = ${guild.id}`, ); - if ((cachedFields as readonly string[]).includes(field)) guild.appData[field] += value * 1; + if (isCachableDbKey(field)) { + cachedGuild.db[field] += value; + } }, - get: async (guild: Guild) => { - const res = await shardDb.query( - guild.appData.dbHost, + const cachedGuild = await cache.get(guild); + + const res = await shardDb.query( + cachedGuild.dbHost, `SELECT * FROM guild WHERE guildId = ${guild.id}`, ); + if (res.length == 0) return null; else return res[0]; }, }; -export type CachedGuild = Pick & { - dbHost: string; - lastAskForPremiumDate: number; - lastResetServer: number; -}; - -async function buildCache(guild: Guild) { +async function buildCache(guild: Guild): Promise { const dbHost = await getDbHost(guild.id); - let cache = await shardDb.query( + let cache = await shardDb.query( dbHost, `SELECT ${cachedFields.join(',')} FROM guild WHERE guildId = ${guild.id}`, ); @@ -127,15 +135,14 @@ async function buildCache(guild: Guild) { guild.members.me!.joinedAt!.getTime() / 1000, )},${Math.floor(Date.now() / 1000)})`, ); - cache = await shardDb.query( + cache = await shardDb.query( dbHost, `SELECT ${cachedFields.join(',')} FROM guild WHERE guildId = ${guild.id}`, ); } const cachedGuild = cache[0]!; - cachedGuild.dbHost = dbHost; - guild.appData = cachedGuild; + return { cache: {}, db: cachedGuild, dbHost }; } const getDbHost = async (guildId: string): Promise => { diff --git a/src/bot/models/guild/guildRoleModel.ts b/src/bot/models/guild/guildRoleModel.ts index e35d683..f0873cb 100644 --- a/src/bot/models/guild/guildRoleModel.ts +++ b/src/bot/models/guild/guildRoleModel.ts @@ -1,9 +1,8 @@ import type { Guild, Role } from 'discord.js'; import shardDb from '../../../models/shardDb/shardDb.js'; import mysql from 'promise-mysql'; -import type { guildRole } from 'models/types/shard.js'; - -const promises = new Map>(); +import type { GuildRoleSchema } from 'models/types/shard.js'; +import guildModel from './guildModel.js'; const cachedFields = [ 'noXp', @@ -13,76 +12,74 @@ const cachedFields = [ 'deassignMessage', ] as const; -export type CachedRole = Pick; +let defaultCache: CachedDbFields | null = null; +let defaultAll: GuildRoleSchema | null = null; -let defaultCache: CachedRole | null = null; -let defaultAll: guildRole | null = null; +type CachedDbFields = Pick; -export const cache = { - load: (role: Role) => { - if (!role.appData) { - if (promises.has(role.id)) return promises.get(role.id)!; - - promises.set( - role.id, - new Promise(async (resolve, reject) => { - try { - await buildCache(role); - promises.delete(role.id); - resolve(); - } catch (e) { - promises.delete(role.id); - reject(e); - } - }), - ); - - return promises.get(role.id)!; - } +interface CachedRole { + db: CachedDbFields; +} - return new Promise(async (resolve) => { - resolve(); - }); +const roleCache = new WeakMap(); + +export const cache = { + get: async function (role: Role): Promise { + if (roleCache.has(role)) return roleCache.get(role)!; + return await buildCache(role); }, }; +function isCachableDbKey(key: keyof GuildRoleSchema): key is keyof CachedDbFields { + return cachedFields.includes(key as keyof CachedDbFields); +} + export const storage = { get: async (guild: Guild, roleId: string) => { - const res = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + + const res = await shardDb.query( + dbHost, `SELECT * FROM guildRole WHERE guildId = ${guild.id} && roleId = ${mysql.escape(roleId)}`, ); if (res.length == 0) { if (!defaultAll) defaultAll = ( - await shardDb.query( - guild.appData.dbHost, + await shardDb.query( + dbHost, `SELECT * FROM guildRole WHERE guildId = 0 AND roleId = 0`, ) )[0]; return defaultAll; } else return res[0]; }, - set: async >( + set: async >( guild: Guild, roleId: string, field: K, - value: guildRole[K], + value: GuildRoleSchema[K], ) => { + const { dbHost } = await guildModel.cache.get(guild); + await shardDb.query( - guild.appData.dbHost, + dbHost, `INSERT INTO guildRole (guildId,roleId,${field}) VALUES (${guild.id},${mysql.escape( roleId, )},${mysql.escape(value)}) ON DUPLICATE KEY UPDATE ${field} = ${mysql.escape(value)}`, ); const role = guild.roles.cache.get(roleId); - if (role && role.appData && cachedFields.indexOf(field) > -1) role.appData[field] = value; + if (role && isCachableDbKey(field)) { + const cachedRole = await cache.get(role); + Object.defineProperty(cachedRole.db, field, { value }); + } }, getRoleAssignments: async (guild: Guild) => { - const res = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + + const res = await shardDb.query( + dbHost, `SELECT * FROM guildRole WHERE guildId = ${guild.id} AND (assignLevel != 0 OR deassignLevel != 0) ORDER BY assignLevel ASC`, ); @@ -93,16 +90,20 @@ export const storage = { type: 'assignLevel' | 'deassignLevel', level: number | null, ) => { - const res = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + + const res = await shardDb.query( + dbHost, `SELECT * FROM guildRole WHERE guildId = ${guild.id} AND ${type} = ${mysql.escape(level)}`, ); return res; }, getRoleAssignmentsByRole: async (guild: Guild, roleId: string) => { - const res = await shardDb.query( - guild.appData.dbHost, + const { dbHost } = await guildModel.cache.get(guild); + + const res = await shardDb.query( + dbHost, `SELECT * FROM guildRole WHERE guildId = ${guild.id} AND roleId = ${roleId}`, ); @@ -111,35 +112,35 @@ export const storage = { }; export const getNoXpRoleIds = async (guild: Guild) => { + const { dbHost } = await guildModel.cache.get(guild); + const res = await shardDb.query<{ roleId: string }[]>( - guild.appData.dbHost, + dbHost, `SELECT roleId FROM guildRole WHERE guildId = ${guild.id} AND noXp = 1`, ); return res.map((role) => role.roleId); }; -const buildCache = async (role: Role) => { - const foundCache = await shardDb.query( - role.guild.appData.dbHost, +const buildCache = async (role: Role): Promise => { + const { dbHost } = await guildModel.cache.get(role.guild); + + const foundCache = await shardDb.query( + dbHost, `SELECT ${cachedFields.join(',')} FROM guildRole WHERE guildId = ${ role.guild.id } AND roleId = ${role.id}`, ); - let cache; + const db = foundCache.length > 0 ? foundCache[0] : await loadDefaultCache(dbHost); - if (foundCache.length > 0) cache = foundCache[0]; - else { - if (!defaultCache) await loadDefaultCache(role.guild.appData.dbHost); - cache = Object.assign({}, defaultCache); - } - - role.appData = cache; + return { db }; }; const loadDefaultCache = async (dbHost: string) => { - let res = await shardDb.query( + if (defaultCache) return defaultCache; + + let res = await shardDb.query( dbHost, `SELECT ${cachedFields.join(',')} FROM guildRole WHERE guildId = 0 AND roleId = 0`, ); @@ -153,6 +154,7 @@ const loadDefaultCache = async (dbHost: string) => { ); defaultCache = res[0]; + return defaultCache; }; export default { diff --git a/src/bot/models/userModel.ts b/src/bot/models/userModel.ts index 38ac3ea..8a2f521 100644 --- a/src/bot/models/userModel.ts +++ b/src/bot/models/userModel.ts @@ -2,26 +2,36 @@ import shardDb from '../../models/shardDb/shardDb.js'; import managerDb from '../../models/managerDb/managerDb.js'; import mysql from 'promise-mysql'; import type { User } from 'discord.js'; -import type { user } from 'models/types/shard.js'; +import type { UserSchema } from 'models/types/shard.js'; import type { PropertiesOfType } from 'models/types/generics.js'; -const promises = new Map>(); - -let defaultCache: CachedUser | null = null; -let defaultAll: user | null = null; +let defaultCache: CachedDbFields | null = null; +let defaultAll: UserSchema | null = null; const cachedFields = ['userId', 'isBanned'] as const; -const cachedFieldSet: Set = new Set(cachedFields); const hostField = process.env.NODE_ENV == 'production' ? 'hostIntern' : 'hostExtern'; -export type CachedUser = Pick & { - dbHost: string; +type CachedDbFields = Pick; + +interface UserCacheStorage { patreonTier?: number; patreonTierUntilDate?: number; lastTopggUpvoteDate?: number; -}; +} + +interface CachedUser { + db: CachedDbFields; + dbHost: string; + cache: UserCacheStorage; +} + +const userCache = new WeakMap(); export const cache = { - load: async function (user: User) { + get: async function (user: User): Promise { + if (userCache.has(user)) return userCache.get(user)!; + return await buildCache(user); + }, + /* load: async function (user: User) { if (user.appData) return; if (promises.has(user.id)) return promises.get(user.id)!; @@ -31,13 +41,18 @@ export const cache = { ); return promises.get(user.id)!; - }, + }, */ }; +function isCachableDbKey(key: keyof UserSchema): key is keyof CachedDbFields { + return cachedFields.includes(key as keyof CachedDbFields); +} + const storage = { - set: async function (user: User, field: K, value: CachedUser[K]) { + set: async function (user: User, field: K, value: UserSchema[K]) { + const cachedUser = await cache.get(user); await shardDb.query( - user.appData.dbHost, + cachedUser.dbHost, `INSERT INTO user (userId,${field}) VALUES @@ -46,55 +61,61 @@ const storage = { ${field} = ${mysql.escape(value)}`, ); - if (cachedFieldSet.has(field)) user.appData[field] = value; + if (isCachableDbKey(field)) { + // Typescript can't handle `cachedUser.db[_field] = value` + // when `value` is a mixed type, even with the type narrowing + // because the relationship between `K` and `value` is lost + Object.defineProperty(cachedUser.db, field, { value }); + } }, - increment: async function >( + increment: async function >( user: User, field: K, - value: CachedUser[K], + value: UserSchema[K], ) { + const cachedUser = await cache.get(user); await shardDb.query( - user.appData.dbHost, + cachedUser.dbHost, `INSERT INTO user (userId,${field}) VALUES (${user.id},DEFAULT(${field}) + ${mysql.escape( value, )}) ON DUPLICATE KEY UPDATE ${field} = ${field} + ${mysql.escape(value)}`, ); - if (cachedFieldSet.has(field)) user.appData[field]! += value; + if (isCachableDbKey(field)) { + cachedUser.db[field] += value; + } }, get: async function (user: User) { - const res = await shardDb.query( - user.appData.dbHost, + const cachedUser = await cache.get(user); + + const res = await shardDb.query( + cachedUser.dbHost, `SELECT * FROM user WHERE userId = ${user.id}`, ); if (res.length == 0) { if (!defaultAll) defaultAll = ( - await shardDb.query(user.appData.dbHost, `SELECT * FROM user WHERE userId = 0`) + await shardDb.query( + cachedUser.dbHost, + `SELECT * FROM user WHERE userId = 0`, + ) )[0]; return defaultAll; } else return res[0]; }, }; -async function buildCache(user: User) { +async function buildCache(user: User): Promise { const dbHost = await getDbHost(user.id); - let foundCache = await shardDb.query( + let foundCache = await shardDb.query( dbHost, `SELECT ${cachedFields.join(',')} FROM user WHERE userId = ${user.id}`, ); - let cache; - - if (foundCache.length > 0) cache = foundCache[0]; - else { - if (!defaultCache) await loadDefaultCache(dbHost); - cache = Object.assign({}, defaultCache); - } + const db = foundCache.length > 0 ? foundCache[0] : { ...(await loadDefaultCache(dbHost)) }; - cache.dbHost = dbHost; - user.appData = cache; + return { dbHost, db, cache: {} }; } async function getDbHost(userId: string): Promise { @@ -113,7 +134,9 @@ async function getDbHost(userId: string): Promise { } async function loadDefaultCache(dbHost: string) { - let res = await shardDb.query( + if (defaultCache) return defaultCache; + + let res = await shardDb.query( dbHost, `SELECT ${cachedFields.join(',')} FROM user WHERE userId = 0`, ); @@ -123,6 +146,7 @@ async function loadDefaultCache(dbHost: string) { res = await shardDb.query(dbHost, `SELECT ${cachedFields.join(',')} FROM user WHERE userId = 0`); defaultCache = res[0]; + return defaultCache; } export default { cache, storage }; diff --git a/src/bot/models/utilModel.ts b/src/bot/models/utilModel.ts index dd1e732..fecc8f9 100644 --- a/src/bot/models/utilModel.ts +++ b/src/bot/models/utilModel.ts @@ -1,5 +1,6 @@ import shardDb from '../../models/shardDb/shardDb.js'; import type { Guild } from 'discord.js'; +import guildModel from './guild/guildModel.js'; export interface LastActivities { textMessage: null | number; @@ -10,12 +11,13 @@ export interface LastActivities { } async function getLastActivities(guild: Guild, userId: string): Promise { + const { dbHost } = await guildModel.cache.get(guild); const keys = ['textMessage', 'voiceMinute', 'invite', 'vote', 'bonus']; const results = await Promise.all( keys.map((key) => shardDb.query<{ changeDate: number }[]>( - guild.appData.dbHost, + dbHost, `SELECT changeDate FROM ${key} WHERE guildId = ${guild.id} AND userId = ${userId} ORDER BY changeDate LIMIT 1`, ), ), @@ -30,8 +32,6 @@ async function getLastActivities(guild: Guild, userId: string): Promise; - texts: TextsData; - statFlushCache: Record; - botShardStat: { - commandsTotal: number; - textMessagesTotal: number; - }; -} declare module 'discord.js' { - export interface Role { - appData: CachedRole; - } - export interface User { - appData: CachedUser; - } export interface Client { - appData: ClientAppData; logger: pino.Logger; } - export interface Guild { - appData: CachedGuild; - } - // yes, it seems like we need to set these all individually :( - export interface CategoryChannel { - appData: CachedGuildChannel; - } - export interface NewsChannel { - appData: CachedGuildChannel; - } - export interface StageChannel { - appData: CachedGuildChannel; - } - export interface TextChannel { - appData: CachedGuildChannel; - } - export interface PrivateThreadChannel { - appData: CachedGuildChannel; - } - export interface PublicThreadChannel { - appData: CachedGuildChannel; - } - export interface VoiceChannel { - appData: CachedGuildChannel; - } - export interface ForumChannel { - appData: CachedGuildChannel; - } - // the channels end here - export interface GuildMember { - appData: CachedGuildMember; - } } diff --git a/src/models/managerDb/settingModel.ts b/src/models/managerDb/settingModel.ts index 0444a8f..0120c9a 100644 --- a/src/models/managerDb/settingModel.ts +++ b/src/models/managerDb/settingModel.ts @@ -1,22 +1,7 @@ -import type { Client } from 'discord.js'; import managerDb from './managerDb.js'; -import type { setting } from 'models/types/manager.js'; +import type { SettingSchema } from 'models/types/manager.js'; -export const storage = { - get: async function () { - const res = await managerDb.query('SELECT * from setting'); - - return Object.fromEntries(res.map((i) => [i.id, i.value])); - }, -}; - -export const cache = { - load: async function (client: Client) { - client.appData.settings = await storage.get(); - }, -}; - -export default { - storage, - cache, -}; +export async function getSettings() { + const res = await managerDb.query('SELECT * from setting'); + return Object.fromEntries(res.map((i) => [i.id, i.value])); +} diff --git a/src/models/managerDb/textModel.ts b/src/models/managerDb/textModel.ts index 255c6a9..cd7e68f 100644 --- a/src/models/managerDb/textModel.ts +++ b/src/models/managerDb/textModel.ts @@ -1,20 +1,6 @@ -import type { Client } from 'discord.js'; import managerDb from './managerDb.js'; import type { TextsData } from 'models/types/external.js'; -export const storage = { - get: async function () { - return await managerDb.fetch(null, '/api/texts', 'get'); - }, -}; - -export const cache = { - load: async function (client: Client) { - client.appData.texts = await storage.get(); - }, -}; - -export default { - storage, - cache, -}; +export async function getTexts() { + return await managerDb.fetch(null, '/api/texts', 'get'); +}