From 088fdfb1ef88a7c9ec7f31152350dc9774cedefd Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 9 Mar 2021 10:16:27 -0600 Subject: [PATCH] Add cache interface, factory, and basic in-mem implementation --- src/index.ts | 4 ++ src/lifecycle/Application.ts | 3 ++ src/support/cache/Cache.ts | 31 +++++++++++++ src/support/cache/CacheFactory.ts | 72 +++++++++++++++++++++++++++++++ src/support/cache/MemoryCache.ts | 28 ++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 src/support/cache/Cache.ts create mode 100644 src/support/cache/CacheFactory.ts create mode 100644 src/support/cache/MemoryCache.ts diff --git a/src/index.ts b/src/index.ts index d9b2d11..620dd4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,6 +59,10 @@ export * from './service/HTTPServer' export * from './service/Routing' export * from './service/Middlewares' +export * from './support/cache/Cache' +export * from './support/cache/MemoryCache' +export * from './support/cache/CacheFactory' + export * from './views/ViewEngine' export * from './views/ViewEngineFactory' export * from './views/PugViewEngine' diff --git a/src/lifecycle/Application.ts b/src/lifecycle/Application.ts index 75f4bc7..344e919 100644 --- a/src/lifecycle/Application.ts +++ b/src/lifecycle/Application.ts @@ -14,6 +14,7 @@ import {Logging} from '../service/Logging'; import {RunLevelErrorHandler} from "./RunLevelErrorHandler"; import {Unit, UnitStatus} from "./Unit"; import * as dotenv from 'dotenv'; +import {CacheFactory} from "../support/cache/CacheFactory"; export function env(key: string, defaultValue?: any): any { return Application.getApplication().env(key, defaultValue) @@ -103,6 +104,8 @@ export class Application extends Container { this.bootstrapEnvironment() this.setupLogging() + this.registerFactory(new CacheFactory()) // FIXME move this somewhere else? + this.make(Logging).debug(`Application root: ${this.baseDir}`) } diff --git a/src/support/cache/Cache.ts b/src/support/cache/Cache.ts new file mode 100644 index 0000000..809a78c --- /dev/null +++ b/src/support/cache/Cache.ts @@ -0,0 +1,31 @@ +/** + * Abstract interface class for an application cache object. + */ +export abstract class Cache { + /** + * Fetch a value from the cache by its key. + * @param {string} key + * @return Promise + */ + public abstract fetch(key: string): string | undefined | Promise; + + /** + * Store the given value in the cache by key. + * @param {string} key + * @param {string} value + */ + public abstract put(key: string, value: string): void | Promise; + + /** + * Check if the cache has the given key. + * @param {string} key + * @return Promise + */ + public abstract has(key: string): boolean | Promise; + + /** + * Drop the given key from the cache. + * @param {string} key + */ + public abstract drop(key: string): void | Promise; +} diff --git a/src/support/cache/CacheFactory.ts b/src/support/cache/CacheFactory.ts new file mode 100644 index 0000000..da61609 --- /dev/null +++ b/src/support/cache/CacheFactory.ts @@ -0,0 +1,72 @@ +import { + AbstractFactory, + Container, + DependencyRequirement, + PropertyDependency, + isInstantiable, + DEPENDENCY_KEYS_METADATA_KEY, + DEPENDENCY_KEYS_PROPERTY_METADATA_KEY +} from "@extollo/di" +import {Collection, ErrorWithContext} from "@extollo/util" +import {Logging} from "../../service/Logging"; +import {Config} from "../../service/Config"; +import {Cache} from "./Cache" +import {MemoryCache} from "./MemoryCache"; + +export class CacheFactory extends AbstractFactory { + protected readonly logging: Logging + protected readonly config: Config + + private static loggedMemoryCacheWarningOnce = false + + constructor() { + super({}) + this.logging = Container.getContainer().make(Logging) + this.config = Container.getContainer().make(Config) + } + + produce(dependencies: any[], parameters: any[]): Cache { + return new (this.getCacheClass()) + } + + match(something: any) { + return something === Cache + } + + getDependencyKeys(): Collection { + const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.getCacheClass()) + if ( meta ) return meta + return new Collection() + } + + getInjectedProperties(): Collection { + const meta = new Collection() + let currentToken = this.getCacheClass() + + do { + const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken) + if ( loadedMeta ) meta.concat(loadedMeta) + currentToken = Object.getPrototypeOf(currentToken) + } while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype) + + return meta + } + + protected getCacheClass() { + const CacheClass = this.config.get('server.cache.driver', MemoryCache) + if ( CacheClass === MemoryCache && !CacheFactory.loggedMemoryCacheWarningOnce ) { + this.logging.warn(`You are using the default memory-based cache driver. It is recommended you configure a persistent cache driver instead.`) + CacheFactory.loggedMemoryCacheWarningOnce = true + } + + if ( !isInstantiable(CacheClass) || !(CacheClass.prototype instanceof Cache) ) { + const e = new ErrorWithContext('Provided session class does not extend from @extollo/lib.Cache'); + e.context = { + config_key: 'server.cache.driver', + class: CacheClass.toString(), + } + } + + return CacheClass + } +} diff --git a/src/support/cache/MemoryCache.ts b/src/support/cache/MemoryCache.ts new file mode 100644 index 0000000..2c5f878 --- /dev/null +++ b/src/support/cache/MemoryCache.ts @@ -0,0 +1,28 @@ +import {Collection} from "@extollo/util" +import {Cache} from "./Cache" + +export class MemoryCache extends Cache { + private static cacheItems: Collection<{key: string, value: string}> = new Collection<{key: string; value: string}>() + + public fetch(key: string): string | Promise | undefined { + return MemoryCache.cacheItems + .firstWhere('key', '=', key)?.value + } + + public put(key: string, value: string): void | Promise { + const existing = MemoryCache.cacheItems.firstWhere('key', '=', key) + if ( existing ) { + existing.value = value + } else { + MemoryCache.cacheItems.push({ key, value }) + } + } + + public has(key: string): boolean | Promise { + return !!MemoryCache.cacheItems.firstWhere('key', '=', key) + } + + public drop(key: string): void | Promise { + MemoryCache.cacheItems = MemoryCache.cacheItems.where('key', '!=', key) + } +}