@v4fire/core
Version:
V4Fire core library
702 lines (556 loc) • 16.2 kB
text/typescript
/*!
* V4Fire Core
* https://github.com/V4Fire/Core
*
* Released under the MIT license
* https://github.com/V4Fire/Core/blob/master/LICENSE
*/
import { EventEmitter2 as EventEmitter } from 'eventemitter2';
import symbolGenerator from 'core/symbol';
import { readonly } from 'core/object/proxy-readonly';
import { unimplement } from 'core/functools/implementation';
import { deprecate } from 'core/functools';
import { concatURLs } from 'core/url';
import Async from 'core/async';
import IO, { Socket } from 'core/socket';
import type AbortablePromise from 'core/promise/abortable';
import {
globalOpts,
CreateRequestOptions,
MiddlewareParams,
RequestQuery,
RequestMethod,
RequestBody,
RequestPromise,
RequestResponseObject,
ResolverResult,
RequestFunctionResponse
} from 'core/request';
import type {
Provider as IProvider,
ProviderOptions,
ModelMethod
} from 'core/data/interface';
import {
namespace,
providers,
queryMethods,
requestCache,
instanceCache,
connectCache
} from 'core/data/const';
import ParamsProvider from 'core/data/modules/params';
export * from 'core/data/modules/params';
export const
$$ = symbolGenerator();
export default abstract class Provider extends ParamsProvider implements IProvider {
/**
* Cache identifier
*/
readonly cacheId!: string;
/** @inheritDoc */
readonly alias?: string;
/** @inheritDoc */
readonly emitter!: EventEmitter;
/** @inheritDoc */
readonly params!: ProviderOptions;
/** @inheritDoc */
get providerName(): string {
return this.constructor[namespace];
}
/** @inheritDoc */
get event(): EventEmitter {
deprecate({name: 'event', type: 'accessor', renamedTo: 'emitter'});
return this.emitter;
}
/**
* API for async operations
*/
protected readonly async!: Async<this>;
/**
* Socket connection
*/
protected connection?: Promise<Socket>;
/**
* @param [opts] - additional options
*/
protected constructor(opts: ProviderOptions = {}) {
super();
const
id = this.getCacheKey(opts),
cacheVal = instanceCache[id];
if (cacheVal != null) {
return <this>cacheVal;
}
instanceCache[id] = this;
requestCache[id] = Object.createDict();
this.cacheId = id;
this.params = opts;
this.async = new Async(this);
this.emitter = new EventEmitter({maxListeners: 1e3, newListener: false});
if (opts.socket || this.socketURL != null) {
this.connect().then(this.initSocketBehaviour.bind(this), stderr);
}
}
/**
* Returns a key to the class instance cache
* @param [paramsForCache]
*/
getCacheKey(paramsForCache: ProviderOptions = {}): string {
return `${this.providerName}:${Object.fastHash(paramsForCache)}`;
}
/**
* Returns an object with authentication parameters
* @param params - additional parameters
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
getAuthParams(params?: Dictionary): Promise<Dictionary> {
return Promise.resolve({});
}
/**
* Function to resolve a request: it takes a request URL and request environment
* and can modify some request parameters.
*
* Also, if the function returns a new string, the string will be appended to the request URL, or
* if the function returns a string that wrapped with an array, the string fully override the original URL.
*
* @see [[RequestResolver]]
* @param url - request URL
* @param params - request parameters
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
resolver<T = unknown>(url: string, params: MiddlewareParams<T>): ResolverResult {
return undefined;
}
/**
* Connects to a socket server and returns the connection
* @param [opts] - additional options for the server
*/
async connect(opts?: Dictionary): Promise<Socket> {
await this.async.wait(() => this.socketURL);
const
{socketURL: url} = this,
key = Object.fastHash(opts);
if (connectCache[key] == null) {
connectCache[key] = new Promise((resolve, reject) => {
const
socket = IO(url);
if (!socket) {
return;
}
function onClear(err: unknown): void {
reject(err);
delete connectCache[key];
}
const asyncParams = {
group: 'connection',
label: $$.connect,
join: true,
onClear
};
this.async.worker(socket, asyncParams);
this.async.once(socket, 'connect', () => resolve(socket), asyncParams);
});
}
return connectCache[key]!;
}
/** @inheritDoc */
name(): CanUndef<ModelMethod>;
/** @inheritDoc */
name(value: ModelMethod): Provider;
name(value?: ModelMethod): CanUndef<ModelMethod | Provider> {
if (value == null) {
return this.eventName;
}
const obj = Object.create(this);
obj.eventName = value;
return obj;
}
/** @inheritDoc */
method(): CanUndef<RequestMethod>;
/** @inheritDoc */
method(value: RequestMethod): Provider;
method(value?: RequestMethod): CanUndef<RequestMethod | Provider> {
if (value == null) {
return this.customMethod;
}
const obj = Object.create(this);
obj.customMethod = value;
return obj;
}
/** @inheritDoc */
base(): string;
/** @inheritDoc */
base(value: string): Provider;
base(value?: string): string | Provider {
if (value == null) {
return this.baseURL;
}
const
obj = Object.create(this);
obj.baseURL = value;
obj.baseGetURL = undefined;
obj.basePeekURL = undefined;
obj.baseAddURL = undefined;
obj.baseUpdURL = undefined;
obj.baseDelURL = undefined;
return obj;
}
/** @inheritDoc */
url(): string;
/** @inheritDoc */
url(value: string): Provider;
url(value?: string): string | Provider {
if (value == null) {
return concatURLs(this.baseURL, this.advURL);
}
const obj = Object.create(this);
obj.advURL = value;
return obj;
}
/** @inheritDoc */
dropCache(): void {
const
cache = requestCache[this.cacheId];
if (cache) {
for (let keys = Object.keys(cache), i = 0; i < keys.length; i++) {
const
obj = cache[keys[i]];
if (obj) {
obj.dropCache();
}
}
}
requestCache[this.cacheId] = Object.createDict();
this.emitter.emit('dropCache');
}
/** @inheritDoc */
get<D = unknown>(query?: RequestQuery, opts?: CreateRequestOptions<D>): RequestPromise<D> {
const
url = this.resolveURL(this.baseGetURL),
alias = this.alias ?? this.providerName;
const
eventName = this.name(),
method = this.method() ?? this.getMethod;
const mergedOpts = this.getRequestOptions<D>('get', {
...opts,
[queryMethods[method] != null ? 'query' : 'body']: query,
method
});
const
req = this.request(url, this.resolver.bind(this), mergedOpts),
res = eventName != null ? this.updateRequest(url, eventName, req) : this.updateRequest(url, req);
const extraProviders = Object.isFunction(this.extraProviders) ?
this.extraProviders({opts: Object.cast(mergedOpts), globalOpts}) :
this.extraProviders;
if (extraProviders) {
const
composition = <D & object>{},
tasks: Array<AbortablePromise<RequestResponseObject>> = [],
cloneTasks: Function[] = [];
for (let keys = Object.keys(extraProviders), i = 0; i < keys.length; i++) {
const
key = keys[i],
el = extraProviders[key] ?? {};
const
ProviderLink = el.provider ?? key,
alias = el.alias ?? key;
let
ProviderConstructor,
providerInstance: IProvider;
if (Object.isString(ProviderLink)) {
ProviderConstructor = <CanUndef<Dictionary & typeof Provider>>providers[ProviderLink];
if (ProviderConstructor == null) {
throw new Error(`Provider "${ProviderLink}" is not defined`);
}
providerInstance = new ProviderConstructor(el.providerOptions);
} else if (Object.isSimpleFunction(ProviderLink)) {
providerInstance = new ProviderLink(el.providerOptions);
} else {
providerInstance = ProviderLink;
}
const
req = providerInstance.get(el.query ?? query, el.request);
tasks.push(
req.then(async (res) => {
const
data = <Nullable<D & object>>(await res.data);
Object.set(composition, alias, data);
cloneTasks.push((composition) => Object.set(composition, alias, data?.valueOf()));
return res;
})
);
}
const compositionRes = res.then(
(res) => Promise.all(tasks).then(async () => {
const
data = <Nullable<D & object>>(await res.data);
Object.set(composition, alias, data);
cloneTasks.push((composition) => Object.set(composition, alias, data?.valueOf()));
Object.defineProperty(composition, 'valueOf', {
writable: true,
configurable: true,
value: () => {
const
clone = {};
for (let i = 0; i < cloneTasks.length; i++) {
cloneTasks[i](clone);
}
return clone;
}
});
// eslint-disable-next-line require-atomic-updates
res.data = Promise.resolve(readonly(composition));
return res;
}),
null,
() => {
for (let i = 0; i < tasks.length; i++) {
tasks[i].abort();
}
}
);
compositionRes['emitter'] = new EventEmitter();
void Object.defineProperty(compositionRes, 'data', {
configurable: true,
enumerable: true,
get: () => compositionRes.then((res: RequestResponseObject) => res.data)
});
const unimplementedStream = unimplement.bind(null, {
name: 'Symbol.asyncIterator',
type: 'property',
notice: "Requests with extra providers can't be streamed"
});
void Object.defineProperty(compositionRes, 'stream', {
configurable: true,
enumerable: true,
get: unimplementedStream
});
compositionRes[Symbol.asyncIterator] = unimplementedStream;
return Object.cast(compositionRes);
}
return res;
}
/** @inheritDoc */
peek<D = unknown>(query?: RequestQuery, opts?: CreateRequestOptions<D>): RequestPromise<D> {
const
url = this.resolveURL(this.basePeekURL),
eventName = this.name(),
method = this.method() ?? this.peekMethod;
const req = this.request(url, this.resolver.bind(this), this.getRequestOptions('peek', {
...opts,
[queryMethods[method] != null ? 'query' : 'body']: query,
method
}));
if (eventName != null) {
return this.updateRequest(url, eventName, req);
}
return this.updateRequest(url, req);
}
/** @inheritDoc */
post<D = unknown>(body?: RequestBody, opts?: CreateRequestOptions<D>): RequestPromise<D> {
const
url = this.resolveURL(),
eventName = this.name(),
method = this.method() ?? 'POST';
const req = this.request(url, this.resolver.bind(this), this.getRequestOptions(eventName ?? 'post', {
...opts,
body,
method
}));
if (eventName != null) {
return this.updateRequest(url, eventName, req);
}
return this.updateRequest(url, req);
}
/** @inheritDoc */
add<D = unknown>(body?: RequestBody, opts?: CreateRequestOptions<D>): RequestPromise<D> {
const
url = this.resolveURL(this.baseAddURL),
eventName = this.name() ?? 'add',
method = this.method() ?? this.addMethod;
const req = this.request(url, this.resolver.bind(this), this.getRequestOptions('add', {
...opts,
body,
method
}));
return this.updateRequest(url, eventName, req);
}
/**
* @alias
* @see [[Provider.upd]]
*/
update<D = unknown>(body?: RequestBody, opts?: CreateRequestOptions<D>): RequestPromise<D> {
return this.upd(body, opts);
}
/** @inheritDoc */
upd<D = unknown>(body?: RequestBody, opts?: CreateRequestOptions<D>): RequestPromise<D> {
const
url = this.resolveURL(this.baseUpdURL),
eventName = this.name() ?? 'upd',
method = this.method() ?? this.updMethod;
const req = this.request(url, this.resolver.bind(this), this.getRequestOptions('upd', {
...opts,
body,
method
}));
return this.updateRequest(url, eventName, req);
}
/**
* @alias
* @see [[Provider.del]]
*/
delete<D = unknown>(body?: RequestBody, opts?: CreateRequestOptions<D>): RequestPromise<D> {
return this.del(body, opts);
}
/** @inheritDoc */
del<D = unknown>(body?: RequestBody, opts?: CreateRequestOptions<D>): RequestPromise<D> {
const
url = this.resolveURL(this.baseDelURL),
eventName = this.name() ?? 'del',
method = this.method() ?? this.delMethod;
const req = this.request(url, this.resolver.bind(this), this.getRequestOptions('del', {
...opts,
body,
method
}));
return this.updateRequest(url, eventName, req);
}
/**
* Returns full request URL by the specified URL chunks
*
* @param [baseURL]
* @param [advURL]
*/
protected resolveURL(baseURL?: Nullable<string>, advURL?: Nullable<string>): string {
return concatURLs(baseURL == null ? this.baseURL : baseURL, advURL == null ? this.advURL : advURL);
}
/**
* Sets a readonly value by the specified key to the current provider
*/
protected setReadonlyParam(key: string, val: unknown): void {
Object.defineProperty(this, key, {
configurable: true,
get: () => val,
set: () => {
// Loopback
}
});
}
/**
* Returns an event cache key by the specified parameters
*
* @param event - event name
* @param data - event data
*/
protected getEventKey(event: string, data: unknown): unknown {
if (Object.isArray(data) || Object.isDictionary(data)) {
return `${event}::${Object.fastHash(data)}`;
}
return {};
}
/**
* Returns an object with request options by the specified model name and object with additional parameters
*
* @param method - model method
* @param [params] - additional parameters
*/
protected getRequestOptions<D = unknown>(
method: ModelMethod,
params?: CreateRequestOptions<D>
): CreateRequestOptions<D> {
const
{middlewares, encoders, decoders} = <typeof Provider>this.constructor;
const merge = (a, b) => {
a = Object.isFunction(a) ? [a] : a;
b = Object.isFunction(b) ? [b] : b;
return {...a, ...b};
};
const
mappedMiddlewares = merge(middlewares, params?.middlewares);
for (let keys = Object.keys(mappedMiddlewares), i = 0; i < keys.length; i++) {
const key = keys[i];
mappedMiddlewares[key] = mappedMiddlewares[key].bind(this);
}
return {
...params,
cacheId: this.cacheId,
middlewares: mappedMiddlewares,
encoder: merge(encoders[method] ?? encoders['def'], params?.encoder),
decoder: merge(decoders[method] ?? decoders['def'], params?.decoder),
meta: {
provider: this,
providerMethod: method,
providerParams: params
}
};
}
/**
* Updates the specified request with adding caching, etc.
*
* @param url - request url
* @param factory - request factory
*/
protected updateRequest<D = unknown>(url: string, factory: RequestFunctionResponse<D>): RequestPromise<D>;
/**
* Updates the specified request with adding caching, etc.
*
* @param url - request url
* @param event - event name that is fired after resolving of the request
* @param factory - request factory
*/
protected updateRequest<D = unknown>(
url: string,
event: string,
factory: RequestFunctionResponse<D>
): RequestPromise<D>;
protected updateRequest<D = unknown>(
url: string,
eventOrFactory: string | RequestFunctionResponse<D>,
factory?: RequestFunctionResponse<D>
): RequestPromise<D> {
let
event;
if (Object.isFunction(eventOrFactory)) {
factory = eventOrFactory;
} else {
event = eventOrFactory;
}
if (factory == null) {
throw new ReferenceError('A factory function to create the requests is not specified');
}
const
req = factory();
req
.then((res) => {
try {
const
cache = requestCache[this.cacheId];
const {
ctx: {
canCache,
cacheKey
}
} = res;
if (canCache && cacheKey != null && cache != null) {
cache[cacheKey] = Object.cast(res);
}
} catch (err) {
stderr(err);
}
if (event != null) {
this.emitter.emit(event, () => res.data);
}
})
.catch(() => {
// Do nothing. Logging is already handled in the request factory.
});
return req;
}
/**
* Initializes the socket behaviour after successful connecting
*/
protected initSocketBehaviour(): Promise<void> {
return Promise.resolve();
}
}