feat: limitedMemoryAdapter

This commit is contained in:
MARCROCK22 2024-04-14 20:08:26 -04:00
parent aa26766b38
commit c573ca9909
6 changed files with 282 additions and 11 deletions

View File

@ -1,4 +1,5 @@
export * from './default'; export * from './default';
export * from './limited';
export * from './redis'; export * from './redis';
export * from './types'; export * from './types';
export * from './workeradapter'; export * from './workeradapter';

249
src/cache/adapters/limited.ts vendored Normal file
View File

@ -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<string, LimitedCollection<string, string>>();
readonly relationships = new Map<string, Map<string, string[]>>();
options: MakeRequired<LimitedMemoryAdapterOptions, 'default'>;
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<string, string[]>());
const relation = this.relationships.get(key)!;
if (!relation.has(to)) {
relation.set(to, []);
}
return relation!.get(to)!;
}
bulkAddToRelationShip(data: Record<string, string[]>) {
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<string, string[]>());
}
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);
}
}
}

4
src/cache/index.ts vendored
View File

@ -362,8 +362,8 @@ export class Cache {
} }
} }
await this.adapter.patch(false, allData);
await this.adapter.bulkAddToRelationShip(relationshipsData); await this.adapter.bulkAddToRelationShip(relationshipsData);
await this.adapter.patch(false, allData);
} }
async bulkSet( async bulkSet(
@ -449,8 +449,8 @@ export class Cache {
} }
} }
await this.adapter.set(allData);
await this.adapter.bulkAddToRelationShip(relationshipsData); await this.adapter.bulkAddToRelationShip(relationshipsData);
await this.adapter.set(allData);
} }
async onPacket(event: GatewayDispatchPayload) { async onPacket(event: GatewayDispatchPayload) {

View File

@ -39,7 +39,7 @@ export class Members extends GuildBasedResource {
members members
.map(rawMember => { .map(rawMember => {
const user = users.find(x => x.id === rawMember.id); 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[], .filter(Boolean) as GuildMember[],
), ),

View File

@ -2,5 +2,5 @@ import type { APIStageInstance } from 'discord-api-types/v10';
import { GuildRelatedResource } from './default/guild-related'; import { GuildRelatedResource } from './default/guild-related';
export class StageInstances extends GuildRelatedResource<APIStageInstance> { export class StageInstances extends GuildRelatedResource<APIStageInstance> {
namespace = 'stage_instances'; namespace = 'stage_instance';
} }

View File

@ -194,9 +194,10 @@ export class Collection<K, V> extends Map<K, V> {
type LimitedCollectionData<V> = { expire: number; expireOn: number; value: V }; type LimitedCollectionData<V> = { expire: number; expireOn: number; value: V };
export interface LimitedCollectionOptions { export interface LimitedCollectionOptions<K> {
limit: number; limit: number;
expire: number; expire: number;
onDelete?: (key: K) => void;
resetOnDemand: boolean; resetOnDemand: boolean;
} }
@ -214,7 +215,7 @@ export interface LimitedCollectionOptions {
* console.log(mappedArray); // Output: ['1: one', '2: two', '3: three'] * console.log(mappedArray); // Output: ['1: one', '2: two', '3: three']
*/ */
export class LimitedCollection<K, V> { export class LimitedCollection<K, V> {
static readonly default: LimitedCollectionOptions = { static readonly default: LimitedCollectionOptions<any> = {
resetOnDemand: false, resetOnDemand: false,
limit: Number.POSITIVE_INFINITY, limit: Number.POSITIVE_INFINITY,
expire: 0, expire: 0,
@ -222,10 +223,10 @@ export class LimitedCollection<K, V> {
private readonly data = new Map<K, LimitedCollectionData<V>>(); private readonly data = new Map<K, LimitedCollectionData<V>>();
private readonly options: LimitedCollectionOptions; private readonly options: LimitedCollectionOptions<K>;
private timeout: NodeJS.Timeout | undefined = undefined; private timeout: NodeJS.Timeout | undefined = undefined;
constructor(options: Partial<LimitedCollectionOptions> = {}) { constructor(options: Partial<LimitedCollectionOptions<K>> = {}) {
this.options = MergeOptions(LimitedCollection.default, options); this.options = MergeOptions(LimitedCollection.default, options);
} }
@ -258,7 +259,8 @@ export class LimitedCollection<K, V> {
if (this.size > this.options.limit) { if (this.size > this.options.limit) {
const iter = this.data.keys(); const iter = this.data.keys();
while (this.size > this.options.limit) { 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<K, V> {
if (this.options.resetOnDemand && data && data.expire !== -1) { if (this.options.resetOnDemand && data && data.expire !== -1) {
const oldExpireOn = data.expireOn; const oldExpireOn = data.expireOn;
data.expireOn = Date.now() + data.expire; data.expireOn = Date.now() + data.expire;
if (this.closer!.expireOn === oldExpireOn) { if (this.closer?.expireOn === oldExpireOn) {
this.resetTimeout(); this.resetTimeout();
} }
} }
@ -329,7 +331,8 @@ export class LimitedCollection<K, V> {
*/ */
delete(key: K) { delete(key: K) {
const value = this.raw(key); 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); return this.data.delete(key);
} }
@ -398,12 +401,30 @@ export class LimitedCollection<K, V> {
}, expireOn - Date.now()); }, 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() { private clearExpired() {
for (const [key, value] of this.data) { for (const [key, value] of this.data) {
if (value.expireOn === -1) { if (value.expireOn === -1) {
continue; continue;
} }
if (Date.now() >= value.expireOn) { if (Date.now() >= value.expireOn) {
this.options.onDelete?.(key);
this.data.delete(key); this.data.delete(key);
} }
} }