UNPKG

@nasriya/cachify

Version:

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

131 lines (130 loc) 5.21 kB
import { Writable } from "stream"; import helpers from "./helpers.js"; import DecryptStream from "./streams/DecryptStream.js"; import StreamLinesParser from "./streams/StreamLinesParser.js"; import restoreQueue from "./restoreQueue.js"; class RestoreStream { #_client; #_streams; #_userErrorHandler; #_errorHandler = (err) => { if (typeof this.#_userErrorHandler === 'function') { this.#_userErrorHandler(err); } else { throw err; } }; constructor(client) { this.#_client = client; this.#_streams = Object.freeze({ lineParser: new StreamLinesParser(), decryptor: (() => { const encryptionKey = helpers.getEncryptionKey(); if (encryptionKey) { return new DecryptStream(encryptionKey); } })(), handler: new Writable({ decodeStrings: false, // Disable automatic string decoding defaultEncoding: 'utf8', write: (chunk, _enc, cb) => { chunk = chunk.trim(); try { // Only process RECORD lines if (!chunk.startsWith('RECORD ')) return cb(); const firstSpace = chunk.indexOf(' '); const recordData = chunk.slice(firstSpace + 1); const data = JSON.parse(recordData); const setAction = helpers.setRecord(data, this.#_client); if (!setAction) { return cb(); } // It means the record is expired restoreQueue.addTask({ type: data.flavor, action: async () => await setAction, onReject(error) { const err = new AggregateError([error, new Error(`Failed to restore record: ${data.key}`)]); console.error(err); }, }); cb(); } catch (error) { cb(error); } }, final: async (cb) => { try { await restoreQueue.untilComplete(); cb(); } catch (error) { cb(error); } } }) }); this.#_helpers.setErrorHandlers(); } #_helpers = { setErrorHandlers: () => { this.#_streams.decryptor?.on('error', this.#_errorHandler); this.#_streams.lineParser.on('error', this.#_errorHandler); this.#_streams.handler.on('error', this.#_errorHandler); }, pipe: (input) => { const { decryptor, lineParser, handler } = this.#_streams; const hasDecryptor = decryptor instanceof DecryptStream; let current = input; if (hasDecryptor) { current.pipe(decryptor); current = decryptor; } current.pipe(lineParser).pipe(handler); } }; /** * 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. * @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 input - The readable 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 streamFrom(input) { return new Promise((resolve, reject) => { const handleError = (err) => { input.destroy(); this.#_streams.decryptor?.destroy(); this.#_streams.lineParser.destroy(); this.#_streams.handler.destroy(); reject(err); }; this.#onError(handleError); input.on('error', handleError); this.#_streams.handler.on('finish', resolve); this.#_helpers.pipe(input); }); } } export default RestoreStream;