UNPKG

@v4fire/core

Version:
368 lines (315 loc) 11.4 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/wrappers/README.md]] * @packageDocumentation */ import type { Provider } from 'core/data'; import type { CreateRequestOptions, RequestQuery, RequestBody } from 'core/request'; import Super, { AsyncOptions, EventEmitterLike } from 'core/async/modules/events'; import type { AsyncStorage, AsyncStorageNamespace } from 'core/kv-storage'; import { emitLikeEvents, asyncOptionsKeys, dataProviderMethodsToReplace } from 'core/async/modules/wrappers/consts'; import type { WrappedDataProvider, DataProviderMethodsToReplace, DataProviderQueryMethodsToReplace, EventEmitterWrapper, EventEmitterOverwritten, WrappedAsyncStorageNamespace, WrappedAsyncStorage, AsyncOptionsForWrappers } from 'core/async/interface'; export * from 'core/async/modules/events'; export * from 'core/async/modules/wrappers/consts'; export * from 'core/async/interface'; export default class Async<CTX extends object = Async<any>> extends Super<CTX> { /** * The wrapper takes a link to the "raw" data provider and returns a new object that based * on the original, but all async methods and properties are wrapped by Async. * Notice, the wrapped methods can take additional Async parameters, like group or label. * If you don't provide a group, it will be taken from the provider name. * * @param provider * @param [opts] - additional options for the wrapper * * @example * ```js * import Async from 'core/async'; * import Provider, { provider } from 'core/data'; * * @provider('api') * export default class User extends Provider { * baseURL = 'user/:id'; * } * * const * $a = new Async(), * wrappedProvider = $a.wrapDataProvider(new User()); * * wrappedProvider.get({uuid: 1}).then((res) => { * console.log(res); * }); * * // By default, all wrapped methods have a group name that is equal to the provider name. * // So we can use it to clear or suspend requests, etc. * $a.clearAll({group: 'api.User'}) * * wrappedProvider.upd({uuid: 1}, { * // All wrapped methods can take additional Async parameters as the second argument: `group`, `label` and `join` * group: 'bla', * label: 'foo', * join: true, * * // Also, the second argument of the wrapped method can take the original parameters from a provider * headers: { * 'X-Foo': '1' * } * * }).then((res) => { * console.log(res); * }); * * // If we are providing a group to the method, it will be joined with the global group by using the `:` character * $a.suspendAll({group: 'api.User:bla'}); * * // Obviously, we can use a group as RegExp * $a.muteAll({group: /api\.User/}); * * // We can use any methods or properties from the original data provider * wrappedProvider.dropCache(); * ``` */ wrapDataProvider< P extends Provider, W extends WrappedDataProvider >(provider: P, opts?: AsyncOptionsForWrappers):W { const wrappedProvider: W = Object.create(provider), wrappedProviderGroup = opts?.group ?? provider.providerName; for (let i = 0; i < dataProviderMethodsToReplace.length; i++) { const methodName = dataProviderMethodsToReplace[i]; wrappedProvider[methodName] = <D = unknown>( body?: RequestBody | RequestQuery, opts?: CreateRequestOptions<D> & AsyncOptions ) => { const ownParams = Object.reject(opts, asyncOptionsKeys), asyncParams = Object.select(opts, asyncOptionsKeys), group = `${wrappedProviderGroup}${asyncParams.group != null ? `:${asyncParams.group}` : ''}`; if (isQueryMethod(methodName)) { return this.request(provider[methodName](<RequestQuery>body, ownParams), { ...asyncParams, group }); } return this.request(provider[methodName](<RequestBody>body, ownParams), { ...asyncParams, group }); }; } wrappedProvider.emitter = this.wrapEventEmitter(provider.emitter, opts); return wrappedProvider; function isQueryMethod(name: DataProviderMethodsToReplace): name is DataProviderQueryMethodsToReplace { return ['get', 'peek'].includes(name); } } /** * The wrapper takes a link to the "raw" event emitter and returns a new object that based * on the original, but all async methods and properties are wrapped by Async. * Notice, the wrapped methods can take additional Async parameters, like group or label. * In addition, the wrapper adds new methods, like "on" or "off", to make the emitter API more standard. * * @param emitter * @param [opts] - additional options for the wrapper * * @example * ```js * import Async from 'core/async'; * * const * $a = new Async(), * wrappedEventEmitter = $a.wrapEventEmitter(window); * * const handler = () => console.log('scroll event'); * * // We can safely listen to emitter events, * // cause all emitter methods, like `addListener` or `on` are wrapped by Async. * const id = wrappedEventEmitter.addEventListener('scroll', handler, { * // Notice, the third argument can take Async parameters in addition to the native emitter parameters * capture: true, * label: 'label' * }); * * // The wrapper preserves the original API of emitter methods, so we can call something like this * wrappedEventEmitter.removeEventListener('scroll', handler); * * // Finally, the wrapper adds a bunch of standard methods to the emitter, like `on`, `once`, and other stuff. * // We can use their instead of the original methods to make our code more universal. * wrappedEventEmitter.once('resize', (e) => { * console.log(e); * }, {group: 'resizers'}); * * $a.muteAll({group: 'resizers'}); * * // We can use any methods or properties from the original emitter * console.log(wrappedEventEmitter.name); // window.name * ``` */ wrapEventEmitter<T extends EventEmitterLike>( emitter: T, opts?: AsyncOptionsForWrappers ): (EventEmitterOverwritten<T> & EventEmitterWrapper) { const wrappedEmitter = Object.create(emitter); wrappedEmitter.on = (event, fn, ...params) => { if (!Object.isFunction(fn)) { throw new TypeError('Wrapped emitters methods `on, addEventListener, addListener` accept only a function as the second parameter'); } return this.on(emitter, event, fn, ...normalizeAdditionalArgs(params)); }; wrappedEmitter.addEventListener = wrappedEmitter.on; wrappedEmitter.addListener = wrappedEmitter.on; wrappedEmitter.once = (event, fn, ...params) => this.once(emitter, event, fn, ...normalizeAdditionalArgs(params)); wrappedEmitter.promisifyOnce = (event, ...params) => this.promisifyOnce(emitter, event, ...normalizeAdditionalArgs(params)); const wrapOff = (originalMethod) => (link, ...args) => { if (link == null || typeof link !== 'object' || args.length > 0) { return Object.isFunction(originalMethod) ? originalMethod.call(emitter, link, ...args) : null; } return this.off(link); }; wrappedEmitter.off = wrapOff(emitter.off); wrappedEmitter.removeEventListener = wrapOff(emitter.removeEventListener); wrappedEmitter.removeListener = wrapOff(emitter.removeListener); wrappedEmitter.emit = (event, ...args) => { for (let i = 0; i < emitLikeEvents.length; i++) { const key = emitLikeEvents[i], property = emitter[key]; if (Object.isFunction(property)) { return property.call(emitter, event, ...args); } } }; return wrappedEmitter; function normalizeAdditionalArgs(params: unknown[]): unknown[] { if (Object.isPlainObject(params[0])) { const ownParam = Object.reject(params[0], asyncOptionsKeys), asyncParam = Object.select(params[0], asyncOptionsKeys); return [ { ...asyncParam, group: [opts?.group, asyncParam.group].filter(Boolean).join(':') }, ownParam, ...params.slice(1) ]; } return [opts?.group != null ? {group: opts.group} : {}, ...params]; } } /** * The wrapper takes a link to the "raw" async storage and returns a new object that based * on the original, but all async methods and properties are wrapped by Async. * Notice, the wrapped methods can take additional Async parameters, like group or label. * * @param storage * @param opts * * @example * ```js * import Async from 'core/async'; * import { asyncLocal } from 'core/kv-storage'; * * const * $a = new Async(), * wrappedStorage = $a.wrapStorage(asyncLocal, {group: 'bar'}); * * wrappedStorage.set('someKey', 'someValue', { * // If we are providing a group to the method, it will be joined with the global group by using the `:` character * group: 'bla', * label: 'foo', * join: true, * }).then(async () => { * console.log(await wrappedStorage.get('someKey') === 'someValue'); * }); * * $a.suspendAll({group: 'bar:bla'}); * * // We can provide own global group to namespace, it will be joined with the parent's global group * const blaStore = wrappedStorage.namespace('[[BLA]]', {group: 'bla'}); * * blaStore.clear({group: 'foo'}); * * $a.muteAll({group: 'bar:bla:foo'}); * ``` */ wrapStorage<T extends AsyncStorage | AsyncStorageNamespace>( storage: T, opts?: AsyncOptionsForWrappers ): T extends AsyncStorage ? WrappedAsyncStorage : WrappedAsyncStorageNamespace { const globalGroup = opts?.group, wrappedStorage = Object.create(storage); wrappedStorage.has = (key, ...args) => { const [asyncOpts, params] = separateArgs(args); return this.promise(storage.has(key, ...params), asyncOpts); }; wrappedStorage.get = <T = unknown>(key, ...args) => { const [asyncOpts, params] = separateArgs(args); return this.promise(storage.get<T>(key, ...params), asyncOpts); }; wrappedStorage.set = (key, value, ...args) => { const [asyncOpts, params] = separateArgs(args); return this.promise(storage.set(key, value, ...params), asyncOpts); }; wrappedStorage.remove = (key, ...args) => { const [asyncOpts, params] = separateArgs(args); return this.promise(storage.remove(key, ...params), asyncOpts); }; wrappedStorage.clear = <T = unknown>(filter?, ...args) => { if (Object.isPlainObject(filter)) { filter = undefined; args = [filter]; } const [asyncOpts, params] = separateArgs(args); return this.promise(storage.clear<T>(filter, ...params), asyncOpts); }; if ('namespace' in storage) { wrappedStorage.namespace = (name, opts?) => { const [asyncOpts] = separateArgs([opts]); const storageNamespace = storage.namespace(name); return this.wrapStorage(storageNamespace, asyncOpts); }; } return wrappedStorage; function separateArgs(args: unknown[]): [AsyncOptions, unknown[]] { const lastArg = args.pop(); if (!Object.isDictionary(lastArg)) { return [globalGroup != null ? {group: globalGroup} : {}, [...args, lastArg]]; } const ownParams = Object.reject(lastArg, asyncOptionsKeys), asyncParams = Object.select(lastArg, asyncOptionsKeys), group = [globalGroup, asyncParams.group].filter(Boolean).join(':'); if (group !== '') { asyncParams.group = group; } if (Object.keys(ownParams).length !== 0) { args.push(ownParams); } return [asyncParams, args]; } } }