metautil
Version:
Metarhia utilities
130 lines (116 loc) • 3.29 kB
JavaScript
'use strict';
class Collector {
done = false;
data = {};
keys = [];
count = 0;
exact = true;
reassign = true;
timeout = 0;
defaults = {};
validate = null;
#fulfill = null;
#reject = null;
#cause = null;
#controller = null;
#signal = null;
#timeout = null;
constructor(keys, options = {}) {
const { exact = true, reassign = false } = options;
const { timeout = 0, defaults = {}, validate } = options;
if (validate) this.validate = validate;
this.keys = keys;
if (exact === false) this.exact = false;
if (reassign === false) this.reassign = reassign;
if (typeof defaults === 'object') this.defaults = defaults;
this.#controller = new AbortController();
this.#signal = this.#controller.signal;
if (typeof timeout === 'number' && timeout > 0) {
this.#timeout = AbortSignal.timeout(timeout);
this.#signal = AbortSignal.any([this.#signal, this.#timeout]);
this.#signal.addEventListener('abort', () => {
if (Object.keys(this.defaults).length > 0) this.#default();
if (this.done) return;
this.fail(this.#signal.reason);
});
}
}
#default() {
for (const [key, value] of Object.entries(this.defaults)) {
if (this.data[key] === undefined) this.set(key, value);
}
}
get signal() {
return this.#signal;
}
set(key, value) {
if (this.done) return;
const expected = this.keys.includes(key);
if (!expected && this.exact) {
this.fail(new Error('Unexpected key: ' + key));
return;
}
const has = this.data[key] !== undefined;
if (has && !this.reassign) {
const error = new Error('Collector reassign mode is off');
return void this.fail(error);
}
if (!has && expected) this.count++;
this.data[key] = value;
if (this.count === this.keys.length) {
this.done = true;
this.#timeout = null;
if (this.#fulfill) this.#fulfill(this.data);
}
}
take(key, fn, ...args) {
fn(...args, (err, data) => {
if (err) this.fail(err);
else this.set(key, data);
});
}
wait(key, fn, ...args) {
const promise = fn instanceof Promise ? fn : fn(...args);
promise.then(
(data) => this.set(key, data),
(err) => this.fail(err),
);
}
collect(sources) {
for (const [key, collector] of Object.entries(sources)) {
collector.then(
(data) => this.set(key, data),
(err) => this.fail(err),
);
}
}
fail(error) {
this.done = true;
this.#timeout = null;
const err = error || new Error('Collector aborted');
this.#cause = err;
this.#controller.abort();
if (this.#reject) this.#reject(err);
}
abort() {
this.fail();
}
then(onFulfilled, onRejected = null) {
return new Promise((resolve, reject) => {
this.#fulfill = resolve;
this.#reject = reject;
if (!this.done) return;
if (this.validate) {
try {
this.validate(this.data);
} catch (err) {
this.#cause = err;
}
}
if (this.#cause) reject(this.#cause);
else resolve(this.data);
}).then(onFulfilled, onRejected);
}
}
const collect = (keys, options) => new Collector(keys, options);
module.exports = { Collector, collect };