@v4fire/client
Version:
V4Fire client core library
309 lines (253 loc) • 6.14 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
/**
* [[include:super/i-block/modules/daemons/README.md]]
* @packageDocumentation
*/
import type iBlock from 'super/i-block/i-block';
import Friend from 'super/i-block/modules/friend';
import { wait } from 'super/i-block/modules/decorators';
import type {
Daemon,
SpawnedDaemon,
DaemonsDict,
DaemonWatcher,
DaemonHook,
DaemonHookOptions
} from 'super/i-block/modules/daemons/interface';
export * from 'super/i-block/modules/daemons/interface';
/**
* Class to manage component daemons
*/
export default class Daemons extends Friend {
//#if runtime has component/daemons
/**
* Creates a new daemon dictionary with extending from the specified parent and returns it
*
* @param base
* @param [parent]
*/
static createDaemons<CTX extends iBlock = iBlock>(
base: DaemonsDict,
parent?: DaemonsDict
): DaemonsDict<CTX['unsafe']> {
const
mixedDaemons = {...parent, ...base};
if (parent) {
for (let keys = Object.keys(parent), i = 0; i < keys.length; i++) {
const
daemonName = keys[i],
parentDaemon = parent[daemonName],
daemon = base[daemonName];
if (daemon && parentDaemon) {
mixedDaemons[daemonName] = mergeDaemons(daemon, parentDaemon);
}
}
}
return mixedDaemons;
}
/**
* Map of component daemons
*/
protected get daemons(): DaemonsDict<this['CTX']> {
return (<typeof iBlock>this.ctx.instance.constructor).daemons;
}
constructor(component: iBlock) {
super(component);
this.init();
}
/**
* Returns true if a daemon by the specified name is exists
* @param name
*/
isExists(name: string): boolean {
return Boolean(this.daemons[name]);
}
/**
* Spawns a new daemon
*
* @param name
* @param spawned
*/
spawn(name: string, spawned: SpawnedDaemon<this['CTX']>): boolean {
const
exists = this.isExists(name);
if (exists) {
return false;
}
spawned = Object.isFunction(spawned) ? {fn: spawned} : spawned;
return this.register(name, this.wrapDaemonFn(<Daemon>spawned));
}
/**
* Runs a daemon with the specified arguments
*
* @param name
* @param args
*/
run<R = unknown>(name: string, ...args: unknown[]): CanUndef<R> {
const
{ctx} = this;
const
daemon = this.get(name);
if (!daemon) {
return;
}
const
fn = daemon.wrappedFn ?? daemon.fn;
if (daemon.immediate !== true) {
const asyncOptions = {
group: `daemons:${this.ctx.componentName}`,
label: `daemons:${name}`,
...daemon.asyncOptions
};
if (asyncOptions.label == null) {
Object.delete(asyncOptions, 'label');
}
ctx.async.setImmediate(() => fn.apply(ctx, args), Object.cast(asyncOptions));
} else {
return fn.apply(ctx, args);
}
}
/**
* Returns a daemon by the specified name
* @param name
*/
protected get(name: string): CanUndef<Daemon<this['CTX']>> {
return this.daemons[name];
}
/**
* Registers a new daemon by the specified name
*
* @param name
* @param daemon
*/
protected register(name: string, daemon: Daemon): boolean {
this.daemons[name] = daemon;
return Boolean(this.daemons[name]);
}
/**
* Creates a wrapped function for the specified daemon
* @param daemon
*/
protected wrapDaemonFn<T extends Daemon>(daemon: T): T {
daemon.wrappedFn = daemon.wait != null ? wait(daemon.wait, daemon.fn) : daemon.fn;
return daemon;
}
/**
* Binds the specified daemon to a component lifecycle
*
* @param hook
* @param name
* @param [opts] - additional options
*/
protected bindToHook(hook: string, name: string, opts?: DaemonHookOptions): void {
const
{hooks} = this.ctx.meta;
hooks[hook].push({
fn: () => this.run(name),
...opts
});
}
/**
* Binds the specified daemon to component watchers
*
* @param watch
* @param name
*/
protected bindToWatch(watch: DaemonWatcher, name: string): void {
const
{watchers} = this.ctx.meta;
const
watchName = Object.isSimpleObject(watch) ? watch.field : watch,
watchParams = Object.isPlainObject(watch) ? Object.reject(watch, 'field') : {};
const watchDaemon = {
handler: (...args) => this.run(name, ...args),
method: name,
args: [],
...watchParams
};
const
w = watchers[watchName];
if (w) {
w.push(watchDaemon);
} else {
watchers[watchName] = [watchDaemon];
}
}
/**
* Initializes all static daemons
*/
protected init(): void {
const
{daemons} = this;
for (let keys = Object.keys(daemons), i = 0; i < keys.length; i++) {
const
name = keys[i],
daemon = this.get(name);
if (!daemon) {
continue;
}
this.wrapDaemonFn(daemon);
const
hooks = Object.isPlainObject(daemon.hook) ? Object.keys(daemon.hook) : daemon.hook;
if (hooks) {
for (let i = 0; i < hooks.length; i++) {
const
hook = hooks[i];
const params = {
after: Object.isPlainObject(daemon.hook) ? new Set<string>(...[].concat(daemon.hook[hook])) : undefined
};
this.bindToHook(hook, name, params);
}
}
if (daemon.watch) {
for (let i = 0; i < daemon.watch.length; i++) {
this.bindToWatch(daemon.watch[i], name);
}
}
}
}
//#endif
}
/**
* Merge the two specified daemons to a new object and returns it
*
* @param base
* @param parent
*/
export function mergeDaemons(base: Daemon, parent: Daemon): Daemon {
const
hook = mergeHooks(base, parent),
watch = (parent.watch ?? []).union(base.watch ?? []);
return {
...parent,
...base,
hook,
watch
};
}
/**
* Merge hooks of two specified daemons to a new object and returns it
*
* @param base
* @param parent
*/
export function mergeHooks(base: Daemon, parent: Daemon): CanUndef<DaemonHook> {
const
{hook: aHooks} = base,
{hook: bHooks} = parent;
if (!aHooks && !bHooks) {
return;
}
const
convertHooksToObject = (h) => Array.isArray(h) ? h.reduce((acc, a) => (acc[a] = undefined, acc), {}) : h;
return {
...convertHooksToObject(bHooks),
...convertHooksToObject(aHooks)
};
}