pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
217 lines (191 loc) • 6.5 kB
text/typescript
/**
* Network request module.
*
* @internal
*/
import { CancellationController, TransportMethod, TransportRequest } from '../types/transport-request';
import { createMalformedResponseError, PubNubError } from '../../errors/pubnub-error';
import { TransportResponse } from '../types/transport-response';
import { PubNubAPIError } from '../../errors/pubnub-api-error';
import RequestOperation from '../constants/operations';
import { PubNubFileInterface } from '../types/file';
import { Request } from '../interfaces/request';
import { Query } from '../types/api';
import uuidGenerator from './uuid';
/**
* Base REST API request class.
*
* @internal
*/
export abstract class AbstractRequest<ResponseType, ServiceResponse extends object> implements Request<ResponseType> {
/**
* Service `ArrayBuffer` response decoder.
*/
protected static decoder = new TextDecoder();
/**
* Request cancellation controller.
*/
private _cancellationController: CancellationController | null;
/**
* Unique request identifier.
*/
requestIdentifier = uuidGenerator.createUUID();
/**
* Construct base request.
*
* Constructed request by default won't be cancellable and performed using `GET` HTTP method.
*
* @param params - Request configuration parameters.
*/
protected constructor(
private readonly params?: { method?: TransportMethod; cancellable?: boolean; compressible?: boolean },
) {
this._cancellationController = null;
}
/**
* Retrieve configured cancellation controller.
*
* @returns Cancellation controller.
*/
public get cancellationController(): CancellationController | null {
return this._cancellationController;
}
/**
* Update request cancellation controller.
*
* Controller itself provided by transport provider implementation and set only when request
* sending has been scheduled.
*
* @param controller - Cancellation controller or `null` to reset it.
*/
public set cancellationController(controller: CancellationController | null) {
this._cancellationController = controller;
}
/**
* Abort request if possible.
*
* @param [reason] Information about why request has been cancelled.
*/
abort(reason?: string): void {
if (this && this.cancellationController) this.cancellationController.abort(reason);
}
/**
* Target REST API endpoint operation type.
*/
operation(): RequestOperation {
throw Error('Should be implemented by subclass.');
}
/**
* Validate user-provided data before scheduling request.
*
* @returns Error message if request can't be sent without missing or malformed parameters.
*/
validate(): string | undefined {
return undefined;
}
/**
* Parse service response.
*
* @param response - Raw service response which should be parsed.
*/
async parse(response: TransportResponse): Promise<ResponseType> {
return this.deserializeResponse(response) as unknown as ResponseType;
}
/**
* Create platform-agnostic request object.
*
* @returns Request object which can be processed using platform-specific requirements.
*/
request(): TransportRequest {
const request: TransportRequest = {
method: this.params?.method ?? TransportMethod.GET,
path: this.path,
queryParameters: this.queryParameters,
cancellable: this.params?.cancellable ?? false,
compressible: this.params?.compressible ?? false,
timeout: 10,
identifier: this.requestIdentifier,
};
// Attach headers (if required).
const headers = this.headers;
if (headers) request.headers = headers;
// Attach body (if required).
if (request.method === TransportMethod.POST || request.method === TransportMethod.PATCH) {
const [body, formData] = [this.body, this.formData];
if (formData) request.formData = formData;
if (body) request.body = body;
}
return request;
}
/**
* Target REST API endpoint request headers getter.
*
* @returns Key/value headers which should be used with request.
*/
protected get headers(): Record<string, string> | undefined {
return {
'Accept-Encoding': 'gzip, deflate',
...((this.params?.compressible ?? false) ? { 'Content-Encoding': 'deflate' } : {}),
};
}
/**
* Target REST API endpoint request path getter.
*
* @returns REST API path.
*/
protected get path(): string {
throw Error('`path` getter should be implemented by subclass.');
}
/**
* Target REST API endpoint request query parameters getter.
*
* @returns Key/value pairs which should be appended to the REST API path.
*/
protected get queryParameters(): Query {
return {};
}
protected get formData(): Record<string, string>[] | undefined {
return undefined;
}
/**
* Target REST API Request body payload getter.
*
* @returns Buffer of stringified data which should be sent with `POST` or `PATCH` request.
*/
protected get body(): ArrayBuffer | PubNubFileInterface | string | undefined {
return undefined;
}
/**
* Deserialize service response.
*
* @param response - Transparent response object with headers and body information.
*
* @returns Deserialized service response data.
*
* @throws {Error} if received service response can't be processed (has unexpected content-type or can't be parsed as
* JSON).
*/
protected deserializeResponse(response: TransportResponse): ServiceResponse {
const responseText = AbstractRequest.decoder.decode(response.body);
const contentType = response.headers['content-type'];
let parsedJson: ServiceResponse;
if (!contentType || (contentType.indexOf('javascript') === -1 && contentType.indexOf('json') === -1))
throw new PubNubError(
'Service response error, check status for details',
createMalformedResponseError(responseText, response.status),
);
try {
parsedJson = JSON.parse(responseText);
} catch (error) {
console.error('Error parsing JSON response:', error);
throw new PubNubError(
'Service response error, check status for details',
createMalformedResponseError(responseText, response.status),
);
}
// Throw and exception in case of client / server error.
if ('status' in parsedJson && typeof parsedJson.status === 'number' && parsedJson.status >= 400)
throw PubNubAPIError.create(response);
return parsedJson;
}
}