diff --git a/src/cache/adapters/index.ts b/src/cache/adapters/index.ts index d626220..d72d574 100644 --- a/src/cache/adapters/index.ts +++ b/src/cache/adapters/index.ts @@ -1,4 +1,5 @@ export * from './default'; +export * from './limited'; export * from './redis'; export * from './types'; export * from './workeradapter'; diff --git a/src/cache/adapters/limited.ts b/src/cache/adapters/limited.ts new file mode 100644 index 0000000..3bc58fe --- /dev/null +++ b/src/cache/adapters/limited.ts @@ -0,0 +1,249 @@ +import { LimitedCollection } from '../..'; +import { MergeOptions, type MakeRequired } from '../../common'; +import type { Adapter } from './types'; + +export interface ResourceLimitedMemoryAdapter { + expire?: number; + limit?: number; +} + +export interface LimitedMemoryAdapterOptions { + default?: ResourceLimitedMemoryAdapter; + guild?: ResourceLimitedMemoryAdapter; + user?: ResourceLimitedMemoryAdapter; + member?: ResourceLimitedMemoryAdapter; + voice_state?: ResourceLimitedMemoryAdapter; + channel?: ResourceLimitedMemoryAdapter; + emoji?: ResourceLimitedMemoryAdapter; + overwrite?: ResourceLimitedMemoryAdapter; + presence?: ResourceLimitedMemoryAdapter; + role?: ResourceLimitedMemoryAdapter; + stage_instance?: ResourceLimitedMemoryAdapter; + sticker?: ResourceLimitedMemoryAdapter; + thread?: ResourceLimitedMemoryAdapter; +} + +export class LimitedMemoryAdapter implements Adapter { + isAsync = false; + + readonly storage = new Map>(); + readonly relationships = new Map>(); + + options: MakeRequired; + + constructor(options: LimitedMemoryAdapterOptions) { + this.options = MergeOptions( + { + default: { + expire: undefined, + limit: Number.POSITIVE_INFINITY, + }, + } satisfies LimitedMemoryAdapterOptions, + options, + ); + } + + scan(query: string, keys?: false): any[]; + scan(query: string, keys: true): string[]; + scan(query: string, keys = false) { + const values = []; + const sq = query.split('.'); + for (const iterator of [...this.storage.values()].flatMap(x => x.entries())) + for (const [key, value] of iterator) { + if (key.split('.').every((value, i) => (sq[i] === '*' ? !!value : sq[i] === value))) { + values.push(keys ? key : JSON.parse(value.value)); + } + } + + return values; + } + + get(keys: string): any; + get(keys: string[]): any[]; + get(keys: string | string[]) { + if (!Array.isArray(keys)) { + const data = this.storage.get(keys.split('.')[0])?.get(keys); + return data ? JSON.parse(data) : null; + } + return keys + .map(x => { + const data = this.storage.get(x.split('.')[0])?.get(x); + return data ? JSON.parse(data) : null; + }) + .filter(x => x); + } + + private __set(key: string, data: any) { + const namespace = key.split('.')[0]; + const self = this; + if (!this.storage.has(namespace)) { + this.storage.set( + namespace, + new LimitedCollection({ + expire: this.options[namespace as keyof LimitedMemoryAdapterOptions]?.expire ?? this.options.default.expire, + limit: this.options[namespace as keyof LimitedMemoryAdapterOptions]?.limit ?? this.options.default.limit, + resetOnDemand: true, + onDelete(k) { + const relation = self.relationships.get(namespace); + if (relation) { + switch (namespace) { + case 'guild': + case 'user': + self.removeToRelationship(namespace, k.split('.')[1]); + break; + case 'member': + case 'voice_state': + { + const split = k.split('.'); + self.removeToRelationship(`${namespace}.${split[1]}`, split[2]); + } + break; + case 'channel': + case 'emoji': + case 'overwrite': + case 'presence': + case 'role': + case 'stage_instance': + case 'sticker': + case 'thread': + { + const split = k.split('.'); + for (const i of relation.entries()) { + if (i[1].includes(split[1])) { + self.removeToRelationship(i[0], split[1]); + break; + } + } + } + break; + } + } + }, + }), + ); + } + + this.storage.get(namespace)!.set(key, JSON.stringify(data)); + } + + set(keys: string, data: any): void; + set(keys: [string, any][]): void; + set(keys: string | [string, any][], data?: any): void { + if (Array.isArray(keys)) { + for (const [key, value] of keys) { + this.__set(key, value); + } + } else { + this.__set(keys, data); + } + } + + patch(updateOnly: boolean, keys: string, data: any): void; + patch(updateOnly: boolean, keys: [string, any][]): void; + patch(updateOnly: boolean, keys: string | [string, any][], data?: any): void { + if (Array.isArray(keys)) { + for (const [key, value] of keys) { + const oldData = this.get(key); + if (updateOnly && !oldData) { + continue; + } + this.__set(key, Array.isArray(value) ? value : { ...(oldData ?? {}), ...value }); + } + } else { + const oldData = this.get(keys); + if (updateOnly && !oldData) { + return; + } + this.__set(keys, Array.isArray(data) ? data : { ...(oldData ?? {}), ...data }); + } + } + + values(to: string) { + const array: any[] = []; + const data = this.keys(to); + + for (const key of data) { + const content = this.get(key); + + if (content) { + array.push(content); + } + } + + return array; + } + + keys(to: string) { + return this.getToRelationship(to).map(id => `${to}.${id}`); + } + + count(to: string) { + return this.getToRelationship(to).length; + } + + remove(keys: string): void; + remove(keys: string[]): void; + remove(keys: string | string[]) { + for (const i of Array.isArray(keys) ? keys : [keys]) { + this.storage.get(i.split('.')[0])?.delete(i); + } + } + + flush(): void { + this.storage.clear(); + this.relationships.clear(); + } + + contains(to: string, keys: string): boolean { + return this.getToRelationship(to).includes(keys); + } + + getToRelationship(to: string) { + const key = to.split('.')[0]; + if (!this.relationships.has(key)) this.relationships.set(key, new Map()); + const relation = this.relationships.get(key)!; + if (!relation.has(to)) { + relation.set(to, []); + } + return relation!.get(to)!; + } + + bulkAddToRelationShip(data: Record) { + for (const i in data) { + this.addToRelationship(i, data[i]); + } + } + + addToRelationship(to: string, keys: string | string[]) { + const key = to.split('.')[0]; + if (!this.relationships.has(key)) { + this.relationships.set(key, new Map()); + } + + const data = this.getToRelationship(to); + + for (const key of Array.isArray(keys) ? keys : [keys]) { + if (!data.includes(key)) { + data.push(key); + } + } + } + + removeToRelationship(to: string, keys: string | string[]) { + const data = this.getToRelationship(to); + if (data) { + for (const key of Array.isArray(keys) ? keys : [keys]) { + const idx = data.indexOf(key); + if (idx !== -1) { + data.splice(idx, 1); + } + } + } + } + + removeRelationship(to: string | string[]) { + for (const i of Array.isArray(to) ? to : [to]) { + this.relationships.delete(i); + } + } +} diff --git a/src/cache/index.ts b/src/cache/index.ts index 37ea27e..9ffe174 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -362,8 +362,8 @@ export class Cache { } } - await this.adapter.patch(false, allData); await this.adapter.bulkAddToRelationShip(relationshipsData); + await this.adapter.patch(false, allData); } async bulkSet( @@ -449,8 +449,8 @@ export class Cache { } } - await this.adapter.set(allData); await this.adapter.bulkAddToRelationShip(relationshipsData); + await this.adapter.set(allData); } async onPacket(event: GatewayDispatchPayload) { diff --git a/src/cache/resources/members.ts b/src/cache/resources/members.ts index 95685d0..ffdda7a 100644 --- a/src/cache/resources/members.ts +++ b/src/cache/resources/members.ts @@ -39,7 +39,7 @@ export class Members extends GuildBasedResource { members .map(rawMember => { const user = users.find(x => x.id === rawMember.id); - return user ? new GuildMember(this.client, rawMember, user, guild) : undefined; + return user ? new GuildMember(this.client, rawMember, user, rawMember.guild_id) : undefined; }) .filter(Boolean) as GuildMember[], ), diff --git a/src/cache/resources/stage-instances.ts b/src/cache/resources/stage-instances.ts index ff49fdf..8486258 100644 --- a/src/cache/resources/stage-instances.ts +++ b/src/cache/resources/stage-instances.ts @@ -2,5 +2,5 @@ import type { APIStageInstance } from 'discord-api-types/v10'; import { GuildRelatedResource } from './default/guild-related'; export class StageInstances extends GuildRelatedResource { - namespace = 'stage_instances'; + namespace = 'stage_instance'; } diff --git a/src/collection.ts b/src/collection.ts index 576aa68..dac2e79 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -194,9 +194,10 @@ export class Collection extends Map { type LimitedCollectionData = { expire: number; expireOn: number; value: V }; -export interface LimitedCollectionOptions { +export interface LimitedCollectionOptions { limit: number; expire: number; + onDelete?: (key: K) => void; resetOnDemand: boolean; } @@ -214,7 +215,7 @@ export interface LimitedCollectionOptions { * console.log(mappedArray); // Output: ['1: one', '2: two', '3: three'] */ export class LimitedCollection { - static readonly default: LimitedCollectionOptions = { + static readonly default: LimitedCollectionOptions = { resetOnDemand: false, limit: Number.POSITIVE_INFINITY, expire: 0, @@ -222,10 +223,10 @@ export class LimitedCollection { private readonly data = new Map>(); - private readonly options: LimitedCollectionOptions; + private readonly options: LimitedCollectionOptions; private timeout: NodeJS.Timeout | undefined = undefined; - constructor(options: Partial = {}) { + constructor(options: Partial> = {}) { this.options = MergeOptions(LimitedCollection.default, options); } @@ -258,7 +259,8 @@ export class LimitedCollection { if (this.size > this.options.limit) { const iter = this.data.keys(); while (this.size > this.options.limit) { - this.delete(iter.next().value); + const keyValue = iter.next().value; + this.delete(keyValue); } } @@ -296,7 +298,7 @@ export class LimitedCollection { if (this.options.resetOnDemand && data && data.expire !== -1) { const oldExpireOn = data.expireOn; data.expireOn = Date.now() + data.expire; - if (this.closer!.expireOn === oldExpireOn) { + if (this.closer?.expireOn === oldExpireOn) { this.resetTimeout(); } } @@ -329,7 +331,8 @@ export class LimitedCollection { */ delete(key: K) { const value = this.raw(key); - if (value && value.expireOn === this.closer!.expireOn) setImmediate(() => this.resetTimeout()); + if (value && value.expireOn === this.closer?.expireOn) setImmediate(() => this.resetTimeout()); + this.options.onDelete?.(key); return this.data.delete(key); } @@ -398,12 +401,30 @@ export class LimitedCollection { }, expireOn - Date.now()); } + keys() { + return this.data.keys; + } + + values() { + return this.data.values; + } + + entries() { + return this.data.entries(); + } + + clear() { + this.data.clear(); + this.resetTimeout(); + } + private clearExpired() { for (const [key, value] of this.data) { if (value.expireOn === -1) { continue; } if (Date.now() >= value.expireOn) { + this.options.onDelete?.(key); this.data.delete(key); } }