import {JsonCompatible, JsonValue} from '@croct/json'; import {hasher, HasherOptions} from 'node-object-hash'; import {CacheLoader, CacheProvider} from './cacheProvider'; export type Transformer = (value: D) => Promise | S; export type HashAlgorithm = 'passthrough' | 'md5' | 'sha1'; const HASHER = hasher({ // Support classes and instances like `ResourceId` coerce: true, // Do not differentiate Objects, Sets and Maps constructed in different order // `new Set([1,2,3])` should match `new Set([3,2,1])` sort: { array: false, typedArray: false, object: true, set: true, map: true, bigint: true, }, // Do not trim values, "foo " and "foo" should not match as the same key trim: false, }); type Configuration = { cache: CacheProvider, keyTransformer: Transformer, valueInputTransformer: Transformer, valueOutputTransformer: Transformer, }; function identity(value: T): T { return value; } /** * A cache provider to transform keys and values between composition layers. */ export class AdaptedCache implements CacheProvider { private readonly cache: CacheProvider; private readonly keyTransformer: Transformer; private readonly valueInputTransformer: Transformer; private readonly valueOutputTransformer: Transformer; public constructor(config: Configuration) { this.cache = config.cache; this.keyTransformer = config.keyTransformer; this.valueInputTransformer = config.valueInputTransformer; this.valueOutputTransformer = config.valueOutputTransformer; } public static transformKeys( cache: CacheProvider, keyTransformer: Transformer, ): AdaptedCache { return new AdaptedCache({ cache: cache, keyTransformer: keyTransformer, valueInputTransformer: identity, valueOutputTransformer: identity, }); } public static transformValues( cache: CacheProvider, inputTransformer: Transformer, outputTransformer: Transformer, ): AdaptedCache { return new AdaptedCache({ cache: cache, keyTransformer: identity, valueInputTransformer: inputTransformer, valueOutputTransformer: outputTransformer, }); } public async get(key: K, loader: CacheLoader): Promise { return this.cache .get( await this.keyTransformer(key), () => loader(key).then(this.valueInputTransformer), ) .then(this.valueOutputTransformer); } public async set(key: K, value: V): Promise { return this.cache.set( await this.keyTransformer(key), await this.valueInputTransformer(value), ); } public async delete(key: K): Promise { return this.cache.delete(await this.keyTransformer(key)); } public static createHashSerializer(algorithm?: HashAlgorithm): Transformer { if (algorithm === 'passthrough') { // Do not hash when algorithm is set to `passthrough` return HASHER.sort.bind(HASHER); } const options: HasherOptions = { enc: 'base64', alg: algorithm, }; return (value: any): string => HASHER.hash(value, options); } /** * Serializes a JSON compatible value to a string using JSON.stringify. * * This is a helper function for type safety. */ public static jsonSerializer(): Transformer { return JSON.stringify; } /** * Deserializes a string into a JSON value using JSON.parse. * * This is a helper function for type safety. */ public static jsonDeserializer(): Transformer { return JSON.parse; } }