UNPKG

data-transport

Version:
331 lines (317 loc) 10.6 kB
import { listenerKey, originalListensMapKey, requestsMapKey, listensMapKey, senderKey, timeoutKey, transportKey, prefixKey, transportType, produceKey, listenKey, serializerKey, logKey, verboseKey, beforeEmitKey, beforeEmitResolveKey, } from './constant'; import type { EmitOptions, IRequest, IResponse, ListenerOptions, Request, ListensMap, Response, TransportOptions, EmitParameter, BaseInteraction, } from './interface'; import { generateId } from './utils'; const DEFAULT_TIMEOUT = 60 * 1000; const DEFAULT_RESPOND = true; const DEFAULT_SILENT = false; const DEFAULT_PREFIX = 'DataTransport'; export const getAction = (prefix: string, name: string) => `${prefix}-${name.toString()}`; const getListenName = (prefix: string, action: string) => action.replace(new RegExp(`^${prefix}-`), ''); /** * Create a base transport */ export abstract class Transport<T extends BaseInteraction = any> { private [listenerKey]: TransportOptions['listener']; private [listenKey]: (options?: ListenerOptions) => void; private [senderKey]: TransportOptions['sender']; private [timeoutKey]: TransportOptions['timeout']; private [prefixKey]: TransportOptions['prefix']; private [serializerKey]: TransportOptions['serializer']; private [requestsMapKey]: Map<string, (value: unknown) => void> = new Map(); private [listensMapKey]!: ListensMap; private [originalListensMapKey]!: Map<string, Function>; private [logKey]?: (listenOptions: ListenerOptions<any>) => void; private [verboseKey]: boolean; protected [beforeEmitKey]?: Promise<void>; protected [beforeEmitResolveKey]?: () => void; /** * dispose transport */ public dispose: () => any; constructor({ listener, sender, timeout = DEFAULT_TIMEOUT, verbose = false, prefix = DEFAULT_PREFIX, listenKeys = [], checkListen = true, serializer, logger, }: TransportOptions) { this[listensMapKey] = this[listensMapKey] ?? new Map(); this[originalListensMapKey] = this[originalListensMapKey] ?? new Map(); this[listenerKey] = listener.bind(this); this[senderKey] = sender.bind(this); this[timeoutKey] = timeout; this[prefixKey] = prefix; this[serializerKey] = serializer; this[verboseKey] = verbose; this[logKey] = logger; new Set(listenKeys).forEach((key) => { const fn = (this as any as Record<string, Function>)[key]; if (__DEV__) { if (typeof fn !== 'function') { console.warn(`'${key}' is NOT a methods or function.`); } } this[originalListensMapKey].set(key, fn); Object.assign(this, { [key]() { if (__DEV__) { throw new Error( `The method '${key}' is a listen function that can NOT be actively called.` ); } }, }); }); this[originalListensMapKey].forEach((value, name) => { this[produceKey](name, value); }); this[listenKey] = (options?: ListenerOptions) => { if (this[verboseKey]) { if (typeof this[logKey] === 'function' && options) { this[logKey]!(options); } else { console.info('DataTransport Receive: ', options); } } if (options?.[transportKey]) { const listenName = getListenName(this[prefixKey]!, options.action); const hasListen = typeof (this as any)[listenName] === 'function'; if ((options as IResponse).type === transportType.response) { const resolve = this[requestsMapKey].get(options[transportKey]); if (resolve) { const { response } = options as IResponse; resolve( typeof response === 'string' && this[serializerKey]?.parse ? this[serializerKey]!.parse!(response) : response ); } else if (hasListen) { if (__DEV__ && checkListen) { console.warn( `The type '${options.action}' event '${options[transportKey]}' has been resolved. Please check for a duplicate response.` ); } } } else if ((options as IRequest).type === transportType.request) { const respond = this[listensMapKey].get(options.action); if (typeof respond === 'function') { const { request } = options as IRequest; respond( typeof request === 'string' && this[serializerKey]?.parse ? this[serializerKey]!.parse!(request) : request, { ...options, transportId: options[transportKey], hasRespond: (options as IRequest).hasRespond, } ); } else if (hasListen) { if (__DEV__ && checkListen) { console.error( `The listen method or function '${listenName}' is NOT decorated by decorator '@listen' or be added 'listenKeys' list.` ); } } } } }; const dispose = this[listenerKey](this[listenKey]); this.dispose = () => { if (typeof dispose === 'function') { this[requestsMapKey].clear(); this[listensMapKey].clear(); this[originalListensMapKey].clear(); return dispose(); } else if (__DEV__) { console.warn( `The return value of the the '${this.constructor.name}' transport's listener should be a 'dispose' function for removing the listener` ); } }; } private [produceKey]<K extends string, P extends Record<string, Function>>( name: K, fn: P[K] ) { // https://github.com/microsoft/TypeScript/issues/40465 const action = getAction(this[prefixKey]!, name); this[listensMapKey].set( action, async (request, { hasRespond, transportId, request: _, ...args }) => { if (typeof fn === 'function') { const response: Response<P[K]> = await fn.apply(this, request); if (!hasRespond) return; const data: IResponse = { ...args, action, response: (typeof response !== 'undefined' && this[serializerKey]?.stringify ? this[serializerKey]!.stringify!(response) : response) as string | undefined, hasRespond, [transportKey]: transportId, type: transportType.response, responseId: this.id, }; this[senderKey](data); } else { throw new Error( `The listener for event ${name} should be a function.` ); } } ); } /** * Listen an event that transport data. * * @param name A transport action as listen message data action type * @param fn A transport listener */ public listen<K extends keyof T['listen']>(name: K, fn: T['listen'][K]) { if (typeof name === 'string') { if (this[originalListensMapKey].get(name)) { throw new Error( `Failed to listen to the event "${name}", the event "${name}" is already listened to.` ); } if (typeof fn === 'function') { this[originalListensMapKey].set(name, fn); this[produceKey](name, fn); } else { throw new Error(`The listener for event ${name} should be a function.`); } } else { throw new Error( `The event name "${name.toString()}" is not a string, it should be a string.` ); } return () => { this[originalListensMapKey].delete(name); const action = getAction(this[prefixKey]!, name); this[listensMapKey].delete(action); }; } public id = generateId(); /** * Emit an event that transport data. * * @param emitOptions A option for the transport data * @param request A request data * * @returns Return a response for the request. */ public async emit<K extends keyof T['emit']>( options: EmitOptions<K>, ...request: Request<T['emit'][K]> ): Promise<Response<T['emit'][K]>> { const params = typeof options === 'object' ? options : ({} as EmitParameter<K>); const hasRespond = params.respond ?? DEFAULT_RESPOND; const isSilent = params.silent ?? DEFAULT_SILENT; const timeout = params.timeout ?? this[timeoutKey]; const name = params.name ?? options; const transportId = generateId(); if (__DEV__ && (!name || typeof name !== 'string')) { throw new Error(`The event name should be a string, and it's required.`); } const action = getAction(this[prefixKey]!, name as string); const rawRequestData: IRequest = { ...(params._extra ? { _extra: params._extra } : {}), type: transportType.request, action, request: (typeof request !== 'undefined' && this[serializerKey]?.stringify ? this[serializerKey]!.stringify!(request) : request) as unknown[], hasRespond, [transportKey]: transportId, requestId: this.id, }; if (this[verboseKey]) { if (typeof this[logKey] === 'function') { this[logKey]!(rawRequestData); } else { console.info('DataTransport Send: ', rawRequestData); } } if (!hasRespond) { if (this[beforeEmitKey] && !params.skipBeforeEmit) { await this[beforeEmitKey]; } this[senderKey](rawRequestData); return Promise.resolve(undefined as Response<T['emit'][K]>); } let timeoutId: NodeJS.Timeout | number; const promise = Promise.race<any>([ new Promise(async (resolve) => { if (this[beforeEmitKey] && !params.skipBeforeEmit) { await this[beforeEmitKey]; } this[requestsMapKey].set(transportId, resolve); this[senderKey](rawRequestData); }), new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(); }, timeout); }), ]); return promise .then((response) => { // support Safari 10-11.1 clearTimeout(timeoutId as NodeJS.Timeout); this[requestsMapKey].delete(transportId); return response; }) .catch((error) => { clearTimeout(timeoutId as NodeJS.Timeout); this[requestsMapKey].delete(transportId); if (typeof error === 'undefined') { if (isSilent) return; console.warn( `The event '${action}' timed out for ${timeout} seconds...`, rawRequestData ); } else { if (__DEV__) { throw error; } } }); } }