@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
JavaScript
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;