UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

215 lines (193 loc) 6.18 kB
import { CacheError } from "../errors/CacheError.ts"; /** * Cache provider interface. * * All methods are asynchronous and return promises. * Values are stored as Uint8Array. */ export abstract class CacheProvider { protected encoder: TextEncoder = new TextEncoder(); protected decoder: TextDecoder = new TextDecoder(); protected codes = { BINARY: 0x01, JSON: 0x02, STRING: 0x03, COMPRESSED: 0x04, }; /** * Get the value of a key. * * @param name Cache name, used to group keys. Should be Redis-like "some:group:name" format. * @param key The key of the value to get. * * @return The value of the key, or undefined if the key does not exist. */ public abstract get( name: string, key: string, ): Promise<Uint8Array | undefined>; /** * Set the string value of a key. * * @param name Cache name, used to group keys. Should be Redis-like "some:group:name" format. * @param key The key of the value to set. * @param value The value to set. * @param ttl The time-to-live of the key, in milliseconds. * * @return The value of the key. */ public abstract set( name: string, key: string, value: Uint8Array, ttl?: number, ): Promise<Uint8Array>; /** * Remove the specified keys. * * @param name Cache name, used to group keys. Should be Redis-like "some:group:name" format. * @param keys The keys to delete. */ public abstract del(name: string, ...keys: string[]): Promise<void>; public abstract has(name: string, key: string): Promise<boolean>; public abstract keys(name: string, filter?: string): Promise<string[]>; /** * Remove all keys from all cache names. */ public abstract clear(): Promise<void>; /** * Increment the integer value of a key by the given amount. * * If the key does not exist, it is set to 0 before performing the operation. * This operation is atomic when using Redis. * * @param name Cache name, used to group keys. * @param key The key to increment. * @param amount The amount to increment by. * @returns The new value after incrementing. */ public abstract incr( name: string, key: string, amount: number, ): Promise<number>; // --------------------------------------------------------------------------- // High-level methods — serialize/compress layer used by CachePrimitive // --------------------------------------------------------------------------- /** * Set a typed value with automatic serialization and optional compression. */ public async setTyped( name: string, key: string, value: unknown, options?: { ttl?: number; compress?: boolean }, ): Promise<void> { let data = this.serialize(value); if (options?.compress) { data = await this.compress(data); } await this.set(name, key, data, options?.ttl); } /** * Get a typed value with automatic deserialization and optional decompression. */ public async getTyped<T>(name: string, key: string): Promise<T | undefined> { const data = await this.get(name, key); if (data) { if (data[0] === this.codes.COMPRESSED) { const decompressed = await this.decompress(data.subarray(1)); return this.deserialize<T>(decompressed); } return this.deserialize<T>(data); } return undefined; } /** * Invalidate cache keys with wildcard support. * * Keys ending in `*` are expanded via `this.keys()`. * Called with no keys, delegates to `this.del(name)` which clears the entire container. */ public async invalidateKeys(name: string, keys: string[]): Promise<void> { const keysToDelete: string[] = []; for (const key of keys) { if (key.endsWith("*")) { const result = await this.keys(name, key.slice(0, -1)); keysToDelete.push(...result); } else { keysToDelete.push(key); } } await this.del(name, ...keysToDelete); } /** * Serialize a value to a typed Uint8Array with a leading type marker byte. */ protected serialize(value: unknown): Uint8Array { if (value instanceof Uint8Array) { const result = new Uint8Array(1 + value.length); result[0] = this.codes.BINARY; result.set(value, 1); return result; } const encoded = this.encoder.encode( typeof value === "string" ? value : JSON.stringify(value), ); const code = typeof value === "string" ? this.codes.STRING : this.codes.JSON; const result = new Uint8Array(1 + encoded.length); result[0] = code; result.set(encoded, 1); return result; } /** * Deserialize a typed Uint8Array back to the original value. */ protected deserialize<T>(uint8Array: Uint8Array): T { const type = uint8Array[0]; const payload = uint8Array.slice(1); if (type === this.codes.BINARY) { return payload as T; } if (type === this.codes.JSON) { return JSON.parse(this.decoder.decode(payload)) as T; } if (type === this.codes.STRING) { return this.decoder.decode(payload) as T; } throw new CacheError(`Unknown serialization type: ${type}`); } /** * Compress data with gzip, prepending a COMPRESSED marker byte. */ protected async compress(data: Uint8Array): Promise<Uint8Array> { const buf = (data.buffer as ArrayBuffer).slice( data.byteOffset, data.byteOffset + data.byteLength, ); const compressed = new Uint8Array( await new Response( new Blob([buf]).stream().pipeThrough(new CompressionStream("gzip")), ).arrayBuffer(), ); const result = new Uint8Array(1 + compressed.length); result[0] = this.codes.COMPRESSED; result.set(compressed, 1); return result; } /** * Decompress gzipped data. */ protected async decompress(data: Uint8Array): Promise<Uint8Array> { const buf = (data.buffer as ArrayBuffer).slice( data.byteOffset, data.byteOffset + data.byteLength, ); return new Uint8Array( await new Response( new Blob([buf]).stream().pipeThrough(new DecompressionStream("gzip")), ).arrayBuffer(), ); } }