mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-06 02:58:31 +00:00
This adds a new module called switchboard that abuses the root React Router instance to forcibly load a chunk, letting us grab `require()` quickly rather than waiting potentially forever for another chunk to be loaded due to user action, etc.
583 lines
No EOL
15 KiB
JavaScript
583 lines
No EOL
15 KiB
JavaScript
'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 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)];
|
|
}
|
|
|
|
|
|
hasModule(name) {
|
|
const module = this.__modules[this.abs_path(name)];
|
|
return module instanceof Module;
|
|
}
|
|
|
|
|
|
__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, require = true) {
|
|
if ( name instanceof Module || name.prototype instanceof Module ) {
|
|
require = module != null ? module : true;
|
|
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]]]
|
|
|
|
requires.push(this.abs_path(full_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`);
|
|
|
|
if ( require )
|
|
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,
|
|
lix = raw_path.lastIndexOf('.'),
|
|
trimmed = lix > 2 ? raw_path.slice(2, lix) : raw_path,
|
|
name = trimmed.endsWith('/index') ? trimmed.slice(0, -6) : trimmed;
|
|
|
|
try {
|
|
added[name] = this.register(name, module);
|
|
} catch(err) {
|
|
log && log.capture(err, {
|
|
extra: {
|
|
module: name,
|
|
path: raw_path
|
|
}
|
|
});
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
export default Module;
|
|
|
|
|
|
// ============================================================================
|
|
// Errors
|
|
// ============================================================================
|
|
|
|
export class ModuleError extends Error { }
|
|
|
|
export class CyclicDependencyError extends ModuleError {
|
|
constructor(message, modules) {
|
|
super(message);
|
|
this.modules = modules;
|
|
}
|
|
} |