UNPKG

@nasriya/cachify

Version:

A lightweight, extensible in-memory caching library for storing anything, with built-in TTL and customizable cache types.

129 lines (128 loc) 5.17 kB
import { Readable } from "stream"; import { once } from "events"; import EncryptStream from "./streams/EncryptStream.js"; import helpers from "./helpers.js"; class BackupStream { #_stream; #_encryptStream; #_errorHandler = (err) => this.#_userErrorHandler?.(err); #_userErrorHandler; #_closed = false; constructor() { // Create a Readable in "object mode" to allow pushing strings easily this.#_stream = new Readable({ read() { }, encoding: 'utf-8', objectMode: false }); this.#_stream.on('error', this.#_errorHandler); const encryptionKey = helpers.getEncryptionKey(); if (encryptionKey) { this.#_encryptStream = new EncryptStream(encryptionKey); this.#_encryptStream.on('error', this.#_errorHandler); } // Push initial header lines this.#_controller.push(`CACHE_BACKUP v1\n`); this.#_controller.push(`CREATED_AT ${new Date().toISOString()}\n`); } #_controller = { push: (chunk) => { if (this.#_closed) { throw new Error("Cannot push after stream is closed"); } // Push data to the readable stream // Return false if the internal buffer is full (backpressure) const ok = this.#_stream.push(chunk); return ok; }, writeRecordSync: (record) => { const json = JSON.stringify(record); return this.#_controller.push(`RECORD ${json}\n`); } }; /** * Writes a record to the persistence stream asynchronously. * * This method is asynchronous to allow for backpressure handling. * If the internal buffer is full (i.e. the writable stream is not consuming data as fast as `writeRecord` is being called) * then this method will await the 'drain' event on the readable stream before returning. * * If the stream is already closed, this method does nothing and returns false. * @param record - The record to write to the persistence. * @returns A promise that resolves with a boolean indicating whether the write was successful. * @since v1.0.0 */ async writeRecord(record) { const ok = this.#_controller.writeRecordSync(record); if (!ok) { await once(this.#_stream, 'drain'); } return ok; } /** * Closes the persistence stream. * * This method is called when the persistence stream is finished writing records. * It will write a footer line to the stream and then signal the end of the stream. * If the internal buffer is full (i.e. the writable stream is not consuming data as fast as `writeRecord` is being called) * then this method will await the 'drain' event on the readable stream before returning. * @since v1.0.0 */ async close() { if (this.#_closed) { return; } if (!this.#_controller.push(`END_BACKUP\n`)) { await once(this.#_stream, 'drain'); } this.#_closed = true; this.#_stream.push(null); // Signal end of stream } /** * Sets the error handler for the persistence stream. * * If an error occurs on the readable or writable stream (i.e. when writing to the stream or when the stream is piped to a destination), * then the handler will be called with the error as an argument. * If the handler is not set, then errors will be thrown. * @param handler - The error handler to call when an error occurs. * @throws {TypeError} If the provided handler is not a function. * @since v1.0.0 */ #onError(handler) { if (typeof handler !== 'function') { throw new TypeError(`The provided handler (${handler}) is not a function.`); } this.#_userErrorHandler = handler; } /** * Pipes the internal readable stream to the provided writable stream. * * This method returns a promise that resolves when the writable stream is finished consuming data. * If an error occurs on either the readable or writable stream, then the promise will reject with the error. * The writable stream is destroyed when an error occurs. * * @param dest - The writable stream to pipe the internal readable stream to. * @returns A promise that resolves when the writable stream is finished consuming data. * @since v1.0.0 */ async streamTo(dest) { return new Promise((resolve, reject) => { const handleError = (err) => { dest.destroy(); this.#_encryptStream?.destroy(); this.#_stream.destroy(); reject(err); }; this.#onError(handleError); dest.on('finish', resolve); dest.on('error', handleError); let current = this.#_stream; if (this.#_encryptStream) { current.pipe(this.#_encryptStream); current = this.#_encryptStream; } current.pipe(dest); }); } } export default BackupStream;