@nasriya/cachify
Version:
A lightweight, extensible in-memory caching library for storing anything, with built-in TTL and customizable cache types.
492 lines (491 loc) • 22.9 kB
JavaScript
/**
* A centralized broker for managing cache-related events.
*
* Provides typed, flavor-specific interfaces to:
* - Listen to individual events (`on`)
* - Listen to all events (`onAny`)
* - Emit events (`emit`)
* - Remove registered handlers (`removeHandler`)
*
* Supports cache flavors: `kvs`, `file`, and (in future) `database`.
*
* @since v1.0.0
*/
class EventsBroker {
#_managers;
constructor(managers) { this.#_managers = managers; }
/**
* Registers handlers for **specific events** within a given cache flavor.
*
* Each property corresponds to a cache flavor and exposes
* methods to listen for individual events like `'create'`, `'evict'`, etc.
*
* @example
* ```ts
* cachify.on.kvs('create', (payload) => { console.log('KV created', payload); });
* cachify.on.file('remove', (payload) => { console.log('File removed', payload); });
* ```
*
* @since v1.0.0
*/
on = {
/**
* Registers an event handler for a specified key-value cache event.
*
* @template E - The type of the key-value cache event.
* @param {E} event - The name of the event to listen for. Must be a non-empty string.
* @param {(payload: KVCacheEvents[E]['payload']) => void} handler - The function to be executed when the event is emitted.
* @param {AddHandlerOptions} [options] - Optional configurations for the event handler.
* @throws {TypeError} Throws if the event is not a string or the handler is not a function.
* @throws {RangeError} Throws if the event is an empty string.
* @since v1.0.0
*/
kvs: (event, handler, options) => {
this.#_managers.kvs.on(event, handler, options);
},
/**
* Registers an event handler for a specified file cache event.
*
* @template E - The type of the file cache event.
* @param {E} event - The name of the event to listen for. Must be a non-empty string.
* @param {(payload: FileCacheEvents[E]['payload']) => void} handler - The function to be executed when the event is emitted.
* @param {AddHandlerOptions} [options] - Optional configurations for the event handler.
* @throws {TypeError} Throws if the event is not a string or the handler is not a function.
* @throws {RangeError} Throws if the event is an empty string.
* @since v1.0.0
*/
files: (event, handler, options) => {
this.#_managers.files.on(event, handler, options);
},
/**
* Registers an event handler for a specified database cache event.
*
* **Not implemented**.
*/
// database() {
// throw new Error('Not implemented.');
// }
};
/**
* Registers handlers that respond to **any event** within a cache flavor.
*
* These are called for **all emitted events** of that flavor, regardless of type.
* Ideal for logging, analytics, or global hooks.
*
* @example
* ```ts
* cachify.onAny.kvsEvent((event) => {
* console.log(`KV event: ${event.type}`);
* });
*
* cachify.onAny.filesEvent((event) => {
* if (event.type === 'remove') {
* console.log('File cache entry removed');
* }
* });
* ```
*
* @since v1.0.0
*/
onAny = {
/**
* Registers a handler that will be executed for **any key-value cache event**.
*
* Useful for logging, debugging, or performing actions on all events regardless of type.
*
* @param handler - A function to handle every emitted KV event.
* @example
* ```ts
* cachify.events.onAny.kvsEvent((event) => {
* console.log(`KV cache event: ${event.type}`);
* });
* ```
* @since v1.0.0
*/
kvsEvent: (handler) => {
this.#_managers.kvs.onAny(handler);
},
/**
* Registers a handler that will be executed for **any file cache event**.
*
* Like `kvsEvent`, this gives you visibility into all emitted file cache events.
*
* @param handler - A function to handle every emitted file event.
* @example
* ```ts
* cachify.onAny.filesEvent((event) => {
* if (event.type === 'touch') {
* console.log('File cache entry was touched.');
* }
* });
* ```
* @since v1.0.0
*/
filesEvent: (handler) => {
this.#_managers.files.onAny(handler);
},
/**
* Registers a handler for all **database cache events**.
*
* ⚠️ Not implemented yet. Calling this will throw an error.
*
* @throws {Error} Always throws: "Not implemented."
* @since v1.0.0
*/
// databaseEvent() {
// throw new Error('Not implemented.');
// }
};
/**
* Emits events for a specific cache flavor.
*
* Each method corresponds to a known event type like `'create'`, `'touch'`, or `'evict'`.
* Used internally by cache managers, but exposed for advanced custom use cases.
*
* @example
* ```ts
* cachify.emit.kvs.create(payload);
* cachify.emit.file.remove(payload);
* ```
*
* ⚠️ `database` flavor is defined but not yet implemented.
*
* @since v1.0.0
*/
emit = {
/**
* Emits events related to the **key-value (KV) cache** flavor.
*
* Each method under this object corresponds to a specific cache event
* (e.g. `create`, `update`, `evict`, etc.), and is used to trigger
* event handlers registered for that event.
*
* Example:
* ```ts
* cachify.events.emit.kvs.create(record);
* cachify.events.emit.kvs.evict(record, { reason: 'expired' });
* ```
*/
kvs: {
/**
* Emits the 'evict' event when a record is evicted from the cache due to various reasons.
* @param {KVCacheRecord} record - The record that was evicted from the cache.
* @param {{ reason: EvictReason }} options - An object containing the reason for the eviction.
* @since v1.0.0
*/
evict: (record, options) => this.#_managers.kvs.emit.evict(record, options),
/**
* Emits the 'expire' event when a record expires from the key-value cache.
* A record is considered expired if it has a TTL greater than 0 and the expiration date is in the past.
* If the record has a TTL of 0, it is never considered expired.
* @param {KVCacheRecord} record - The record that expired from the cache.
* @since v1.0.0
*/
expire: (record) => this.#_managers.kvs.emit.expire(record),
/**
* Emits the 'remove' event for a record being removed from the key-value cache.
*
* This method creates and emits a payload for the 'remove' event, including the record's data and the reason for removal.
*
* @param {KVCacheRecord} record - The key-value cache record that is being removed.
* @param {Object} [options] - The options for the removal event.
* @param {KVRemovalReason} [options.reason='manual'] - The reason for the removal. Defaults to 'manual'.
* @since v1.0.0
*/
remove: (record, options = { reason: 'manual' }) => this.#_managers.kvs.emit.remove(record, options),
/**
* Emits the 'bulkRemove' event for multiple records being removed from the key-value cache.
*
* This method creates and emits a payload for the 'bulkRemove' event, including the records' data and the reason for removal.
*
* @param {KVCacheRecord[]} records - An array of key-value cache records that are being removed.
* @param {Object} [options] - The options for the removal event.
* @param {KVRemovalReason} [options.reason='manual'] - The reason for the removal. Defaults to 'manual'.
* @since v1.0.0
*/
bulkRemove: (records, options = { reason: 'manual' }) => this.#_managers.kvs.emit.bulkRemove(records, options),
/**
* Emits the 'create' event when a record is added to the key-value cache.
*
* This method creates and emits a payload for the 'create' event, including the record's data and whether it was preloaded or not.
*
* @param {KVCacheRecord} record - The key-value cache record that was added to the cache.
* @param {Object} [options] - The options for the creation event.
* @param {boolean} [options.preload=false] - Whether the record was preloaded, or not. Defaults to false.
* @since v1.0.0
*/
create: (record, options = { preload: false }) => this.#_managers.kvs.emit.create(record, options),
/**
* Emits the 'read' event when a record is accessed in the key-value cache.
*
* This method creates and emits a payload for the 'read' event, including the record's data.
*
* @param {KVCacheRecord} record - The key-value cache record that was accessed.
* @since v1.0.0
*/
read: (record) => this.#_managers.kvs.emit.read(record),
/**
* Emits the 'update' event when a record is updated in the key-value cache.
* This method creates and emits a payload for the 'update' event, including the record's data.
* @param {KVCacheRecord} record - The key-value cache record that was updated.
* @since v1.0.0
*/
update: (record) => this.#_managers.kvs.emit.update(record),
/**
* Emits the 'clear' event when the entire key-value cache is being cleared.
* This results in the removal of all entries from the cache.
*
* **Note:** This event is only emitted by the cache manager.
* @param {KVCacheRecord} record - The record associated with the clear operation.
* @since v1.0.0
*/
clear: (record) => this.#_managers.kvs.emit.clear(record),
/**
* Emits the 'touch' event for a record accessed in the key-value cache.
*
* This method creates and emits a payload for the 'touch' event, including the record's data.
* It is used to update the record's metadata, such as LRU, without modifying the value.
*
* @param {KVCacheRecord} record - The key-value cache record that was accessed.
* @since v1.0.0
*/
touch: (record) => this.#_managers.kvs.emit.touch(record),
},
/**
* Emits events related to the **file cache** flavor.
*
* Similar to `emit.kvs`, this object exposes emitters for events
* such as `read`, `remove`, `touch`, etc., related to file-based caching.
*
* Example:
* ```ts
* emit.files.remove(record, { reason: 'manual' });
* emit.files.touch(record);
* ```
*/
files: {
/**
* Emits the 'evict' event when a record is evicted from the cache due to various reasons.
* @param {FileCacheRecord} record - The record that was evicted from the cache.
* @param {{ reason: EvictReason }} options - An object containing the reason for the eviction.
* @since v1.0.0
*/
evict: (record, options) => this.#_managers.files.emit.evict(record, options),
/**
* Emits the 'expire' event when a record expires from the cache.
* A record is considered expired if it has a TTL greater than 0 and the expiration date is in the past.
* If the record has a TTL of 0, it is never considered expired.
* @param {FileCacheRecord} record - The record that expired from the cache.
* @since v1.0.0
*/
expire: (record) => this.#_managers.files.emit.expire(record),
/**
* Emits the 'remove' event for a record being removed from the file cache.
*
* This method creates and emits a payload for the 'remove' event, including the record's data and the reason for removal.
*
* @param {FileCacheRecord} record - The file cache record that is being removed.
* @param {Object} [options] - The options for the removal event.
* @param {FileRemovalReason} [options.reason='manual'] - The reason for the removal. Defaults to 'manual'.
* @since v1.0.0
*/
remove: (record, options = { reason: 'manual' }) => this.#_managers.files.emit.remove(record, options),
/**
* Emits the 'remove' event for a record being removed from the file cache.
*
* This method creates and emits a payload for the 'remove' event, including the record's data and the reason for removal.
*
* @param {FileCacheRecord} record - The file cache record that is being removed.
* @param {Object} [options] - The options for the removal event.
* @param {FileRemovalReason} [options.reason='manual'] - The reason for the removal. Defaults to 'manual'.
* @since v1.0.0
*/
bulkRemove: (records, options = { reason: 'manual' }) => this.#_managers.files.emit.bulkRemove(records, options),
/**
* Emits the 'create' event when a file record is added to the file cache.
*
* This method creates and emits a payload for the 'create' event, including the record's data and whether it was preloaded or not.
*
* @param {FileCacheRecord} record - The file cache record that was added to the cache.
* @param {Object} [options] - The options for the creation event.
* @param {boolean} [options.preload=false] - Whether the record was preloaded, or not. Defaults to false.
* @since v1.0.0
*/
create: (record, options = { preload: false }) => this.#_managers.files.emit.create(record, options),
/**
* Emits the 'read' event when a record is accessed in the file cache.
* This method creates and emits a payload for the 'read' event, including the record's data and the status of the read operation.
* @param {FileCacheRecord} record - The file cache record that was accessed.
* @param {{ status: 'hit' | 'miss' }} options - An object containing the status of the read operation.
* @param {'hit' | 'miss'} options.status - Whether the record was found in the cache ('hit') or not ('miss').
* @since v1.0.0
*/
read: (record, options) => this.#_managers.files.emit.read(record, options),
/**
* Emits the 'update' event when a record is updated in the file cache.
* This method creates and emits a payload for the 'update' event, including the record's data.
* @param {FileCacheRecord} record - The file cache record that was updated in the cache.
* @since v1.0.0
*/
update: (record) => this.#_managers.files.emit.update(record),
/**
* Emits the 'clear' event when the entire file cache is being cleared.
* This results in the removal of all entries from the cache.
*
* **Note:** This event is only emitted by the cache manager.
* @param {FileCacheRecord} record - The record associated with the clear operation.
* @since v1.0.0
*/
clear: (record) => this.#_managers.files.emit.clear(record),
/**
* Emits the 'touch' event when a record is accessed in the file cache.
* This method creates and emits a payload for the 'touch' event,
* including the record's data. It is used to update the record's metadata,
* such as last access time, without modifying the value.
*
* @param {FileCacheRecord} record - The file cache record that was accessed.
* @since v1.0.0
*/
touch: (record) => this.#_managers.files.emit.touch(record),
/**
* Emits the 'hit' event when a file record is accessed and found in the cache.
* **Note:** This event is only emitted internally by the `read` event.
* @param {FileCacheRecord} record - The file cache record that was accessed and found in the cache.
* @since v1.0.0
*/
hit: (record) => this.#_managers.files.emit.hit(record),
/**
* Emits the 'miss' event when a file record is accessed and not found in the cache.
* **Note:** This event is only emitted internally by the `read` event.
* @param {FileCacheRecord} record - The file cache record that was accessed and not found in the cache.
* @since v1.0.0
*/
miss: (record) => this.#_managers.files.emit.miss(record),
/**
* Emits the 'fileContentSizeChange' event when the content size of a file changes.
* @param {FileCacheRecord} record - The file cache record that has changed.
* @param {number} delta - The change in content size (positive for addition, negative for removal).
* @since v1.0.0
*/
contentSizeChange: (record, delta) => this.#_managers.files.emit.contentSizeChange(record, delta),
/**
* Emits the 'fileRenameChange' event when a file record is renamed.
* @param {FileCacheRecord} record - The file cache record that was renamed.
* @param {RenameEvent} renameEvent - The rename event containing the old and new paths.
* @since v1.0.0
*/
fileRenameChange: (record, renameEvent) => this.#_managers.files.emit.fileRenameChange(record, renameEvent),
},
/**
* Emits events for the **database cache** flavor.
*
* ⚠️ This flavor is not implemented yet.
* Calling any methods under this object will currently result in an error.
*
* This key exists as a placeholder for future database caching features.
*/
// database: {}
};
/**
* Removes a previously registered event handler.
*
* This includes:
* - Handlers registered for specific events (`on`)
* - Handlers registered for all events (`onAny`)
*
* @example
* ```ts
* const handler = (event) => { ... };
* cachify.on.kvs('update', handler);
* cachify.removeHandler.fromKvs('update', handler); // removes it
*
* const anyHandler = (event) => { ... };
* cachify.onAny.filesEvent(anyHandler);
* cachify.removeHandler.fromFiles('Any', anyHandler); // removes any-handler
* ```
*
* ⚠️ `fromDatabase` is a placeholder and will throw if called.
*
* @since v1.0.0
*/
removeHandler = {
/**
* Removes a previously registered handler for a **key-value cache** event.
*
* You can remove a handler for a specific event (`'create'`, `'evict'`, etc.)
* or remove a generic "any" handler by passing `'Any'` as the event name.
*
* @template K - A specific key-value cache event name or `'Any'`.
* @param event - The name of the event to remove the handler for, or `'Any'`.
* @param handler - The exact handler function to be removed.
* Must match the function reference used in `cachify.on.kvs(...)` or `cachify.onAny.kvsEvent(...)`.
* @example
* ```ts
* cachify.removeHandler.fromKvs('update', onUpdateHandler);
* cachify.removeHandler.fromKvs('Any', onAnyHandler);
* ```
* @since v1.0.0
*/
fromKvs: (event, handler) => {
this.#_managers.kvs.removeHandler(event, handler);
},
/**
* Removes a previously registered handler for a **file cache** event.
*
* Like `fromKvs`, this supports removal of both specific event handlers and
* global "any" handlers.
*
* @template K - A specific file cache event name or `'Any'`.
* @param event - The event to remove the handler for, or `'Any'`.
* @param handler - The handler function to remove.
* Must match the reference used in `cachify.on.file(...)` or `cachify.onAny.filesEvent(...)`.
* @example
* ```ts
* cachify.removeHandler.fromFiles('remove', handler);
* cachify.removeHandler.fromFiles('Any', anyFileHandler);
* ```
* @since v1.0.0
*/
fromFiles: (event, handler) => {
this.#_managers.files.removeHandler(event, handler);
},
/**
* Removes a handler from the **database cache** flavor.
*
* ⚠️ Not implemented yet.
* Currently calling this method will throw an error.
*
* @throws {Error} Always throws: "Not implemented."
* @since v1.0.0
*/
// fromDatabase() {
// throw new Error('Not implemented.');
// }
};
/**
* Cleans up internal state and resources used by the events manager.
*
* This includes:
* - Canceling any pending internal timers (e.g. debounced warnings)
* - Releasing memory references to ensure the process can exit cleanly
* - Resetting internal state in preparation for teardown or testing
*
* This method is especially useful in unit tests to prevent open handles
* (such as `setTimeout`) from blocking Jest's process exit.
*
* > Note: This does **not** remove any user-registered event handlers. If needed,
* handlers should be removed explicitly via `removeHandler()` before calling this.
*
* @example
* afterEach(() => {
* cachify.events.dispose();
* });
*
* @since v1.0.0
*/
dispose() {
this.#_managers.kvs.dispose();
this.#_managers.files.dispose();
}
}
export default EventsBroker;