UNPKG

@v4fire/core

Version:
728 lines (567 loc) 14.3 kB
/*! * V4Fire Core * https://github.com/V4Fire/Core * * Released under the MIT license * https://github.com/V4Fire/Core/blob/master/LICENSE */ /** * [[include:core/async/modules/base/README.md]] * @packageDocumentation */ import { deprecate, deprecated } from 'core/functools'; import { namespaces, NamespacesDictionary } from 'core/async/const'; import { isZombieGroup, isPromisifyNamespace } from 'core/async/modules/base/const'; import type { ClearOptions, ClearProxyOptions, LocalCache, GlobalCache, TaskCtx, FullAsyncOptions, FullClearOptions } from 'core/async/interface'; export * from 'core/async/modules/base/const'; export * from 'core/async/modules/base/helpers'; export * from 'core/async/interface'; export default class Async<CTX extends object = Async<any>> { /** * Map of namespaces for async operations */ static namespaces: NamespacesDictionary = namespaces; /** * @deprecated * @see Async.namespaces */ static linkNames: NamespacesDictionary = namespaces; /** * The lock status. * If true, then all new tasks won't be registered. */ locked: boolean = false; /** * Cache for async operations */ protected readonly cache: Dictionary<GlobalCache> = Object.createDict(); /** * Cache for initialized workers */ protected readonly workerCache: WeakMap<object, boolean> = new WeakMap(); /** * Map for task identifiers */ protected readonly idsMap: WeakMap<object, object> = new WeakMap(); /** * Context of applying for async handlers */ protected readonly ctx: CTX; /** * @deprecated * @see [[Async.ctx]] */ protected readonly context: CTX; /** * Set of used async namespaces */ protected readonly usedNamespaces: Set<string> = new Set(); /** * Link to `Async.namespaces` */ protected get namespaces(): NamespacesDictionary { const constr = <typeof Async>this.constructor; if (constr.namespaces !== constr.linkNames) { return constr.linkNames; } return constr.namespaces; } /** * @deprecated * @see [[Async.namespaces]] */ protected get linkNames(): NamespacesDictionary { deprecate({name: 'linkNames', type: 'accessor', renamedTo: 'namespaces'}); return this.namespaces; } /** * @param [ctx] - context of applying for async handlers */ constructor(ctx?: CTX) { this.ctx = ctx ?? Object.cast(this); this.context = this.ctx; } /** * Clears all async tasks * @param [opts] - additional options for the operation */ clearAll(opts?: ClearOptions): this { for (let o = this.usedNamespaces.values(), el = o.next(); !el.done; el = o.next()) { const key = el.value, alias = `clear-${this.namespaces[key]}`.camelize(false); if (Object.isFunction(this[alias])) { this[alias](opts); } else if (!isPromisifyNamespace.test(key)) { throw new ReferenceError(`The method "${alias}" is not defined`); } } return this; } /** * Mutes all async tasks * @param [opts] - additional options for the operation */ muteAll(opts?: ClearOptions): this { for (let o = this.usedNamespaces.values(), el = o.next(); !el.done; el = o.next()) { const key = el.value, alias = `mute-${this.namespaces[key]}`.camelize(false); if (!isPromisifyNamespace.test(key) && Object.isFunction(this[alias])) { this[alias](opts); } } return this; } /** * Unmutes all async tasks * @param [opts] - additional options for the operation */ unmuteAll(opts?: ClearOptions): this { for (let o = this.usedNamespaces.values(), el = o.next(); !el.done; el = o.next()) { const key = el.value, alias = `unmute-${this.namespaces[key]}`.camelize(false); if (!isPromisifyNamespace.test(key) && Object.isFunction(this[alias])) { this[alias](opts); } } return this; } /** * Suspends all async tasks * @param [opts] - additional options for the operation */ suspendAll(opts?: ClearOptions): this { for (let o = this.usedNamespaces.values(), el = o.next(); !el.done; el = o.next()) { const key = el.value, alias = `suspend-${this.namespaces[key]}`.camelize(false); if (!isPromisifyNamespace.test(key) && Object.isFunction(this[alias])) { this[alias](opts); } } return this; } /** * Unsuspends all async tasks * @param [opts] - additional options for the operation */ unsuspendAll(opts?: ClearOptions): this { for (let o = this.usedNamespaces.values(), el = o.next(); !el.done; el = o.next()) { const key = el.value, alias = `unsuspend-${this.namespaces[key]}`.camelize(false); if (!isPromisifyNamespace.test(key) && Object.isFunction(this[alias])) { this[alias](opts); } } return this; } /** * Returns a cache object by the specified name * * @param name * @param [promise] - if true, the namespace is marked as promisified */ protected getCache(name: string, promise?: boolean): GlobalCache { name = promise ? `${name}Promise` : name; return this.cache[name] = this.cache[name] ?? { root: { labels: Object.createDict(), links: new Map() }, groups: Object.createDict() }; } /** * @deprecated * @see [[Async.getCache]] */ @deprecated({renamedTo: 'getCache'}) protected initCache(name: string, promise?: boolean): GlobalCache { return this.getCache(name, promise); } /** * Registers the specified async task * @param task */ protected registerTask<R = unknown>(task: FullAsyncOptions<any>): R | null { if (this.locked) { return null; } this.usedNamespaces.add(task.name); const {ctx} = this; const baseCache = this.getCache(task.name, task.promise), callable = task.callable ?? task.needCall; let cache: LocalCache; if (task.group != null) { cache = baseCache.groups[task.group] ?? { labels: Object.createDict(), links: new Map() }; baseCache.groups[task.group] = cache; } else { cache = baseCache.root; } const {label} = task, {labels, links} = cache, {links: baseLinks} = baseCache.root; const labelCache = label != null ? labels[label] : null; if (labelCache != null && task.join === true) { const mergeHandlers = Array.concat([], task.onMerge), link = links.get(labelCache); for (let i = 0; i < mergeHandlers.length; i++) { mergeHandlers[i].call(ctx, link); } return labelCache; } const normalizedObj = callable && Object.isFunction(task.obj) ? task.obj.call(ctx) : task.obj; let wrappedObj = normalizedObj, taskId = normalizedObj; if (!task.periodic || Object.isFunction(wrappedObj)) { wrappedObj = (...args) => { const link = links.get(taskId); if (link?.muted === true) { const mutedCallHandlers = Array.concat([], task.onMutedCall); for (let i = 0; i < mutedCallHandlers.length; i++) { mutedCallHandlers[i].call(ctx, link); } } if (!link || link.muted) { return; } if (!task.periodic) { if (link.paused) { link.muted = true; } else { link.unregister(); } } const invokeHandlers = (i = 0) => (...args) => { const fns = link.onComplete; if (Object.isArray(fns)) { for (let j = 0; j < fns.length; j++) { const fn = fns[j]; if (Object.isFunction(fn)) { fn.apply(ctx, args); } else { fn[i].apply(ctx, args); } } } }; const needDelete = !task.periodic && link.paused; const exec = () => { if (needDelete) { link.unregister(); } let res = normalizedObj; if (Object.isFunction(normalizedObj)) { res = normalizedObj.apply(ctx, args); } if (Object.isPromiseLike(res)) { res.then(invokeHandlers(), invokeHandlers(1)); } else { invokeHandlers()(...args); } return res; }; if (link.paused) { link.queue.push(exec); return; } return exec(); }; } if (task.wrapper) { const link = task.wrapper.apply(null, [wrappedObj].concat(callable ? taskId : [], task.args)); if (task.linkByWrapper) { taskId = link; } } const link = { id: taskId, obj: task.obj, objName: task.obj.name, group: task.group, label: task.label, paused: false, muted: false, queue: [], clearFn: task.clearFn, onComplete: [], onClear: Array.concat([], task.onClear), unregister: () => { links.delete(taskId); baseCache.root.links.delete(taskId); if (label != null && labels[label] != null) { labels[label] = undefined; } } }; if (labelCache != null) { this.cancelTask({...task, replacedBy: link, reason: 'collision'}); } links.set(taskId, link); if (links !== baseLinks) { baseLinks.set(taskId, link); } if (label != null) { labels[label] = taskId; } return taskId; } /** * @deprecated * @see [[Async.registerTask]] */ @deprecated({renamedTo: 'registerTask'}) protected setAsync<R = unknown>(task: FullAsyncOptions): R | null { return this.registerTask(task); } /** * Cancels a task (or a group of tasks) from the specified namespace * * @param task - operation options or task link * @param [name] - namespace of the operation */ protected cancelTask(task: CanUndef<FullClearOptions | any>, name?: string): this { task = task != null ? this.idsMap.get(task) ?? task : task; let p: FullClearOptions; if (name != null) { if (task === undefined) { return this.cancelTask({name, reason: 'all'}); } p = Object.isPlainObject(task) ? {...task, name} : {name, id: task}; } else { p = task ?? {}; } const baseCache = this.getCache(p.name, p.promise); let cache: LocalCache; if (p.group != null) { if (Object.isRegExp(p.group)) { const obj = baseCache.groups, keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { const group = keys[i]; if (p.group.test(group)) { this.cancelTask({...p, group, reason: 'rgxp'}); } } return this; } const group = baseCache.groups[p.group]; if (group == null) { return this; } cache = group; if (p.reason == null) { p.reason = 'group'; } } else { cache = baseCache.root; } const {labels, links} = cache; if (p.label != null) { const tmp = labels[p.label]; if (p.id != null && p.id !== tmp) { return this; } p.id = tmp; if (p.reason == null) { p.reason = 'label'; } } if (p.reason == null) { p.reason = 'id'; } if (p.id != null) { const link = links.get(p.id); if (link != null) { const skipZombie = link.group != null && p.reason === 'all' && isZombieGroup.test(link.group); if (skipZombie) { return this; } link.unregister(); const ctx = <TaskCtx>{ ...p, link, type: 'clearAsync' }; const clearHandlers = link.onClear, {clearFn} = link; for (let i = 0; i < clearHandlers.length; i++) { clearHandlers[i].call(this.ctx, ctx); } if (clearFn != null && !p.preventDefault) { clearFn(link.id, ctx); } } } else { for (let o = links.values(), el = o.next(); !el.done; el = o.next()) { this.cancelTask({...p, id: el.value.id}); } } return this; } /** * @deprecated * @see [[Async.cancelTask]] */ @deprecated({renamedTo: 'cancelTask'}) protected clearAsync(opts: CanUndef<FullClearOptions | any>, name?: string): this { return this.cancelTask(opts, name); } /** * Marks a task (or a group of tasks) from the namespace by the specified label * * @param label * @param task - operation options or a link to the task * @param [name] - namespace of the operation */ protected markTask(label: string, task: CanUndef<ClearProxyOptions | any>, name?: string): this { task = task != null ? this.idsMap.get(task) ?? task : task; let p: FullClearOptions; if (name != null) { if (task === undefined) { return this.markTask(label, {name, reason: 'all'}); } p = Object.isPlainObject(task) ? {...task, name} : {name, id: task}; } else { p = task ?? {}; } const baseCache = this.getCache(p.name); let cache: LocalCache; if (p.group != null) { if (Object.isRegExp(p.group)) { const obj = baseCache.groups, keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { const group = keys[i]; if (p.group.test(group)) { this.markTask(label, {...p, group, reason: 'rgxp'}); } } return this; } const groupCache = baseCache.groups[p.group]; if (groupCache == null) { return this; } cache = groupCache; } else { cache = baseCache.root; } const {labels, links} = cache; if (p.label != null) { const tmp = labels[p.label]; if (p.id != null && p.id !== tmp) { return this; } p.id = tmp; if (p.reason == null) { p.reason = 'label'; } } if (p.reason == null) { p.reason = 'id'; } if (p.id != null) { const link = links.get(p.id); if (link) { const skipZombie = link.group != null && p.reason === 'all' && isZombieGroup.test(link.group); if (skipZombie) { return this; } if (label === '!paused') { for (let o = link.queue, i = 0; i < o.length; i++) { o[i](); } link.muted = false; link.paused = false; link.queue = []; } else if (label.startsWith('!')) { link[label.slice(1)] = false; } else { link[label] = true; } } } else { const values = links.values(); for (let el = values.next(); !el.done; el = values.next()) { this.markTask(label, {...p, id: el.value.id}); } } return this; } /** * @deprecated * @see [[Async.markTask]] */ @deprecated({renamedTo: 'markTask'}) protected markAsync(label: string, opts: CanUndef<ClearProxyOptions | any>, name?: string): this { return this.markTask(label, opts, name); } /** * Marks all async tasks from the namespace by the specified label * * @param label * @param opts - operation options */ protected markAllTasks(label: string, opts: FullClearOptions): this { this.markTask(label, opts); return this; } }