feat: add memory cache to boost performance (#2620)
* feat: add memory cache to boost performance * fix: tests * fix: logging * fix: missing commented stuff
This commit is contained in:
@ -0,0 +1,3 @@
|
||||
export enum MemoryStorageType {
|
||||
Local = 'local',
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import { createMemoryStorageInjectionToken } from 'src/integrations/memory-storage/memory-storage.util';
|
||||
|
||||
export const InjectMemoryStorage = (identifier: string) => {
|
||||
const injectionToken = createMemoryStorageInjectionToken(identifier);
|
||||
|
||||
return Inject(injectionToken);
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
export interface MemoryStorageDriver<T> {
|
||||
read(params: { key: string }): Promise<T | null>;
|
||||
write(params: { key: string; data: T }): Promise<void>;
|
||||
delete(params: { key: string }): Promise<void>;
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
import { MemoryStorageDriver } from './interfaces/memory-storage-driver.interface';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface LocalMemoryDriverOptions {}
|
||||
|
||||
export class LocalMemoryDriver<T> implements MemoryStorageDriver<T> {
|
||||
private identifier: string;
|
||||
private options: LocalMemoryDriverOptions;
|
||||
private serializer: MemoryStorageSerializer<T>;
|
||||
private storage: Map<string, string> = new Map();
|
||||
|
||||
constructor(
|
||||
identifier: string,
|
||||
options: LocalMemoryDriverOptions,
|
||||
serializer: MemoryStorageSerializer<T>,
|
||||
) {
|
||||
this.identifier = identifier;
|
||||
this.options = options;
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
async write(params: { key: string; data: T }): Promise<void> {
|
||||
const compositeKey = this.generateCompositeKey(params.key);
|
||||
const serializedData = this.serializer.serialize(params.data);
|
||||
|
||||
this.storage.set(compositeKey, serializedData);
|
||||
}
|
||||
|
||||
async read(params: { key: string }): Promise<T | null> {
|
||||
const compositeKey = this.generateCompositeKey(params.key);
|
||||
|
||||
if (!this.storage.has(compositeKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = this.storage.get(compositeKey)!;
|
||||
const deserializeData = this.serializer.deserialize(data);
|
||||
|
||||
return deserializeData;
|
||||
}
|
||||
|
||||
async delete(params: { key: string }): Promise<void> {
|
||||
const compositeKey = this.generateCompositeKey(params.key);
|
||||
|
||||
if (!this.storage.has(compositeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.delete(compositeKey);
|
||||
}
|
||||
|
||||
private generateCompositeKey(key: string): string {
|
||||
return `${this.identifier}:${key}`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './memory-storage.interface';
|
||||
@ -0,0 +1,29 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface';
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
import { LocalMemoryDriverOptions } from 'src/integrations/memory-storage/drivers/local.driver';
|
||||
|
||||
export interface LocalMemoryDriverFactoryOptions {
|
||||
type: MemoryStorageType.Local;
|
||||
options: LocalMemoryDriverOptions;
|
||||
}
|
||||
|
||||
interface MemoryStorageModuleBaseOptions {
|
||||
identifier: string;
|
||||
serializer?: MemoryStorageSerializer<any>;
|
||||
}
|
||||
|
||||
export type MemoryStorageModuleOptions = MemoryStorageModuleBaseOptions &
|
||||
LocalMemoryDriverFactoryOptions;
|
||||
|
||||
export type MemoryStorageModuleAsyncOptions = {
|
||||
identifier: string;
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) =>
|
||||
| Omit<MemoryStorageModuleOptions, 'identifier'>
|
||||
| Promise<Omit<MemoryStorageModuleOptions, 'identifier'>>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1 @@
|
||||
export const MEMORY_STORAGE_SERVICE = 'MEMORY_STORAGE_SERVICE';
|
||||
@ -0,0 +1,73 @@
|
||||
import { DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface';
|
||||
|
||||
import { MemoryStorageDefaultSerializer } from 'src/integrations/memory-storage/serializers/default.serializer';
|
||||
import { createMemoryStorageInjectionToken } from 'src/integrations/memory-storage/memory-storage.util';
|
||||
|
||||
import {
|
||||
MemoryStorageModuleAsyncOptions,
|
||||
MemoryStorageModuleOptions,
|
||||
} from './interfaces';
|
||||
|
||||
import { LocalMemoryDriver } from './drivers/local.driver';
|
||||
|
||||
@Global()
|
||||
export class MemoryStorageModule {
|
||||
static forRoot(options: MemoryStorageModuleOptions): DynamicModule {
|
||||
// Dynamic injection token to allow multiple instances of the same driver
|
||||
const injectionToken = createMemoryStorageInjectionToken(
|
||||
options.identifier,
|
||||
);
|
||||
const provider = {
|
||||
provide: injectionToken,
|
||||
useValue: this.createStorageDriver(options),
|
||||
};
|
||||
|
||||
return {
|
||||
module: MemoryStorageModule,
|
||||
providers: [provider],
|
||||
exports: [provider],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: MemoryStorageModuleAsyncOptions): DynamicModule {
|
||||
// Dynamic injection token to allow multiple instances of the same driver
|
||||
const injectionToken = createMemoryStorageInjectionToken(
|
||||
options.identifier,
|
||||
);
|
||||
const provider = {
|
||||
provide: injectionToken,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
return this.createStorageDriver({
|
||||
identifier: options.identifier,
|
||||
...config,
|
||||
});
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
return {
|
||||
module: MemoryStorageModule,
|
||||
imports: options.imports || [],
|
||||
providers: [provider],
|
||||
exports: [provider],
|
||||
};
|
||||
}
|
||||
|
||||
private static createStorageDriver(options: MemoryStorageModuleOptions) {
|
||||
switch (options.type) {
|
||||
case MemoryStorageType.Local:
|
||||
return new LocalMemoryDriver(
|
||||
options.identifier,
|
||||
options.options,
|
||||
options.serializer ?? new MemoryStorageDefaultSerializer<string>(),
|
||||
);
|
||||
// Future case for Redis or other types
|
||||
default:
|
||||
throw new Error(`Unsupported storage type: ${options.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { MemoryStorageService } from './memory-storage.service';
|
||||
|
||||
describe('MemoryStorageService', () => {
|
||||
let service: MemoryStorageService<any>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [MemoryStorageService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MemoryStorageService<any>>(MemoryStorageService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { MemoryStorageDriver } from 'src/integrations/memory-storage/drivers/interfaces/memory-storage-driver.interface';
|
||||
|
||||
export class MemoryStorageService<T> implements MemoryStorageDriver<T> {
|
||||
private driver: MemoryStorageDriver<T>;
|
||||
|
||||
constructor(driver: MemoryStorageDriver<T>) {
|
||||
this.driver = driver;
|
||||
}
|
||||
|
||||
write(params: { key: string; data: T }): Promise<void> {
|
||||
return this.driver.write(params);
|
||||
}
|
||||
|
||||
read(params: { key: string }): Promise<T | null> {
|
||||
return this.driver.read(params);
|
||||
}
|
||||
|
||||
delete(params: { key: string }): Promise<void> {
|
||||
return this.driver.delete(params);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { MEMORY_STORAGE_SERVICE } from 'src/integrations/memory-storage/memory-storage.constants';
|
||||
|
||||
export const createMemoryStorageInjectionToken = (identifier: string) => {
|
||||
return `${MEMORY_STORAGE_SERVICE}_${identifier}`;
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
export class MemoryStorageDefaultSerializer<T>
|
||||
implements MemoryStorageSerializer<T>
|
||||
{
|
||||
serialize(item: T): string {
|
||||
if (typeof item !== 'string') {
|
||||
throw new Error('DefaultSerializer can only serialize strings');
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
deserialize(data: string): T {
|
||||
return data as unknown as T;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export interface MemoryStorageSerializer<T> {
|
||||
serialize(item: T): string;
|
||||
deserialize(data: string): T;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
export class MemoryStorageJsonSerializer<T>
|
||||
implements MemoryStorageSerializer<T>
|
||||
{
|
||||
serialize(item: T): string {
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
|
||||
deserialize(data: string): T {
|
||||
return JSON.parse(data) as T;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user