@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
258 lines (222 loc) • 6.3 kB
text/typescript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { LRUCache } from 'lru-cache';
import type MemoryUsage from './MemoryUsage';
import { isMemoryUsage, type GetMemoryUsageContext } from './MemoryUsage';
/**
* The options for a cache entry.
*/
export interface CacheOptions {
/**
* The time to live of this entry, in milliseconds.
*/
ttl?: number;
/**
* The entry size, in bytes. It does not have to be an exact value, but
* it helps the cache determine when to remove entries to save memory.
*/
size?: number;
/**
* A optional callback called when the entry is deleted from the cache.
*/
onDelete?: (entry: unknown) => void;
}
/**
* The default max number of entries.
*/
export const DEFAULT_MAX_ENTRIES = 8192;
/**
* The default TTL (time to live), in milliseconds.
*/
export const DEFAULT_TTL: number = 240_000; // 240 seconds
/**
* The default capacity, in bytes.
*/
export const DEFAULT_CAPACITY: number = 536_870_912; // 512 MB
export interface CacheConfiguration {
/**
* The default TTL (time to live) of entries, in milliseconds.
* Can be overriden for each entry (see {@link CacheOptions}).
* @defaultValue {@link DEFAULT_TTL}
*/
ttl?: number;
/**
* The capacity, in bytes, of the cache.
* @defaultValue {@link DEFAULT_CAPACITY}
*/
byteCapacity?: number;
/**
* The capacity, in number of entries, of the cache.
* @defaultValue {@link DEFAULT_MAX_ENTRIES}
*/
maxNumberOfEntries?: number;
}
/**
* The cache.
*
*/
export class Cache implements MemoryUsage {
public readonly isMemoryUsage = true as const;
private readonly _deleteHandlers: Map<string, (entry: object) => void>;
private _lru: LRUCache<string, object>;
private _enabled: boolean;
/**
* Constructs a cache.
*
* @param opts - The options.
*/
public constructor(opts?: CacheConfiguration) {
this._deleteHandlers = new Map();
this._enabled = true;
this._lru = this.createLRUCache(opts);
}
private createLRUCache(opts?: CacheConfiguration): LRUCache<string, object> {
return new LRUCache<string, object>({
ttl: opts?.ttl ?? DEFAULT_TTL,
ttlResolution: 1000, // 1 second
updateAgeOnGet: true,
maxSize: opts?.byteCapacity ?? DEFAULT_CAPACITY,
max: opts?.maxNumberOfEntries ?? DEFAULT_MAX_ENTRIES,
allowStale: false,
dispose: (value, key): void => {
this.onDisposed(key, value);
},
});
}
/**
* Configure the cache with the specified configuration. The cache must be
* empty otherwise this method will throw an error.
*/
public configure(config: CacheConfiguration): void {
if (this.count > 0) {
throw new Error('cannot configure the cache as it is not empty.');
}
this._lru = this.createLRUCache(config);
}
public getMemoryUsage(context: GetMemoryUsageContext): void {
this._lru.forEach(e => {
if (isMemoryUsage(e)) {
e.getMemoryUsage(context);
}
});
}
/**
* Enables or disables the cache.
*/
public get enabled(): boolean {
return this._enabled;
}
public set enabled(v: boolean) {
this._enabled = v;
}
/**
* Gets or sets the default TTL (time to live) of the cache.
*/
public get defaultTtl(): number {
return this._lru.ttl;
}
public set defaultTtl(v: number) {
this._lru.ttl = v;
}
/**
* Gets the maximum size of the cache, in bytes.
*/
public get maxSize(): number {
return this._lru.maxSize;
}
/**
* Gets the maximum number of entries.
*/
public get capacity(): number {
return this._lru.max;
}
/**
* Gets the number of entries.
*/
public get count(): number {
return this._lru.size;
}
/**
* Gets the size of entries, in bytes
*/
public get size(): number {
return this._lru.calculatedSize;
}
/**
* Returns an array of entries.
*/
public entries(): Array<unknown> {
return [...this._lru.entries()];
}
private onDisposed(key: string, value: object): void {
const handler = this._deleteHandlers.get(key);
if (handler) {
this._deleteHandlers.delete(key);
handler(value);
}
}
/**
* Removes stale entries.
*/
public purge(): void {
this._lru.purgeStale();
}
/**
* Returns the entry with the specified key, or `undefined` if no entry matches this key.
*
* @param key - The entry key.
* @returns The entry, or `undefined`.
*/
public get(key: string): unknown | undefined {
if (!this.enabled) {
return undefined;
}
return this._lru.get(key);
}
/**
* Stores an entry in the cache, or replaces an existing entry with the same key.
*
* @param key - The key.
* @param value - The value.
* @param options - The options.
*/
public set<T extends object>(key: string, value: T, options: CacheOptions = {}): T {
if (!this.enabled) {
return value;
}
if (typeof key !== 'string') {
throw new Error('the cache expects strings as keys.');
}
this._lru.set(key, value, {
ttl: options.ttl ?? this.defaultTtl,
size: options.size ?? 1024, // Use a default size if not provided
});
if (options.onDelete) {
this._deleteHandlers.set(key, options.onDelete);
}
return value;
}
/**
* Deletes an entry.
*
* @param key - The key.
* @returns `true` if the entry was deleted, `false` otherwise.
*/
public delete(key: string): boolean {
return this._lru.delete(key);
}
/**
* Clears the cache.
*
*/
public clear(): void {
this._lru.clear();
}
}
/**
* A global singleton cache.
*/
export const GlobalCache: Cache = new Cache();