UNPKG

@v4fire/client

Version:

V4Fire client core library

309 lines (253 loc) • 6.14 kB
/*! * 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) }; }