hyper-express
Version:
High performance Node.js webserver with a simple-to-use API powered by uWebsockets.js under the hood.
173 lines (143 loc) • 4.81 kB
JavaScript
'use strict';
const FileSystem = require('fs');
const EventEmitter = require('events');
const { wrap_object, async_wait } = require('../../shared/operators.js');
class LiveFile extends EventEmitter {
#name;
#watcher;
#extension;
#buffer;
#content;
#last_update;
#options = {
path: '',
retry: {
every: 300,
max: 3,
},
};
constructor(options) {
// Initialize EventEmitter instance
super();
// Wrap options object with provided object
wrap_object(this.#options, options);
// Determine the name of the file
const chunks = options.path.split('/');
this.#name = chunks[chunks.length - 1];
// Determine the extension of the file
this.#extension = this.#options.path.split('.');
this.#extension = this.#extension[this.#extension.length - 1];
// Initialize file watcher to keep file updated in memory
this.reload();
this._initiate_watcher();
}
/**
* @private
* Initializes File Watcher to reload file on changes
*/
_initiate_watcher() {
// Create FileWatcher that trigger reload method
this.#watcher = FileSystem.watch(this.#options.path, () => this.reload());
}
#reload_promise;
#reload_resolve;
#reload_reject;
/**
* Reloads buffer/content for file asynchronously with retry policy.
*
* @private
* @param {Boolean} fresh
* @param {Number} count
* @returns {Promise}
*/
reload(fresh = true, count = 0) {
const reference = this;
if (fresh) {
// Reuse promise if there if one pending
if (this.#reload_promise instanceof Promise) return this.#reload_promise;
// Create a new promise for fresh lookups
this.#reload_promise = new Promise((resolve, reject) => {
reference.#reload_resolve = resolve;
reference.#reload_reject = reject;
});
}
// Perform filesystem lookup query
FileSystem.readFile(this.#options.path, async (error, buffer) => {
// Pipe filesystem error through promise
if (error) {
reference._flush_ready();
return reference.#reload_reject(error);
}
// Perform retries in accordance with retry policy
// This is to prevent empty reads on atomicity based modifications from third-party programs
const { every, max } = reference.#options.retry;
if (buffer.length == 0 && count < max) {
await async_wait(every);
return reference.reload(false, count + 1);
}
// Update instance buffer/content/last_update variables
reference.#buffer = buffer;
reference.#content = buffer.toString();
reference.#last_update = Date.now();
// Cleanup reload promises and methods
reference.#reload_resolve();
reference._flush_ready();
reference.#reload_resolve = null;
reference.#reload_reject = null;
reference.#reload_promise = null;
});
return this.#reload_promise;
}
#ready_promise;
#ready_resolve;
/**
* Flushes pending ready promise.
* @private
*/
_flush_ready() {
if (typeof this.#ready_resolve == 'function') {
this.#ready_resolve();
this.#ready_resolve = null;
}
this.#ready_promise = true;
}
/**
* Returns a promise which resolves once first reload is complete.
*
* @returns {Promise}
*/
ready() {
// Return true if no ready promise exists
if (this.#ready_promise === true) return Promise.resolve();
// Create a Promise if one does not exist for ready event
if (this.#ready_promise === undefined)
this.#ready_promise = new Promise((resolve) => (this.#ready_resolve = resolve));
return this.#ready_promise;
}
/* LiveFile Getters */
get is_ready() {
return this.#ready_promise === true;
}
get name() {
return this.#name;
}
get path() {
return this.#options.path;
}
get extension() {
return this.#extension;
}
get content() {
return this.#content;
}
get buffer() {
return this.#buffer;
}
get last_update() {
return this.#last_update;
}
get watcher() {
return this.#watcher;
}
}
module.exports = LiveFile;