mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
* Convert AddonManager to TypeScript.
* Convert PubSub (not PubSubClient) to TypeScript. * Convert StagingSelector to TypeScript. * Make sure to add ExperimentManager's events to the global interface.
This commit is contained in:
parent
6c6d4ceb98
commit
31e7ce4ac5
6 changed files with 221 additions and 93 deletions
|
@ -4,21 +4,71 @@
|
||||||
// Add-On System
|
// Add-On System
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import { EXTENSION, SERVER_OR_EXT } from 'utilities/constants';
|
import { EXTENSION, SERVER_OR_EXT } from 'utilities/constants';
|
||||||
import { createElement } from 'utilities/dom';
|
import { createElement } from 'utilities/dom';
|
||||||
import { timeout, has, deep_copy } from 'utilities/object';
|
import { timeout, has, deep_copy, fetchJSON } from 'utilities/object';
|
||||||
import { getBuster } from 'utilities/time';
|
import { getBuster } from 'utilities/time';
|
||||||
|
import type SettingsManager from './settings';
|
||||||
|
import type TranslationManager from './i18n';
|
||||||
|
import type LoadTracker from './load_tracker';
|
||||||
|
import type FrankerFaceZ from './main';
|
||||||
|
import type { AddonInfo } from 'utilities/types';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ffzAddonsWebpackJsonp: unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'utilities/types' {
|
||||||
|
interface ModuleMap {
|
||||||
|
addons: AddonManager;
|
||||||
|
}
|
||||||
|
interface ModuleEventMap {
|
||||||
|
addons: AddonManagerEvents;
|
||||||
|
}
|
||||||
|
interface SettingsTypeMap {
|
||||||
|
'addons.dev.server': boolean;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddonManagerEvents = {
|
||||||
|
':ready': [];
|
||||||
|
':data-loaded': [];
|
||||||
|
':reload-required': [];
|
||||||
|
|
||||||
|
':added': [id: string, info: AddonInfo];
|
||||||
|
':addon-loaded': [id: string];
|
||||||
|
':addon-enabled': [id: string];
|
||||||
|
':addon-disabled': [id: string];
|
||||||
|
':fully-unload': [id: string];
|
||||||
|
};
|
||||||
|
|
||||||
const fetchJSON = (url, options) => fetch(url, options).then(r => r.ok ? r.json() : null).catch(() => null);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// AddonManager
|
// AddonManager
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export default class AddonManager extends Module {
|
export default class AddonManager extends Module<'addons'> {
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
// Dependencies
|
||||||
|
i18n: TranslationManager = null as any;
|
||||||
|
load_tracker: LoadTracker = null as any;
|
||||||
|
settings: SettingsManager = null as any;
|
||||||
|
|
||||||
|
// State
|
||||||
|
has_dev: boolean;
|
||||||
|
reload_required: boolean;
|
||||||
|
target: string;
|
||||||
|
|
||||||
|
addons: Record<string, AddonInfo | string[]>;
|
||||||
|
enabled_addons: string[];
|
||||||
|
|
||||||
|
private _loader?: Promise<void>;
|
||||||
|
|
||||||
|
constructor(name?: string, parent?: GenericModule) {
|
||||||
|
super(name, parent);
|
||||||
|
|
||||||
this.should_enable = true;
|
this.should_enable = true;
|
||||||
|
|
||||||
|
@ -28,7 +78,7 @@ export default class AddonManager extends Module {
|
||||||
|
|
||||||
this.load_requires = ['settings'];
|
this.load_requires = ['settings'];
|
||||||
|
|
||||||
this.target = this.parent.flavor || 'unknown';
|
this.target = (this.parent as unknown as FrankerFaceZ).flavor || 'unknown';
|
||||||
|
|
||||||
this.has_dev = false;
|
this.has_dev = false;
|
||||||
this.reload_required = false;
|
this.reload_required = false;
|
||||||
|
@ -39,6 +89,7 @@ export default class AddonManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
// We don't actually *wait* for this, we just start it.
|
||||||
this._loader = this.loadAddonData();
|
this._loader = this.loadAddonData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,20 +105,20 @@ export default class AddonManager extends Module {
|
||||||
getFFZ: () => this,
|
getFFZ: () => this,
|
||||||
isReady: () => this.enabled,
|
isReady: () => this.enabled,
|
||||||
getAddons: () => Object.values(this.addons),
|
getAddons: () => Object.values(this.addons),
|
||||||
hasAddon: id => this.hasAddon(id),
|
hasAddon: (id: string) => this.hasAddon(id),
|
||||||
getVersion: id => this.getVersion(id),
|
getVersion: (id: string) => this.getVersion(id),
|
||||||
doesAddonTarget: id => this.doesAddonTarget(id),
|
doesAddonTarget: (id: string) => this.doesAddonTarget(id),
|
||||||
isAddonEnabled: id => this.isAddonEnabled(id),
|
isAddonEnabled: (id: string) => this.isAddonEnabled(id),
|
||||||
isAddonExternal: id => this.isAddonExternal(id),
|
isAddonExternal: (id: string) => this.isAddonExternal(id),
|
||||||
enableAddon: id => this.enableAddon(id),
|
enableAddon: (id: string) => this.enableAddon(id),
|
||||||
disableAddon: id => this.disableAddon(id),
|
disableAddon: (id: string) => this.disableAddon(id),
|
||||||
reloadAddon: id => this.reloadAddon(id),
|
reloadAddon: (id: string) => this.reloadAddon(id),
|
||||||
canReloadAddon: id => this.canReloadAddon(id),
|
canReloadAddon: (id: string) => this.canReloadAddon(id),
|
||||||
isReloadRequired: () => this.reload_required,
|
isReloadRequired: () => this.reload_required,
|
||||||
refresh: () => window.location.reload(),
|
refresh: () => window.location.reload(),
|
||||||
|
|
||||||
on: (...args) => this.on(...args),
|
on: (...args: Parameters<typeof this.on>) => this.on(...args),
|
||||||
off: (...args) => this.off(...args)
|
off: (...args: Parameters<typeof this.off>) => this.off(...args)
|
||||||
});
|
});
|
||||||
|
|
||||||
if ( ! EXTENSION )
|
if ( ! EXTENSION )
|
||||||
|
@ -85,7 +136,7 @@ export default class AddonManager extends Module {
|
||||||
|
|
||||||
this.settings.provider.on('changed', this.onProviderChange, this);
|
this.settings.provider.on('changed', this.onProviderChange, this);
|
||||||
|
|
||||||
this._loader.then(() => {
|
this._loader?.then(() => {
|
||||||
this.enabled_addons = this.settings.provider.get('addons.enabled', []);
|
this.enabled_addons = this.settings.provider.get('addons.enabled', []);
|
||||||
|
|
||||||
// We do not await enabling add-ons because that would delay the
|
// We do not await enabling add-ons because that would delay the
|
||||||
|
@ -103,8 +154,8 @@ export default class AddonManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
doesAddonTarget(id) {
|
doesAddonTarget(id: string) {
|
||||||
const data = this.addons[id];
|
const data = this.getAddon(id);
|
||||||
if ( ! data )
|
if ( ! data )
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -118,12 +169,15 @@ export default class AddonManager extends Module {
|
||||||
|
|
||||||
generateLog() {
|
generateLog() {
|
||||||
const out = ['Known'];
|
const out = ['Known'];
|
||||||
for(const [id, addon] of Object.entries(this.addons))
|
for(const [id, addon] of Object.entries(this.addons)) {
|
||||||
|
if ( Array.isArray(addon) )
|
||||||
|
continue;
|
||||||
out.push(`${id} | ${this.isAddonEnabled(id) ? 'enabled' : 'disabled'} | ${addon.dev ? 'dev | ' : ''}${this.isAddonExternal(id) ? 'external | ' : ''}${addon.short_name} v${addon.version}`);
|
out.push(`${id} | ${this.isAddonEnabled(id) ? 'enabled' : 'disabled'} | ${addon.dev ? 'dev | ' : ''}${this.isAddonExternal(id) ? 'external | ' : ''}${addon.short_name} v${addon.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
out.push('');
|
out.push('');
|
||||||
out.push('Modules');
|
out.push('Modules');
|
||||||
for(const [key, module] of Object.entries(this.__modules)) {
|
for(const [key, module] of Object.entries((this as any).__modules as Record<string, GenericModule>)) {
|
||||||
if ( module )
|
if ( module )
|
||||||
out.push(`${module.loaded ? 'loaded ' : module.loading ? 'loading ' : 'unloaded'} | ${module.enabled ? 'enabled ' : module.enabling ? 'enabling' : 'disabled'} | ${key}`)
|
out.push(`${module.loaded ? 'loaded ' : module.loading ? 'loading ' : 'unloaded'} | ${module.enabled ? 'enabled ' : module.enabling ? 'enabling' : 'disabled'} | ${key}`)
|
||||||
}
|
}
|
||||||
|
@ -131,22 +185,20 @@ export default class AddonManager extends Module {
|
||||||
return out.join('\n');
|
return out.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
onProviderChange(key, value) {
|
onProviderChange(key: string, value: unknown) {
|
||||||
if ( key != 'addons.enabled' )
|
if ( key != 'addons.enabled' )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( ! value )
|
const val: string[] = Array.isArray(value) ? value : [],
|
||||||
value = [];
|
old_enabled = [...this.enabled_addons];
|
||||||
|
|
||||||
const old_enabled = [...this.enabled_addons];
|
|
||||||
|
|
||||||
// Add-ons to disable
|
// Add-ons to disable
|
||||||
for(const id of old_enabled)
|
for(const id of old_enabled)
|
||||||
if ( ! value.includes(id) )
|
if ( ! val.includes(id) )
|
||||||
this.disableAddon(id, false);
|
this.disableAddon(id, false);
|
||||||
|
|
||||||
// Add-ons to enable
|
// Add-ons to enable
|
||||||
for(const id of value)
|
for(const id of val)
|
||||||
if ( ! old_enabled.includes(id) )
|
if ( ! old_enabled.includes(id) )
|
||||||
this.enableAddon(id, false);
|
this.enableAddon(id, false);
|
||||||
}
|
}
|
||||||
|
@ -187,7 +239,7 @@ export default class AddonManager extends Module {
|
||||||
this.emit(':data-loaded');
|
this.emit(':data-loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
addAddon(addon, is_dev = false) {
|
addAddon(addon: AddonInfo, is_dev: boolean = false) {
|
||||||
const old = this.addons[addon.id];
|
const old = this.addons[addon.id];
|
||||||
this.addons[addon.id] = addon;
|
this.addons[addon.id] = addon;
|
||||||
|
|
||||||
|
@ -227,7 +279,7 @@ export default class AddonManager extends Module {
|
||||||
getFFZ: () => this
|
getFFZ: () => this
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emit(':added');
|
this.emit(':added', addon.id, addon);
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildAddonSearch() {
|
rebuildAddonSearch() {
|
||||||
|
@ -258,39 +310,39 @@ export default class AddonManager extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isAddonEnabled(id) {
|
isAddonEnabled(id: string) {
|
||||||
if ( this.isAddonExternal(id) )
|
if ( this.isAddonExternal(id) )
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return this.enabled_addons.includes(id);
|
return this.enabled_addons.includes(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddon(id) {
|
getAddon(id: string) {
|
||||||
const addon = this.addons[id];
|
const addon = this.addons[id];
|
||||||
return Array.isArray(addon) ? null : addon;
|
return Array.isArray(addon) ? null : addon;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAddon(id) {
|
hasAddon(id: string) {
|
||||||
return this.getAddon(id) != null;
|
return this.getAddon(id) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion(id) {
|
getVersion(id: string) {
|
||||||
const addon = this.getAddon(id);
|
const addon = this.getAddon(id);
|
||||||
if ( ! addon )
|
if ( ! addon )
|
||||||
throw new Error(`Unknown add-on id: ${id}`);
|
throw new Error(`Unknown add-on id: ${id}`);
|
||||||
|
|
||||||
const module = this.resolve(`addon.${id}`);
|
const module = this.resolve(`addon.${id}`);
|
||||||
if ( module ) {
|
if ( module ) {
|
||||||
if ( has(module, 'version') )
|
if ( 'version' in module ) // has(module, 'version') )
|
||||||
return module.version;
|
return module.version;
|
||||||
else if ( module.constructor && has(module.constructor, 'version') )
|
else if ( module.constructor && 'version' in module.constructor ) // has(module.constructor, 'version') )
|
||||||
return module.constructor.version;
|
return module.constructor.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addon.version;
|
return addon.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
isAddonExternal(id) {
|
isAddonExternal(id: string) {
|
||||||
if ( ! this.hasAddon(id) )
|
if ( ! this.hasAddon(id) )
|
||||||
throw new Error(`Unknown add-on id: ${id}`);
|
throw new Error(`Unknown add-on id: ${id}`);
|
||||||
|
|
||||||
|
@ -306,10 +358,10 @@ export default class AddonManager extends Module {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Finally, let the module flag itself as external.
|
// Finally, let the module flag itself as external.
|
||||||
return module.external || (module.constructor && module.constructor.external);
|
return (module as any).external || (module.constructor as any)?.external;
|
||||||
}
|
}
|
||||||
|
|
||||||
canReloadAddon(id) {
|
canReloadAddon(id: string) {
|
||||||
// Obviously we can't reload it if we don't have it.
|
// Obviously we can't reload it if we don't have it.
|
||||||
if ( ! this.hasAddon(id) )
|
if ( ! this.hasAddon(id) )
|
||||||
throw new Error(`Unknown add-on id: ${id}`);
|
throw new Error(`Unknown add-on id: ${id}`);
|
||||||
|
@ -334,8 +386,8 @@ export default class AddonManager extends Module {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fullyUnloadModule(module) {
|
async fullyUnloadModule(module: GenericModule) {
|
||||||
if ( ! module )
|
if ( ! module || ! module.addon_id )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( module.children )
|
if ( module.children )
|
||||||
|
@ -346,47 +398,47 @@ export default class AddonManager extends Module {
|
||||||
await module.unload();
|
await module.unload();
|
||||||
|
|
||||||
// Clean up parent references.
|
// Clean up parent references.
|
||||||
if ( module.parent && module.parent.children[module.name] === module )
|
if ( module.parent instanceof Module && module.parent.children[module.name] === module )
|
||||||
delete module.parent.children[module.name];
|
delete module.parent.children[module.name];
|
||||||
|
|
||||||
// Clean up all individual references.
|
// Clean up all individual references.
|
||||||
for(const entry of module.references) {
|
for(const entry of module.references) {
|
||||||
const other = this.resolve(entry[0]),
|
const other = this.resolve(entry[0]),
|
||||||
name = entry[1];
|
name = entry[1];
|
||||||
if ( other && other[name] === module )
|
if ( (other as any)[name] === module )
|
||||||
other[name] = null;
|
(other as any)[name] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send off a signal for other modules to unload related data.
|
// Send off a signal for other modules to unload related data.
|
||||||
this.emit('addon:fully-unload', module.addon_id);
|
this.emit(':fully-unload', module.addon_id);
|
||||||
|
|
||||||
// Clean up the global reference.
|
// Clean up the global reference.
|
||||||
if ( this.__modules[module.__path] === module )
|
if ( (this as any).__modules[(module as any).__path] === module )
|
||||||
delete this.__modules[module.__path]; /* = [
|
delete (this as any).__modules[(module as any).__path]; /* = [
|
||||||
module.dependents,
|
module.dependents,
|
||||||
module.load_dependents,
|
module.load_dependents,
|
||||||
module.references
|
module.references
|
||||||
];*/
|
];*/
|
||||||
|
|
||||||
// Remove any events we didn't unregister.
|
// Remove any events we didn't unregister.
|
||||||
this.offContext(null, module);
|
this.off(undefined, undefined, module);
|
||||||
|
|
||||||
// Do the same for settings.
|
// Do the same for settings.
|
||||||
for(const ctx of this.settings.__contexts)
|
for(const ctx of this.settings.__contexts)
|
||||||
ctx.offContext(null, module);
|
ctx.off(undefined, undefined, module);
|
||||||
|
|
||||||
// Clean up all settings.
|
// Clean up all settings.
|
||||||
for(const [key, def] of Array.from(this.settings.definitions.entries())) {
|
for(const [key, def] of Array.from(this.settings.definitions.entries())) {
|
||||||
if ( def && def.__source === module.addon_id ) {
|
if ( ! Array.isArray(def) && def?.__source === module.addon_id ) {
|
||||||
this.settings.remove(key);
|
this.settings.remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the logger too.
|
// Clean up the logger too.
|
||||||
module.__log = null;
|
(module as any).__log = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadAddon(id) {
|
async reloadAddon(id: string) {
|
||||||
const addon = this.getAddon(id),
|
const addon = this.getAddon(id),
|
||||||
button = this.resolve('site.menu_button');
|
button = this.resolve('site.menu_button');
|
||||||
if ( ! addon )
|
if ( ! addon )
|
||||||
|
@ -456,7 +508,7 @@ export default class AddonManager extends Module {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _enableAddon(id) {
|
private async _enableAddon(id: string) {
|
||||||
const addon = this.getAddon(id);
|
const addon = this.getAddon(id);
|
||||||
if ( ! addon )
|
if ( ! addon )
|
||||||
throw new Error(`Unknown add-on id: ${id}`);
|
throw new Error(`Unknown add-on id: ${id}`);
|
||||||
|
@ -476,7 +528,7 @@ export default class AddonManager extends Module {
|
||||||
this.load_tracker.notify(event, `addon.${id}`, false);
|
this.load_tracker.notify(event, `addon.${id}`, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAddon(id) {
|
async loadAddon(id: string) {
|
||||||
const addon = this.getAddon(id);
|
const addon = this.getAddon(id);
|
||||||
if ( ! addon )
|
if ( ! addon )
|
||||||
throw new Error(`Unknown add-on id: ${id}`);
|
throw new Error(`Unknown add-on id: ${id}`);
|
||||||
|
@ -500,7 +552,7 @@ export default class AddonManager extends Module {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Error if this takes more than 5 seconds.
|
// Error if this takes more than 5 seconds.
|
||||||
await timeout(this.waitFor(`addon.${id}:registered`), 60000);
|
await timeout(this.waitFor(`addon.${id}:registered` as any), 60000);
|
||||||
|
|
||||||
module = this.resolve(`addon.${id}`);
|
module = this.resolve(`addon.${id}`);
|
||||||
if ( module && ! module.loaded )
|
if ( module && ! module.loaded )
|
||||||
|
@ -509,13 +561,13 @@ export default class AddonManager extends Module {
|
||||||
this.emit(':addon-loaded', id);
|
this.emit(':addon-loaded', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
unloadAddon(id) {
|
unloadAddon(id: string) {
|
||||||
const module = this.resolve(`addon.${id}`);
|
const module = this.resolve(`addon.${id}`);
|
||||||
if ( module )
|
if ( module )
|
||||||
return module.unload();
|
return module.unload();
|
||||||
}
|
}
|
||||||
|
|
||||||
enableAddon(id, save = true) {
|
enableAddon(id: string, save: boolean = true) {
|
||||||
const addon = this.getAddon(id);
|
const addon = this.getAddon(id);
|
||||||
if( ! addon )
|
if( ! addon )
|
||||||
throw new Error(`Unknown add-on id: ${id}`);
|
throw new Error(`Unknown add-on id: ${id}`);
|
||||||
|
@ -546,7 +598,7 @@ export default class AddonManager extends Module {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableAddon(id, save = true) {
|
async disableAddon(id: string, save: boolean = true) {
|
||||||
const addon = this.getAddon(id);
|
const addon = this.getAddon(id);
|
||||||
if ( ! addon )
|
if ( ! addon )
|
||||||
throw new Error(`Unknown add-on id: ${id}`);
|
throw new Error(`Unknown add-on id: ${id}`);
|
|
@ -18,6 +18,9 @@ declare module 'utilities/types' {
|
||||||
interface ModuleMap {
|
interface ModuleMap {
|
||||||
experiments: ExperimentManager;
|
experiments: ExperimentManager;
|
||||||
}
|
}
|
||||||
|
interface ModuleEventMap {
|
||||||
|
experiments: ExperimentEvents;
|
||||||
|
}
|
||||||
interface ProviderTypeMap {
|
interface ProviderTypeMap {
|
||||||
'experiment-overrides': Record<string, unknown>
|
'experiment-overrides': Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,57 @@
|
||||||
// PubSub Client
|
// PubSub Client
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import { PUBSUB_CLUSTERS } from 'utilities/constants';
|
import { PUBSUB_CLUSTERS } from 'utilities/constants';
|
||||||
|
import type ExperimentManager from '../experiments';
|
||||||
|
import type SettingsManager from '../settings';
|
||||||
|
import type PubSubClient from 'utilities/pubsub';
|
||||||
|
import type { PubSubCommands } from 'utilities/types';
|
||||||
|
|
||||||
|
declare module 'utilities/types' {
|
||||||
|
interface ModuleMap {
|
||||||
|
pubsub: PubSub;
|
||||||
|
}
|
||||||
|
interface ModuleEventMap {
|
||||||
|
pubsub: PubSubEvents;
|
||||||
|
}
|
||||||
|
interface SettingsTypeMap {
|
||||||
|
'pubsub.use-cluster': keyof typeof PUBSUB_CLUSTERS | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default class PubSub extends Module {
|
type PubSubCommandData<K extends keyof PubSubCommands> = {
|
||||||
constructor(...args) {
|
topic: string;
|
||||||
super(...args);
|
cmd: K;
|
||||||
|
data: PubSubCommands[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
type PubSubCommandKey = `:command:${keyof PubSubCommands}`;
|
||||||
|
|
||||||
|
type PubSubEvents = {
|
||||||
|
':sub-change': [];
|
||||||
|
':message': [topic: string, data: unknown];
|
||||||
|
} & {
|
||||||
|
[K in keyof PubSubCommands as `:command:${K}`]: [data: PubSubCommands[K], meta: PubSubCommandData<K>];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default class PubSub extends Module<'pubsub', PubSubEvents> {
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
experiments: ExperimentManager = null as any;
|
||||||
|
settings: SettingsManager = null as any;
|
||||||
|
|
||||||
|
// State
|
||||||
|
_topics: Map<string, Set<unknown>>;
|
||||||
|
_client: PubSubClient | null;
|
||||||
|
|
||||||
|
_mqtt?: typeof PubSubClient | null;
|
||||||
|
_mqtt_loader?: Promise<typeof PubSubClient> | null;
|
||||||
|
|
||||||
|
constructor(name?: string, parent?: GenericModule) {
|
||||||
|
super(name, parent);
|
||||||
|
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
this.inject('experiments');
|
this.inject('experiments');
|
||||||
|
@ -161,18 +205,18 @@ export default class PubSub extends Module {
|
||||||
|
|
||||||
client.on('message', event => {
|
client.on('message', event => {
|
||||||
const topic = event.topic,
|
const topic = event.topic,
|
||||||
data = event.data;
|
data = event.data as PubSubCommandData<any>;
|
||||||
|
|
||||||
if ( ! data?.cmd ) {
|
if ( ! data?.cmd ) {
|
||||||
this.log.debug(`Received message on topic "${topic}":`, data);
|
this.log.debug(`Received message on topic "${topic}":`, data);
|
||||||
this.emit(`pubsub:message`, topic, data);
|
this.emit(`:message`, topic, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.topic = topic;
|
data.topic = topic;
|
||||||
|
|
||||||
this.log.debug(`Received command on topic "${topic}" for command "${data.cmd}":`, data.data);
|
this.log.debug(`Received command on topic "${topic}" for command "${data.cmd}":`, data.data);
|
||||||
this.emit(`pubsub:command:${data.cmd}`, data.data, data);
|
this.emit(`:command:${data.cmd}` as PubSubCommandKey, data.data, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to topics.
|
// Subscribe to topics.
|
||||||
|
@ -196,20 +240,23 @@ export default class PubSub extends Module {
|
||||||
// Topics
|
// Topics
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
subscribe(referrer, ...topics) {
|
subscribe(referrer: unknown, ...topics: string[]) {
|
||||||
const t = this._topics;
|
const topic_map = this._topics;
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for(const topic of topics) {
|
for(const topic of topics) {
|
||||||
if ( ! t.has(topic) ) {
|
let refs = topic_map.get(topic);
|
||||||
|
if ( refs )
|
||||||
|
refs.add(referrer);
|
||||||
|
else {
|
||||||
if ( this._client )
|
if ( this._client )
|
||||||
this._client.subscribe(topic);
|
this._client.subscribe(topic);
|
||||||
|
|
||||||
t.set(topic, new Set);
|
refs = new Set;
|
||||||
|
refs.add(referrer);
|
||||||
|
|
||||||
|
topic_map.set(topic, refs);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tp = t.get(topic);
|
|
||||||
tp.add(referrer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( changed )
|
if ( changed )
|
||||||
|
@ -217,19 +264,19 @@ export default class PubSub extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
unsubscribe(referrer, ...topics) {
|
unsubscribe(referrer: unknown, ...topics: string[]) {
|
||||||
const t = this._topics;
|
const topic_map = this._topics;
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for(const topic of topics) {
|
for(const topic of topics) {
|
||||||
if ( ! t.has(topic) )
|
const refs = topic_map.get(topic);
|
||||||
|
if ( ! refs )
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const tp = t.get(topic);
|
refs.delete(referrer);
|
||||||
tp.delete(referrer);
|
|
||||||
|
|
||||||
if ( ! tp.size ) {
|
if ( ! refs.size ) {
|
||||||
changed = true;
|
changed = true;
|
||||||
t.delete(topic);
|
topic_map.delete(topic);
|
||||||
if ( this._client )
|
if ( this._client )
|
||||||
this._client.unsubscribe(topic);
|
this._client.unsubscribe(topic);
|
||||||
}
|
}
|
|
@ -30,4 +30,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,12 +4,38 @@
|
||||||
// Staging Selector
|
// Staging Selector
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module, { GenericModule } from 'utilities/module';
|
||||||
import { API_SERVER, SERVER, STAGING_API, STAGING_CDN } from './utilities/constants';
|
import { API_SERVER, SERVER, STAGING_API, STAGING_CDN } from './utilities/constants';
|
||||||
|
import type SettingsManager from './settings';
|
||||||
|
|
||||||
export default class StagingSelector extends Module {
|
declare module 'utilities/types' {
|
||||||
constructor(...args) {
|
interface ModuleMap {
|
||||||
super(...args);
|
staging: StagingSelector;
|
||||||
|
}
|
||||||
|
interface ModuleEventMap {
|
||||||
|
staging: StagingEvents;
|
||||||
|
}
|
||||||
|
interface SettingsTypeMap {
|
||||||
|
'data.use-staging': boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StagingEvents = {
|
||||||
|
':updated': [api: string, cdn: string];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class StagingSelector extends Module<'staging', StagingEvents> {
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
settings: SettingsManager = null as any;
|
||||||
|
|
||||||
|
// State
|
||||||
|
api: string = API_SERVER;
|
||||||
|
cdn: string = SERVER;
|
||||||
|
active: boolean = false;
|
||||||
|
|
||||||
|
constructor(name?: string, parent?: GenericModule) {
|
||||||
|
super(name, parent);
|
||||||
|
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
|
|
||||||
|
@ -26,11 +52,12 @@ export default class StagingSelector extends Module {
|
||||||
this.updateStaging(false);
|
this.updateStaging(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.settings.getChanges('data.use-staging', this.updateStaging, this);
|
this.settings.getChanges('data.use-staging', this.updateStaging, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStaging(val) {
|
private updateStaging(val: boolean) {
|
||||||
this.active = val;
|
this.active = val;
|
||||||
|
|
||||||
this.api = val
|
this.api = val
|
||||||
|
@ -43,4 +70,4 @@ export default class StagingSelector extends Module {
|
||||||
|
|
||||||
this.emit(':updated', this.api, this.cdn);
|
this.emit(':updated', this.api, this.cdn);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -225,6 +225,10 @@ export interface ProviderTypeMap {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface PubSubCommands {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: Move this event into addons.
|
// TODO: Move this event into addons.
|
||||||
|
@ -247,15 +251,10 @@ export interface ModuleMap {
|
||||||
'i18n': TranslationManager;
|
'i18n': TranslationManager;
|
||||||
'link_card': LinkCard;
|
'link_card': LinkCard;
|
||||||
'main_menu': MainMenu;
|
'main_menu': MainMenu;
|
||||||
'pubsub': PubSub;
|
|
||||||
'site.apollo': Apollo;
|
'site.apollo': Apollo;
|
||||||
'site.css_tweaks': CSSTweaks;
|
'site.css_tweaks': CSSTweaks;
|
||||||
'site.elemental': Elemental;
|
|
||||||
'site.fine': Fine;
|
|
||||||
'site.twitch_data': TwitchData;
|
|
||||||
'site.web_munch': WebMunch;
|
'site.web_munch': WebMunch;
|
||||||
'socket': SocketClient;
|
'socket': SocketClient;
|
||||||
'staging': StagingSelector;
|
|
||||||
'translation_ui': TranslationUI;
|
'translation_ui': TranslationUI;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue