1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00

WIP async modules

This commit is contained in:
SirStendec 2021-02-15 14:25:53 -05:00
parent 0d433c3ebd
commit 0fd86b1bb8
12 changed files with 522 additions and 274 deletions

View file

@ -284,7 +284,7 @@ export default class AddonManager extends Module {
}));
// Error if this takes more than 5 seconds.
await timeout(this.waitFor(`addon.${id}:registered`), 5000);
await timeout(this.waitFor(`addon.${id}:instanced`), 5000);
module = this.resolve(`addon.${id}`);
if ( module && ! module.loaded )

View file

@ -11,6 +11,8 @@ import {serializeBlob, deserializeBlob} from 'utilities/blobs';
import SettingsManager from './settings/index';
class FFZBridge extends Module {
static construct_requires = null;
constructor() {
super();
const start_time = performance.now(),

View file

@ -45,11 +45,11 @@ function sortExperimentLog(a,b) {
// ============================================================================
export default class ExperimentManager extends Module {
static construct_requires = ['settings'];
constructor(...args) {
super(...args);
this.inject('settings');
this.settings.addUI('experiments', {
path: 'Debugging > Experiments',
component: 'experiments',
@ -289,7 +289,7 @@ export default class ExperimentManager extends Module {
return window.__twilightSettings.experiments;
const core = this.resolve('site')?.getCore?.();
return core && core.experiments.experiments;
return core && core.experiments?.experiments;
}

View file

@ -69,6 +69,8 @@ const FACES = ['(・`ω´・)', ';;w;;', 'owo', 'ono', 'oAo', 'oxo', 'ovo;', 'Uw
// ============================================================================
export class TranslationManager extends Module {
static construct_requires = null;
constructor(...args) {
super(...args);
this.inject('settings');
@ -90,7 +92,71 @@ export class TranslationManager extends Module {
this.changed_strings = 0;
this.capturing = false;
this.captured = new Map;
}
getLocaleOptions(val) {
if( val === undefined )
val = this.settings.get('i18n.locale');
const normal_out = [],
joke_out = [];
for(const locale of this.availableLocales) {
const data = this.localeData[locale];
let title = data?.native_name || data?.name || locale;
if ( data?.coverage != null && data?.coverage < 100 )
title = this.t('i18n.locale-coverage', '{name} ({coverage,number,percent} Complete)', {
name: title,
coverage: data.coverage / 100
});
const entry = {
selected: val === locale,
value: locale,
title
};
if ( data?.joke )
joke_out.push(entry);
else
normal_out.push(entry);
}
normal_out.sort((a, b) => a.title.localeCompare(b.title));
joke_out.sort((a, b) => a.title.localeCompare(b.title));
let out = [{
selected: val === -1,
value: -1,
i18n_key: 'setting.appearance.localization.general.language.twitch',
title: "Use Twitch's Language"
}];
if ( normal_out.length ) {
out.push({
separator: true,
i18n_key: 'setting.appearance.localization.general.language.languages',
title: 'Supported Languages'
});
out = out.concat(normal_out);
}
if ( joke_out.length ) {
out.push({
separator: true,
i18n_key: 'setting.appearance.localization.general.language.joke',
title: 'Joke Languages'
});
out = out.concat(joke_out);
}
return out;
}
onEnable() {
this.settings.addUI('i18n.debug.open', {
path: 'Debugging > Localization >> Editing',
component: 'i18n-open',
@ -264,71 +330,7 @@ export class TranslationManager extends Module {
this.emit(':update')
}
});
}
getLocaleOptions(val) {
if( val === undefined )
val = this.settings.get('i18n.locale');
const normal_out = [],
joke_out = [];
for(const locale of this.availableLocales) {
const data = this.localeData[locale];
let title = data?.native_name || data?.name || locale;
if ( data?.coverage != null && data?.coverage < 100 )
title = this.t('i18n.locale-coverage', '{name} ({coverage,number,percent} Complete)', {
name: title,
coverage: data.coverage / 100
});
const entry = {
selected: val === locale,
value: locale,
title
};
if ( data?.joke )
joke_out.push(entry);
else
normal_out.push(entry);
}
normal_out.sort((a, b) => a.title.localeCompare(b.title));
joke_out.sort((a, b) => a.title.localeCompare(b.title));
let out = [{
selected: val === -1,
value: -1,
i18n_key: 'setting.appearance.localization.general.language.twitch',
title: "Use Twitch's Language"
}];
if ( normal_out.length ) {
out.push({
separator: true,
i18n_key: 'setting.appearance.localization.general.language.languages',
title: 'Supported Languages'
});
out = out.concat(normal_out);
}
if ( joke_out.length ) {
out.push({
separator: true,
i18n_key: 'setting.appearance.localization.general.language.joke',
title: 'Joke Languages'
});
out = out.concat(joke_out);
}
return out;
}
onEnable() {
this.capturing = this.settings.get('i18n.debug.capture');
if ( this.capturing )
this.loadStrings();

View file

@ -15,6 +15,8 @@ import {TranslationManager} from './i18n';
import Site from './sites/player';
class FFZPlayer extends Module {
static construct_requires = null;
constructor() {
super();
const start_time = performance.now(),

View file

@ -54,6 +54,8 @@ const ERROR_STRINGS = [
// ============================================================================
export default class RavenLogger extends Module {
static construct_requires = null;
constructor(...args) {
super(...args);
@ -62,7 +64,9 @@ export default class RavenLogger extends Module {
// Do these in an event handler because we're initialized before
// settings are even ready.
this.once('settings:enabled', () => {
this.settings.add('reports.error.enable', {
const settings = this.resolve('settings');
settings.add('reports.error.enable', {
default: true,
ui: {
path: 'Data Management > Reporting >> Error Reports',
@ -71,7 +75,7 @@ export default class RavenLogger extends Module {
}
});
this.settings.add('reports.error.include-user', {
settings.add('reports.error.include-user', {
default: false,
ui: {
path: 'Data Management > Reporting >> Error Reports',
@ -81,7 +85,7 @@ export default class RavenLogger extends Module {
}
});
this.settings.add('reports.error.include-settings', {
settings.add('reports.error.include-settings', {
default: true,
ui: {
path: 'Data Management > Reporting >> Error Reports',
@ -91,7 +95,7 @@ export default class RavenLogger extends Module {
}
});
this.settings.addUI('reports.error.example', {
settings.addUI('reports.error.example', {
path: 'Data Management > Reporting >> Example Report',
component: 'async-text',

View file

@ -40,6 +40,8 @@ export const NO_SYNC_KEYS = ['session'];
* @extends Module
*/
export default class SettingsManager extends Module {
static construct_requires = null;
/**
* Create a SettingsManager module.
*/

View file

@ -23,12 +23,15 @@ export default class PlayerSite extends BaseSite {
constructor(...args) {
super(...args);
this.inject('settings');
this.inject('i18n');
this.inject(Fine);
this.inject(Player);
this.inject('tooltips', Tooltips);
this.inject('css_tweaks', CSSTweaks);
}
onEnable() {
this.DataSource = this.fine.define(
'data-source',
n => n.consentMetadata && n.onPlaying && n.props && n.props.data
@ -38,10 +41,6 @@ export default class PlayerSite extends BaseSite {
'player-menu',
n => n.closeSettingsMenu && n.state && n.state.activeMenu && n.getMaxMenuHeight
);
}
onEnable() {
this.settings = this.resolve('settings');
this.DataSource.on('mount', this.updateData, this);
this.DataSource.on('update', this.updateData, this);

View file

@ -22,20 +22,6 @@ export default class Metadata extends Module {
this.definitions = {};
this.settings.add('metadata.player-stats', {
default: false,
changed: () => this.updateMetadata('player-stats')
});
this.settings.add('metadata.stream-delay-warning', {
default: 0
});
this.settings.add('metadata.uptime', {
default: 1,
changed: () => this.updateMetadata('uptime')
});
this.definitions.uptime = {
inherit: true,
no_arrow: true,
@ -353,6 +339,20 @@ export default class Metadata extends Module {
}
onEnable() {
this.settings.add('metadata.player-stats', {
default: false,
changed: () => this.updateMetadata('player-stats')
});
this.settings.add('metadata.stream-delay-warning', {
default: 0
});
this.settings.add('metadata.uptime', {
default: 1,
changed: () => this.updateMetadata('uptime')
});
const md = this.tooltips.types.metadata = target => {
let el = target;
if ( el._ffz_stat )

View file

@ -23,7 +23,6 @@ export default class Player extends PlayerBase {
'player-source',
n => n.setSrc && n.setInitialPlaybackSettings
);
}
wantsMetadata() {
return this.settings.get('player.embed-metadata');

View file

@ -27,7 +27,7 @@ export default class VideoChatHook extends Module {
this.inject('site.web_munch');
this.inject('chat');
this.injectAs('site_chat', 'site.chat');
this.inject('site.chat', undefined, false, 'site_chat');
this.inject('site.chat.chat_line.rich_content');
this.VideoChatController = this.fine.define(

View file

@ -7,6 +7,7 @@
import EventEmitter from 'utilities/events';
import {has} from 'utilities/object';
import { load } from './font-awesome';
// ============================================================================
@ -22,8 +23,12 @@ export const State = {
DISABLED: 0,
ENABLING: 1,
ENABLED: 2,
DISABLING: 3
}
DISABLING: 3,
UNINJECTED: 0,
LOAD_INJECTED: 1,
FULL_INJECTED: 2
};
export class Module extends EventEmitter {
@ -34,6 +39,9 @@ export class Module extends EventEmitter {
}
super(name, parent);
this.__module_promises = parent ? parent.__module_promises : {};
this.__module_dependents = parent ? parent.__module_dependents : {};
this.__module_sources = parent ? parent.__module_sources : {};
this.__modules = parent ? parent.__modules : {};
this.children = {};
@ -43,12 +51,22 @@ export class Module extends EventEmitter {
if ( this.root === this )
this.__modules[this.__path || ''] = this;
this.__constructed = false;
this.__load_injections = {};
this.__enable_injections = {};
this.__inject_state = State.UNINJECTED;
this.__load_state = this.onLoad ? State.UNLOADED : State.LOADED;
this.__state = this.onLoad || this.onEnable ?
State.DISABLED : State.ENABLED;
// Inject any pre-construction injections.
const injections = this.__get_construct_requires();
if ( injections )
for(const [key, path] of Object.entries(injections))
this[key] = this.resolve(path, false, false);
this.__time('instance');
this.emit(':registered');
this.emit(':instanced');
}
@ -65,7 +83,6 @@ export class Module extends EventEmitter {
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);
@ -112,6 +129,17 @@ export class Module extends EventEmitter {
}
__inject(injections) {
for(const [attr, name] of Object.entries(injections)) {
const module = this.resolve(name);
if ( ! module || !(module instanceof Module) )
throw new ModuleError(`unable to inject dependency ${name} for module ${this.name}`);
this[attr] = module;
}
}
__load(args, initial, chain) {
const path = this.__path || this.name,
state = this.__load_state;
@ -142,16 +170,27 @@ export class Module extends EventEmitter {
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}`);
// Resolve and instantiate the module.
promises.push(Promise.resolve(this.resolve(name, true)).then(module => {
if ( ! module || !(module instanceof Module) )
throw new ModuleError(`cannot find required module ${name} when loading ${path}`);
promises.push(module.__enable([], initial, Array.from(chain)));
return module.__enable([], initial, Array.from(chain));
}));
}
await Promise.all(promises);
}
if ( this.__inject_state === State.UNINJECTED ) {
if ( this.__load_injections ) {
this.__inject(this.__load_injections);
this.__load_injections = null;
}
this.__inject_state = State.LOAD_INJECTED;
}
if ( this.onLoad ) {
this.__time('load-self');
return this.onLoad(...args);
@ -163,6 +202,7 @@ export class Module extends EventEmitter {
this.__time('load-end');
this.emit(':loaded', this);
return ret;
}).catch(err => {
this.__load_state = State.UNLOADED;
this.__load_promise = null;
@ -208,6 +248,8 @@ export class Module extends EventEmitter {
if ( this.load_dependents ) {
const promises = [];
for(const name of this.load_dependents) {
// All our dependents should be instantiated. An uninstantiated module is not loaded
// so we obviously do not need to unload it at this time.
const module = this.resolve(name);
if ( ! module )
//throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`);
@ -228,6 +270,7 @@ export class Module extends EventEmitter {
this.__time('unload-end');
this.emit(':unloaded', this);
return ret;
}).catch(err => {
this.__load_state = State.LOADED;
this.__load_promise = null;
@ -237,6 +280,44 @@ export class Module extends EventEmitter {
}
/*generateLoadGraph(chain) {
let initial = false;
if ( ! chain ) {
chain = [];
initial = true;
}
if ( chain.includes(this) )
return [`${this.name}: cyclic requirement`];
chain.push(this);
const out = [];
out.push(`${this.name}: ${this.enabled ? 'enabled' : this.enabling ? 'enabling' : this.disabling ? 'disabling' : 'disabled'}`);
const requires = this.requires;
if ( requires )
for(const req of requires) {
const module = this.resolve(req)
let mod_out;
if ( ! module )
mod_out = [`${req}: uninstantiated`];
else if ( ! module.enabled )
mod_out = module.generateLoadGraph(Array.from(chain));
else
continue;
for(const line of mod_out)
out.push(` ${line}`);
}
if ( initial )
return out.join('\n');
return out;
}*/
__enable(args, initial, chain) {
const path = this.__path || this.name,
state = this.__state;
@ -268,24 +349,42 @@ export class Module extends EventEmitter {
requires = this.requires,
load_state = this.__load_state;
// Make sure our module is loaded before enabling it.
if ( load_state === State.UNLOADING )
// We'd abort for this later to, but kill it now before we start
// We'd abort for this later too, 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());
// We also want to load all our dependencies.
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(Promise.resolve(this.resolve(name, true).then(module => {
if ( ! module || !(module instanceof Module) )
throw new ModuleError(`cannot find required module ${name} when enabling ${path}`);
promises.push(module.__enable([], initial, Array.from(chain)));
return module.__enable([], initial, Array.from(chain));
})));
}
await Promise.all(promises);
if ( this.__inject_state !== State.FULL_INJECTED ) {
if ( this.__load_injections ) {
this.__inject(this.__load_injections);
this.__load_injections = null;
}
if ( this.__enable_injections ) {
this.__inject(this.__enable_injections);
this.__enable_injections = null;
}
this.__inject_state = State.FULL_INJECTED;
}
if ( this.onEnable ) {
this.__time('enable-self');
return this.onEnable(...args);
@ -336,15 +435,18 @@ export class Module extends EventEmitter {
this.__time('disable-start');
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
// We'd abort for this later too, 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) {
// All our dependents should be instantiated. An uninstantiated module is not enabled
// so we obviously do not need to disable it at this time.
const module = this.resolve(name);
if ( ! module )
// Assume a non-existent module isn't enabled.
@ -413,38 +515,210 @@ export class Module extends EventEmitter {
// Child Control
// ========================================================================
loadModules(...names) {
return Promise.all(names.map(n => this.resolve(n).load()))
// These aren't being used anywhere.
/*loadModules(...names) {
return Promise.all(names.map(n => this.resolve(n, true).then(module => module.load())));
}
unloadModules(...names) {
return Promise.all(names.map(n => this.resolve(n).unload()))
return Promise.all(names.map(n => this.resolve(n)?.unload?.()));
}
enableModules(...names) {
return Promise.all(names.map(n => this.resolve(n).enable()))
return Promise.all(names.map(n => this.resolve(n, true).then(module => module.enable())));
}
disableModules(...names) {
return Promise.all(names.map(n => this.resolve(n).disable()))
}
return Promise.all(names.map(n => this.resolve(n)?.disable?.()));
}*/
// ========================================================================
// Module Management
// ========================================================================
resolve(name) {
if ( name instanceof Module )
return name;
/**
* Resolve a module. This will only return a module that has already been
* constructed, by default. If `construct` is true, a Promise will be
* returned and a module instance will be constructed.
*
* @param {String} name The name of the module to resolve.
* @param {Boolean} [construct=false] Whether or not a module
* should be constructed if it has not been already. When this is true,
* this method will always return a promise. When this is false, the
* method will never return a promise.
* @param {Boolean} [allow_missing=true] When this is false, an exception
* will be thrown for a missing module, rather than returning null.
* @returns {Module|Promise} A module, or a Promise that will return a
* module, depending on the value of `construct`.
*/
resolve(name, construct = false, allow_missing = true) {
const path = this.abs_path(name),
source = this.__module_sources[path],
module = this.__modules[path];
return this.__modules[this.abs_path(name)];
if ( ! construct ) {
if ( ! module && ! allow_missing ) {
if ( source )
throw new ModuleError(`instance for module "${path}" has not been constructed`);
else
throw new ModuleError(`unknown module "${path}"`);
}
return module || null;
}
// We have the module already, but wrap it in a promise for safety.
if ( module )
return Promise.resolve(module);
// We do not have the module. Do we know how to load it?
// If not, then return null or an exception.
if ( ! source ) {
if ( allow_missing )
return Promise.resolve(null);
return Promise.reject(new ModuleError(`unknown module "${path}"`));
}
// To instantiate a module, we need the name and the parent module.
const idx = path.lastIndexOf('.'),
nm = path.slice(idx + 1);
let p_path = null;
if ( idx !== -1 )
p_path = path.slice(0, idx);
console.log('resolve', name, path, nm, p_path);
// Is there an existing promise for constructing this module?
if ( this.__module_promises[path] )
return new Promise((s,f) => this.__module_promises[path].push([s,f]));
// We're still here, so load and instantiate the module, then
// return it.
return new Promise((s,f) => {
const proms = this.__module_promises[path] = [[s,f]];
(async () => {
let parent;
if ( p_path === this.__path )
parent = this;
else if ( p_path )
parent = await this.resolve(p_path, true, false);
else
parent = this.root;
const loader = await source;
if ( ! loader )
throw new ModuleError(`invalid loader for module "${path}`);
// Do we have pre-construct requirements?
const pre_requires = name === 'settings' ? null : ['settings'];
if ( Array.isArray(pre_requires) ) {
const promises = [];
for(const dep of pre_requires)
promises.push(Promise.resolve(this.resolve(dep, true)).then(module => {
if ( ! module || !(module instanceof Module) )
throw new ModuleError(`cannot find required module ${dep} when loading ${path}`);
return module.__enable([], path, []);
}));
await Promise.all(promises);
}
let module;
if ( loader.prototype instanceof Module )
module = new loader(nm, parent);
else
module = loader(nm, parent);
if ( ! module || !(module instanceof Module))
throw new ModuleError(`invalid return value from module constructor for module "${path}"`);
module.__constructed = true;
this.__modules[path] = module;
const deps = this.__module_dependents[path];
this.__module_dependents[path] = null;
// Copy over the dependencies.
if ( deps ) {
if ( deps.load )
module.load_dependents = module.load_dependents ? [...module.load_dependents, ...deps.load] : deps.load;
if ( deps.enable )
module.dependents = module.dependents ? [...module.dependents, ...deps.enable] : deps.enable;
}
// Inject any requirements.
this.__reflectDependencies();
return module;
})().then(result => {
this.__module_promises[path] = null;
for(const pair of proms)
pair[0](result);
}).catch(err => {
this.__module_promises[path] = null;
for(const pair of proms)
pair[1](err);
});
});
}
hasModule(name) {
const module = this.__modules[this.abs_path(name)];
return module instanceof Module;
/**
* Reflect the dependencies of this module, registering it with
* all modules it depends on as a dependent so that those modules
* know to disable or unload this module when appropriate.
*
* @param {String[]} [load=null] An optional array of specific load dependencies to reflect.
* @param {String[]} [enable=null] An optional array of specific dependencies to reflect.
* @returns {undefined} Nothing
*/
__reflectDependencies(load = null, enable = null) {
if ( load == null )
load = this.__get_load_requires();
if ( enable == null )
enable = this.__get_requires();
if ( load && ! Array.isArray(load) )
load = [load];
if ( enable && ! Array.isArray(enable) )
enable = [enable];
const local = this.__path || 'core';
if ( load && load.length )
for(const path of load) {
const module = this.__modules[path];
if ( module ) {
const dependents = module.load_dependents = module.load_dependents || [];
if ( ! dependents.includes(local) )
dependents.push(local);
} else {
const dependents = this.__module_dependents[path] = this.__module_dependents[path] || {},
set = dependents.load = dependents.load || [];
if ( ! set.includes(local) )
set.push(local);
}
}
if ( enable && enable.length )
for(const path of enable) {
const module = this.__modules[path];
if ( module ) {
const dependents = module.dependents = module.dependents || [];
if ( ! dependents.includes(local) )
dependents.push(local);
} else {
const dependents = this.__module_dependents[path] = this.__module_dependents[path] || {},
set = dependents.enable = dependents.enable || [];
if ( ! set.includes(local) )
set.push(local);
}
}
}
@ -464,182 +738,146 @@ export class Module extends EventEmitter {
}
inject(name, module, require = true) {
if ( name instanceof Module || name.prototype instanceof Module ) {
require = module != null ? module : true;
module = name;
name = null;
__get_construct_requires() {
let out;
if ( has(this, 'construct_requires') )
out = this.construct_requires;
else if ( has(this.constructor, 'construct_requires') )
out = this.constructor.construct_requires;
else
out = ['settings', 'i18n', 'experiments'];
if ( ! out || ! Array.isArray(out) )
return out;
const obj = {};
for(const path of out) {
const full = this.abs_path(path),
idx = full.lastIndexOf('.'),
name = full.slice(idx + 1);
obj[name] = full;
}
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;
return obj;
}
injectAs(variable, name, module, require = true) {
if ( name instanceof Module || name.prototype instanceof Module ) {
require = module != null ? module : true;
module = name;
name = null;
/**
* Inject a dependency into this module. Dependencies are added as
* requirements, and are saved as variables with the module's name
* within this module for easy access. Injecting the module `settings`
* for example allows access to `this.settings` to use the settings
* module.
*
* Please note that injected dependencies are NOT available until
* the module is being enabled in the case of normal dependencies,
* or until the module is being loaded in the case of load
* dependencies.
*
* **Note:** Rather than providing a name, you can provide only
* a Module class or instance and the name will be determined
* based on the class name of the module. When doing so, you are
* not allowed to provide a Promise or other type of function
* loader.
*
* @param {String} name The name of the module to inject.
* @param {Class|Function|Promise} [loader] The loader that will
* provide the module we're injecting. This will be used to
* construct the module on demand .
* @param {Boolean} [load=false] If this is true, the injected
* dependency will be treated as a load dependency rather and
* injected prior to this module being loaded.
* @param {String} [key=null] An optional attribute name for
* injecting this dependency. If not provided, the name will be
* used as the attribute name.
* @returns {undefined} Nothing
*/
inject(name, loader, load = false, key = null) {
if ( this.__constructed )
throw new ModuleError(`Unable to use inject() outside constructor`);
// Did we get a name?
if ( typeof name !== 'string' ) {
// We didn't. Did we get a Module?
if ( name instanceof Module || name.prototype instanceof Module ) {
key = load;
load = loader;
loader = name;
name = null;
} else
throw new Error(`invalid type for name`);
}
const requires = this.requires = this.__get_requires() || [];
// If we have a loader, go ahead and register it. This will also give us
// a name if we don't have one.
if ( loader )
name = this.register(name, loader);
if ( module instanceof Module ) {
// Existing Instance
if ( ! name )
name = module.constructor.name.toSnakeCase();
if ( ! key ) {
const idx = name.lastIndexOf('.');
key = (idx === -1 ? name : name.slice(idx + 1)).toSnakeCase();
}
} else if ( module && module.prototype instanceof Module ) {
// New Instance
if ( ! name )
name = module.name.toSnakeCase();
const path = this.abs_path(name);
module = this.register(name, module);
// Save this dependency, and also save it on the target module.
if ( load ) {
const requires = this.load_requires = this.__get_load_requires() || [];
if ( ! requires.includes(path) )
requires.push(path);
} else if ( name ) {
// Just a Name
const full_name = name;
name = name.replace(/^(?:[^.]*\.)+/, '');
module = this.resolve(full_name);
this.__reflectDependencies(path, false);
this.__load_injections[key] = path;
// Allow injecting a module that doesn't exist yet?
} else {
const requires = this.requires = this.__get_requires() || [];
if ( ! requires.includes(path) )
requires.push(path);
if ( ! module || !(module instanceof Module) ) {
if ( module )
module[2].push([this.__path, variable]);
else
this.__modules[this.abs_path(full_name)] = [[], [], [[this.__path, variable]]]
requires.push(this.abs_path(full_name));
return this[variable] = 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[variable] = module;
this.__reflectDependencies(false, path);
this.__enable_injections[key] = path;
}
}
register(name, module, inject_reference) {
if ( name.prototype instanceof Module ) {
inject_reference = module;
module = name;
name = module.name.toSnakeCase();
/**
* Register a module into the module tree. By default, this does very
* little. When providing a Module class, you can omit the name
* argument. In that case, a name will be inferred from the class name.
*
* When supplying a function or Promise for asynchronous loading, a
* name is required.
*
* The name is always treated as being relative to the current module.
* This is done by prefixing the name with a `.` character.
*
* @param {String} [name] The name of the Module being registered.
* @param {Class|Function|Promise} loader A Module class, or a function or
* promise that will eventually return a Module class.
* @returns {String} The name of the Module.
*/
register(name, loader) {
if ( name && name.prototype instanceof Module ) {
loader = name;
name = null;
}
const path = this.abs_path(`.${name}`),
proto = module.prototype,
old_val = this.__modules[path];
if ( ! name && loader && loader.prototype instanceof Module )
name = loader.name.toSnakeCase();
if ( !(proto instanceof Module) )
throw new TypeError(`Module ${name} is not subclass of Module.`);
if ( ! name || typeof name !== 'string' )
throw new TypeError('Invalid name');
if ( old_val instanceof Module )
// Make sure the name is relative.
name = `.${name}`;
const path = this.abs_path(name);
if ( this.__modules[path] || this.__module_sources[path] )
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;
this.__module_sources[path] = loader;
return name;
}
@ -695,7 +933,7 @@ export class ModuleError extends Error { }
export class CyclicDependencyError extends ModuleError {
constructor(message, modules) {
super(`${message} (${modules.map(x => x.path).join(' => ')})`);
super(`${message} ${modules ? `(${modules.map(x => x.path).join(' => ')})` : ''}`);
this.modules = modules;
}
}