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:
parent
0d433c3ebd
commit
0fd86b1bb8
12 changed files with 522 additions and 274 deletions
|
@ -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 )
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
130
src/i18n.js
130
src/i18n.js
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
12
src/raven.js
12
src/raven.js
|
@ -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',
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue