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:
Jérémy M
2023-11-21 18:29:31 +01:00
committed by GitHub
parent 74e0122294
commit dd125ddfcc
27 changed files with 458 additions and 17 deletions

View File

@ -0,0 +1,3 @@
export enum MemoryStorageType {
Local = 'local',
}

View File

@ -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);
};

View File

@ -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>;
}

View File

@ -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}`;
}
}

View File

@ -0,0 +1 @@
export * from './memory-storage.interface';

View File

@ -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'>;

View File

@ -0,0 +1 @@
export const MEMORY_STORAGE_SERVICE = 'MEMORY_STORAGE_SERVICE';

View File

@ -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}`);
}
}
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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}`;
};

View File

@ -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;
}
}

View File

@ -0,0 +1,4 @@
export interface MemoryStorageSerializer<T> {
serialize(item: T): string;
deserialize(data: string): T;
}

View File

@ -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;
}
}