UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

155 lines (137 loc) 5.95 kB
import { RequestSendingError, RequestSendingSuccess } from '../subscription-worker-types'; import { BasePubNubRequest } from './request'; /** * SharedWorker's requests manager. * * Manager responsible for storing client-provided request for the time while enqueue / dequeue service request which * is actually sent to the PubNub service. */ export class RequestsManager extends EventTarget { // -------------------------------------------------------- // ------------------ Request processing ------------------ // -------------------------------------------------------- // region Request processing /** * Begin service request processing. * * @param request - Reference to the service request which should be sent. * @param success - Request success completion handler. * @param failure - Request failure handler. * @param responsePreprocess - Raw response pre-processing function which is used before calling handling callbacks. */ sendRequest( request: BasePubNubRequest, success: (fetchRequest: Request, response: RequestSendingSuccess) => void, failure: (fetchRequest: Request, errorResponse: RequestSendingError) => void, responsePreprocess?: (response: [Response, ArrayBuffer]) => [Response, ArrayBuffer], ) { request.handleProcessingStarted(); if (request.cancellable) request.fetchAbortController = new AbortController(); const fetchRequest = request.asFetchRequest; (async () => { Promise.race([ fetch(fetchRequest, { ...(request.fetchAbortController ? { signal: request.fetchAbortController.signal } : {}), keepalive: true, }), request.requestTimeoutTimer(), ]) .then((response): Promise<[Response, ArrayBuffer]> | [Response, ArrayBuffer] => response.arrayBuffer().then((buffer) => [response, buffer]), ) .then((response) => (responsePreprocess ? responsePreprocess(response) : response)) .then((response) => { if (response[0].status >= 400) failure(fetchRequest, this.requestProcessingError(undefined, response)); else success(fetchRequest, this.requestProcessingSuccess(response)); }) .catch((error) => { let fetchError = error; if (typeof error === 'string') { const errorMessage = error.toLowerCase(); fetchError = new Error(error); if (!errorMessage.includes('timeout') && errorMessage.includes('cancel')) fetchError.name = 'AbortError'; } request.stopRequestTimeoutTimer(); failure(fetchRequest, this.requestProcessingError(fetchError)); }); })(); } // endregion // -------------------------------------------------------- // ----------------------- Helpers ------------------------ // -------------------------------------------------------- // region Helpers /** * Create processing success event from service response. * * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each * specific PubNub client state. * * @param res - Service response for used REST API endpoint along with response body. * * @returns Request processing success event object. */ private requestProcessingSuccess(res: [Response, ArrayBuffer]): RequestSendingSuccess { const [response, body] = res; const responseBody = body.byteLength > 0 ? body : undefined; const contentLength = parseInt(response.headers.get('Content-Length') ?? '0', 10); const contentType = response.headers.get('Content-Type')!; const headers: Record<string, string> = {}; // Copy Headers object content into plain Record. response.headers.forEach((value, key) => (headers[key.toLowerCase()] = value.toLowerCase())); return { type: 'request-process-success', clientIdentifier: '', identifier: '', url: '', response: { contentLength, contentType, headers, status: response.status, body: responseBody }, }; } /** * Create processing error event from service response. * * **Note:** The rest of information like `clientIdentifier`,`identifier`, and `url` will be added later for each * specific PubNub client state. * * @param [error] - Client-side request processing error (for example network issues). * @param [response] - Service error response (for example permissions error or malformed * payload) along with service body. * @returns Request processing error event object. */ private requestProcessingError(error?: unknown, response?: [Response, ArrayBuffer]): RequestSendingError { // Use service response as error information source. if (response) return { ...this.requestProcessingSuccess(response), type: 'request-process-error' }; let type: NonNullable<RequestSendingError['error']>['type'] = 'NETWORK_ISSUE'; let message = 'Unknown error'; let name = 'Error'; if (error && error instanceof Error) { message = error.message; name = error.name; } const errorMessage = message.toLowerCase(); if (errorMessage.includes('timeout')) type = 'TIMEOUT'; else if (name === 'AbortError' || errorMessage.includes('aborted') || errorMessage.includes('cancel')) { message = 'Request aborted'; type = 'ABORTED'; } return { type: 'request-process-error', clientIdentifier: '', identifier: '', url: '', error: { name, type, message }, }; } /** * Percent-encode input string. * * **Note:** Encode content in accordance of the `PubNub` service requirements. * * @param input - Source string or number for encoding. * @returns Percent-encoded string. */ protected encodeString(input: string | number) { return encodeURIComponent(input).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); } // endregion }