mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-07 11:38:32 +00:00
4.0.0 Beta 1
This commit is contained in:
parent
c2688646af
commit
262757a20d
187 changed files with 22878 additions and 38882 deletions
563
src/utilities/module.js
Normal file
563
src/utilities/module.js
Normal file
|
@ -0,0 +1,563 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Module System
|
||||
// Modules are cool.
|
||||
// ============================================================================
|
||||
|
||||
import EventEmitter from 'utilities/events';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Module
|
||||
// ============================================================================
|
||||
|
||||
export const State = {
|
||||
UNLOADED: 0,
|
||||
LOADING: 1,
|
||||
LOADED: 2,
|
||||
UNLOADING: 3,
|
||||
|
||||
DISABLED: 0,
|
||||
ENABLING: 1,
|
||||
ENABLED: 2,
|
||||
DISABLING: 3
|
||||
}
|
||||
|
||||
|
||||
export default class Module extends EventEmitter {
|
||||
constructor(name, parent) {
|
||||
if ( ! parent && name instanceof Module ) {
|
||||
parent = name;
|
||||
name = null;
|
||||
}
|
||||
|
||||
super(name, parent);
|
||||
this.__modules = parent ? parent.__modules : {};
|
||||
this.children = {};
|
||||
|
||||
if ( parent && ! parent.children[this.name] )
|
||||
parent.children[this.name] = this;
|
||||
|
||||
if ( this.root === this )
|
||||
this.__modules[this.__path || ''] = this;
|
||||
|
||||
this.__load_state = this.onLoad ? State.UNLOADED : State.LOADED;
|
||||
this.__state = this.onLoad || this.onEnable ?
|
||||
State.DISABLED : State.ENABLED;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Public Properties
|
||||
// ========================================================================
|
||||
|
||||
get state() { return this.__state }
|
||||
get load_state() { return this.__load_state }
|
||||
|
||||
get loaded() { return this.__load_state === State.LOADED }
|
||||
get loading() { return this.__load_state === State.LOADING }
|
||||
|
||||
get enabled() { return this.__state === State.ENABLED }
|
||||
get enabling() { return this.__state === State.ENABLING }
|
||||
|
||||
|
||||
get log() {
|
||||
if ( ! this.__log )
|
||||
this.__log = this.parent && this.parent.log.get(this.name);
|
||||
return this.__log
|
||||
}
|
||||
|
||||
set log(log) {
|
||||
this.__log = log;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// State! Glorious State
|
||||
// ========================================================================
|
||||
|
||||
load(...args) {
|
||||
return this.__load(args, this.__path, []);
|
||||
}
|
||||
|
||||
unload(...args) {
|
||||
return this.__unload(args, this.__path, []);
|
||||
}
|
||||
|
||||
enable(...args) {
|
||||
return this.__enable(args, this.__path, []);
|
||||
}
|
||||
|
||||
disable(...args) {
|
||||
return this.__disable(args, this.__path, []);
|
||||
}
|
||||
|
||||
|
||||
__load(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__load_state;
|
||||
|
||||
if ( state === State.LOADING )
|
||||
return this.__load_promise;
|
||||
|
||||
else if ( state === State.LOADED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( state === State.UNLOADING )
|
||||
return Promise.reject(new ModuleError(`attempted to load module ${path} while module is being unloaded`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__load_state = State.LOADING;
|
||||
return this.__load_promise = (async () => {
|
||||
if ( this.load_requires ) {
|
||||
const promises = [];
|
||||
for(const name of this.load_requires) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module || !(module instanceof Module) )
|
||||
throw new ModuleError(`cannot find required module ${name} when loading ${path}`);
|
||||
|
||||
promises.push(module.__enable([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
if ( this.onLoad )
|
||||
return this.onLoad(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__load_state = State.LOADED;
|
||||
this.__load_promise = null;
|
||||
this.emit(':loaded', this);
|
||||
return ret;
|
||||
}).catch(err => {
|
||||
this.__load_state = State.UNLOADED;
|
||||
this.__load_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
__unload(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__load_state;
|
||||
|
||||
if ( state === State.UNLOADING )
|
||||
return this.__load_promise;
|
||||
|
||||
else if ( state === State.UNLOADED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( ! this.onUnload )
|
||||
return Promise.reject(new ModuleError(`attempted to unload module ${path} but module cannot be unloaded`));
|
||||
|
||||
else if ( state === State.LOADING )
|
||||
return Promise.reject(new ModuleError(`attempted to unload module ${path} while module is being loaded`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__load_state = State.UNLOADING;
|
||||
return this.__load_promise = (async () => {
|
||||
if ( this.__state !== State.DISABLED )
|
||||
await this.disable();
|
||||
|
||||
if ( this.load_dependents ) {
|
||||
const promises = [];
|
||||
for(const name of this.load_dependents) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module )
|
||||
throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`);
|
||||
|
||||
promises.push(module.__unload([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
return this.onUnload(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__load_state = State.UNLOADED;
|
||||
this.__load_promise = null;
|
||||
this.emit(':unloaded', this);
|
||||
return ret;
|
||||
}).catch(err => {
|
||||
this.__load_state = State.LOADED;
|
||||
this.__load_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
__enable(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__state;
|
||||
|
||||
if ( state === State.ENABLING )
|
||||
return this.__state_promise;
|
||||
|
||||
else if ( state === State.ENABLED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( state === State.DISABLING )
|
||||
return Promise.reject(new ModuleError(`attempted to enable module ${path} while module is being disabled`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__state = State.ENABLING;
|
||||
return this.__state_promise = (async () => {
|
||||
const promises = [],
|
||||
requires = this.requires,
|
||||
load_state = this.__load_state;
|
||||
|
||||
if ( load_state === State.UNLOADING )
|
||||
// We'd abort for this later to, but kill it now before we start
|
||||
// any unnecessary work.
|
||||
throw new ModuleError(`attempted to load module ${path} while module is being unloaded`);
|
||||
|
||||
else if ( load_state === State.LOADING || load_state === State.UNLOADED )
|
||||
promises.push(this.load());
|
||||
|
||||
if ( requires )
|
||||
for(const name of requires) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module || !(module instanceof Module) )
|
||||
throw new ModuleError(`cannot find required module ${name} when enabling ${path}`);
|
||||
|
||||
promises.push(module.__enable([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
if ( this.onEnable )
|
||||
return this.onEnable(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__state = State.ENABLED;
|
||||
this.__state_promise = null;
|
||||
this.emit(':enabled', this);
|
||||
return ret;
|
||||
|
||||
}).catch(err => {
|
||||
this.__state = State.DISABLED;
|
||||
this.__state_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
__disable(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__state;
|
||||
|
||||
if ( state === State.DISABLING )
|
||||
return this.__state_promise;
|
||||
|
||||
else if ( state === State.DISABLED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( ! this.onDisable )
|
||||
return Promise.reject(new ModuleError(`attempted to disable module ${path} but module cannot be disabled`));
|
||||
|
||||
else if ( state === State.ENABLING )
|
||||
return Promise.reject(new ModuleError(`attempted to disable module ${path} but module is being enabled`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__state = State.DISABLING;
|
||||
return this.__state_promise = (async () => {
|
||||
if ( this.__load_state !== State.LOADED )
|
||||
// We'd abort for this later to, but kill it now before we start
|
||||
// any unnecessary work.
|
||||
throw new ModuleError(`attempted to disable module ${path} but module is unloaded -- weird state`);
|
||||
|
||||
if ( this.dependents ) {
|
||||
const promises = [];
|
||||
for(const name of this.dependents) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module )
|
||||
throw new ModuleError(`cannot find depending module ${name} when disabling ${path}`);
|
||||
|
||||
promises.push(module.__disable([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
return this.onDisable(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__state = State.ENABLED;
|
||||
this.__state_promise = null;
|
||||
this.emit(':disabled', this);
|
||||
return ret;
|
||||
|
||||
}).catch(err => {
|
||||
this.__state = State.DISABLED;
|
||||
this.__state_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Slightly Easier Events
|
||||
// ========================================================================
|
||||
|
||||
on(event, fn, ctx) {
|
||||
return super.on(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
prependOn(event, fn, ctx) {
|
||||
return super.prependOn(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
many(event, ttl, fn, ctx) {
|
||||
return super.many(event, ttl, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
prependMany(event, ttl, fn, ctx) {
|
||||
return super.prependMany(event, ttl, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
once(event, fn, ctx) {
|
||||
return super.once(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
prependOnce(event, fn, ctx) {
|
||||
return super.prependOnce(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
off(event, fn, ctx) {
|
||||
return super.off(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Child Control
|
||||
// ========================================================================
|
||||
|
||||
loadModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).load()))
|
||||
}
|
||||
|
||||
unloadModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).unload()))
|
||||
}
|
||||
|
||||
enableModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).enable()))
|
||||
}
|
||||
|
||||
disableModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).disable()))
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Module Management
|
||||
// ========================================================================
|
||||
|
||||
resolve(name) {
|
||||
if ( name instanceof Module )
|
||||
return name;
|
||||
|
||||
return this.__modules[this.abs_path(name)];
|
||||
}
|
||||
|
||||
|
||||
__get_requires() {
|
||||
if ( has(this, 'requires') )
|
||||
return this.requires;
|
||||
if ( has(this.constructor, 'requires') )
|
||||
return this.constructor.requires;
|
||||
}
|
||||
|
||||
|
||||
__get_load_requires() {
|
||||
if ( has(this, 'load_requires') )
|
||||
return this.load_requires;
|
||||
if ( has(this.constructor, 'load_requires') )
|
||||
return this.constructor.load_requires;
|
||||
}
|
||||
|
||||
|
||||
inject(name, module) {
|
||||
if ( name instanceof Module || name.prototype instanceof Module ) {
|
||||
module = name;
|
||||
name = null;
|
||||
}
|
||||
|
||||
const requires = this.requires = this.__get_requires() || [];
|
||||
|
||||
if ( module instanceof Module ) {
|
||||
// Existing Instance
|
||||
if ( ! name )
|
||||
name = module.constructor.name.toSnakeCase();
|
||||
|
||||
} else if ( module && module.prototype instanceof Module ) {
|
||||
// New Instance
|
||||
if ( ! name )
|
||||
name = module.name.toSnakeCase();
|
||||
|
||||
module = this.register(name, module);
|
||||
|
||||
} else if ( name ) {
|
||||
// Just a Name
|
||||
const full_name = name;
|
||||
name = name.replace(/^(?:[^.]*\.)+/, '');
|
||||
module = this.resolve(full_name);
|
||||
|
||||
// Allow injecting a module that doesn't exist yet?
|
||||
|
||||
if ( ! module || !(module instanceof Module) ) {
|
||||
if ( module )
|
||||
module[2].push([this.__path, name]);
|
||||
else
|
||||
this.__modules[this.abs_path(full_name)] = [[], [], [[this.__path, name]]]
|
||||
|
||||
return this[name] = null;
|
||||
}
|
||||
|
||||
} else
|
||||
throw new TypeError(`must provide a valid module name or class`);
|
||||
|
||||
if ( ! module )
|
||||
throw new Error(`cannot find module ${name} or no module provided`);
|
||||
|
||||
requires.push(module.abs_path('.'));
|
||||
|
||||
if ( this.enabled && ! module.enabled )
|
||||
module.enable();
|
||||
|
||||
return this[name] = module;
|
||||
}
|
||||
|
||||
|
||||
register(name, module, inject_reference) {
|
||||
if ( name.prototype instanceof Module ) {
|
||||
inject_reference = module;
|
||||
module = name;
|
||||
name = module.name.toSnakeCase();
|
||||
}
|
||||
|
||||
const path = this.abs_path(`.${name}`),
|
||||
proto = module.prototype,
|
||||
old_val = this.__modules[path];
|
||||
|
||||
if ( !(proto instanceof Module) )
|
||||
throw new TypeError(`Module ${name} is not subclass of Module.`);
|
||||
|
||||
if ( old_val instanceof Module )
|
||||
throw new ModuleError(`Name Collision for Module ${path}`);
|
||||
|
||||
const dependents = old_val || [[], [], []],
|
||||
inst = this.__modules[path] = new module(name, this),
|
||||
requires = inst.requires = inst.__get_requires() || [],
|
||||
load_requires = inst.load_requires = inst.__get_load_requires() || [];
|
||||
|
||||
inst.dependents = dependents[0];
|
||||
inst.load_dependents = dependents[1];
|
||||
|
||||
if ( inst instanceof SiteModule && ! requires.includes('site') )
|
||||
requires.push('site');
|
||||
|
||||
for(const req_name of requires) {
|
||||
const req_path = inst.abs_path(req_name),
|
||||
req_mod = this.__modules[req_path];
|
||||
|
||||
if ( ! req_mod )
|
||||
this.__modules[req_path] = [[path],[],[]];
|
||||
else if ( Array.isArray(req_mod) )
|
||||
req_mod[0].push(path);
|
||||
else
|
||||
req_mod.dependents.push(path);
|
||||
}
|
||||
|
||||
for(const req_name of load_requires) {
|
||||
const req_path = inst.abs_path(req_name),
|
||||
req_mod = this.__modules[req_path];
|
||||
|
||||
if ( ! req_mod )
|
||||
this.__modules[req_path] = [[], [path], []];
|
||||
else if ( Array.isArray(req_mod) )
|
||||
req_mod[1].push(path);
|
||||
else
|
||||
req_mod.load_dependents.push(path);
|
||||
}
|
||||
|
||||
for(const [in_path, in_name] of dependents[2]) {
|
||||
const in_mod = this.resolve(in_path);
|
||||
if ( in_mod )
|
||||
in_mod[in_name] = inst;
|
||||
else
|
||||
this.log.warn(`Unable to find module "${in_path}" that wanted "${in_name}".`);
|
||||
}
|
||||
|
||||
if ( inject_reference )
|
||||
this[name] = inst;
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
||||
|
||||
populate(ctx, log) {
|
||||
log = log || this.log;
|
||||
const added = {};
|
||||
for(const raw_path of ctx.keys()) {
|
||||
const raw_module = ctx(raw_path),
|
||||
module = raw_module.module || raw_module.default,
|
||||
name = raw_path.slice(2, raw_path.length - (raw_path.endsWith('/index.js') ? 9 : 3));
|
||||
|
||||
try {
|
||||
added[name] = this.register(name, module);
|
||||
} catch(err) {
|
||||
log && log.warn(err, `Skipping ${raw_path}`);
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Module.State = State;
|
||||
Module.prototype.State = State;
|
||||
|
||||
|
||||
export class SiteModule extends Module {
|
||||
constructor(name, parent) {
|
||||
super(name, parent);
|
||||
this.site = this.resolve('site');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Errors
|
||||
// ============================================================================
|
||||
|
||||
export class ModuleError extends Error { }
|
||||
|
||||
export class CyclicDependencyError extends ModuleError {
|
||||
constructor(message, modules) {
|
||||
super(message);
|
||||
this.modules = modules;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue