Skip to content

Commit

Permalink
Improve typings relating to requestAsEventEmitter
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmelnikow committed Mar 22, 2019
1 parent 0a79ccb commit 3696a2e
Show file tree
Hide file tree
Showing 11 changed files with 87 additions and 66 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dependencies": {
"@sindresorhus/is": "^0.15.0",
"@szmarczak/http-timer": "^1.1.2",
"@types/cacheable-request": "^6.0.0",
"@types/form-data": "^2.2.1",
"cacheable-lookup": "^0.1.0",
"cacheable-request": "^6.0.0",
Expand Down
9 changes: 2 additions & 7 deletions source/as-promise.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ClientRequest, IncomingMessage} from 'http';
import {IncomingMessage} from 'http';
import EventEmitter from 'events';
import getStream from 'get-stream';
import is from '@sindresorhus/is';
Expand All @@ -9,11 +9,6 @@ import {mergeOptions} from './merge';
import {reNormalizeArguments} from './normalize-arguments';
import {CancelableRequest, Options, Response} from './utils/types';

// TODO: Remove once request-as-event-emitter is converted to TypeScript
interface RequestAsEventEmitter extends ClientRequest {
retry: (error: Error) => boolean;
}

export default function asPromise(options: Options) {
const proxy = new EventEmitter();

Expand All @@ -28,7 +23,7 @@ export default function asPromise(options: Options) {
};

const promise = new PCancelable<IncomingMessage>((resolve, reject, onCancel) => {
const emitter = requestAsEventEmitter(options) as RequestAsEventEmitter;
const emitter = requestAsEventEmitter(options);

onCancel(emitter.abort);

Expand Down
3 changes: 1 addition & 2 deletions source/as-stream.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {ClientRequest} from 'http';
import {PassThrough as PassThroughStream} from 'stream';
import duplexer3 from 'duplexer3';
import requestAsEventEmitter from './request-as-event-emitter';
Expand All @@ -20,7 +19,7 @@ export default function asStream(options: MergedOptions) {
};
}

const emitter = requestAsEventEmitter(options, input) as ClientRequest;
const emitter = requestAsEventEmitter(options, input);

// Cancels the request
proxy._destroy = emitter.abort;
Expand Down
6 changes: 3 additions & 3 deletions source/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import urlLib from 'url';
import http, {IncomingHttpHeaders} from 'http';
import * as urlLib from 'url';
import * as http from 'http';
import is from '@sindresorhus/is';
import {Response, Timings, Options} from './utils/types';
import {TimeoutError as TimedOutError} from './utils/timed-out';
Expand Down Expand Up @@ -69,7 +69,7 @@ export class ParseError extends GotError {
}

export class HTTPError extends GotError {
headers?: IncomingHttpHeaders;
headers?: http.IncomingHttpHeaders;

body: string | Buffer;

Expand Down
2 changes: 1 addition & 1 deletion source/get-response.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {IncomingMessage} from 'http';
import EventEmitter from 'events';
import {EventEmitter} from 'events';
import {Transform as TransformStream} from 'stream';
import is from '@sindresorhus/is';
import decompressResponse from 'decompress-response';
Expand Down
3 changes: 2 additions & 1 deletion source/normalize-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import validateSearchParams from './utils/validate-search-params';
import supportsBrotli from './utils/supports-brotli';
import merge from './merge';
import knownHookEvents from './known-hook-events';
import {NormalizedOptions} from './utils/types';

const retryAfterStatusCodes = new Set([413, 429, 503]);

Expand Down Expand Up @@ -99,7 +100,7 @@ export const preNormalizeArguments = (options: any, defaults?: any) => {
delete options.dnsCache;
}

return options;
return options as NormalizedOptions;
};

export const normalizeArguments = (url, options, defaults?: any) => {
Expand Down
82 changes: 48 additions & 34 deletions source/request-as-event-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import urlLib, {URL, URLSearchParams} from 'url'; // TODO: Use the `URL` global when targeting Node.js 10
import util from 'util';
import EventEmitter from 'events';
import {format as urlFormat, URL, URLSearchParams} from 'url'; // TODO: Use the `URL` global when targeting Node.js 10
import * as util from 'util';
import {EventEmitter} from 'events';
import {Transform as TransformStream} from 'stream';
import http from 'http';
import https from 'https';
import CacheableRequest from 'cacheable-request';
import * as http from 'http';
import * as https from 'https';
import * as CacheableRequest from 'cacheable-request';
import toReadableStream from 'to-readable-stream';
import is from '@sindresorhus/is';
import timer from '@szmarczak/http-timer';
Expand All @@ -15,25 +15,36 @@ import getResponse from './get-response';
import {uploadProgress} from './progress';
import {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError, TimeoutError} from './errors';
import urlToOptions from './utils/url-to-options';
import {RequestFn, Options, Delays, RetryFn, RetryOption} from './utils/types';

const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
const allMethodRedirectCodes = new Set([300, 303, 307, 308]);

type ProcessVersions = typeof process.versions
interface ProcessVersionsWithElectron extends ProcessVersions {
electron: string;
}

export interface RequestAsEventEmitter extends EventEmitter {
retry: (error: Error) => (boolean | undefined);
abort: () => void;
}

export default (options, input?: TransformStream) => {
const emitter = new EventEmitter();
const redirects = [];
let currentRequest;
let requestUrl;
let redirectString;
let uploadBodySize;
const redirects = [] as string[];
let currentRequest: http.ClientRequest;
let requestUrl: string;
let redirectString: string;
let uploadBodySize: number | undefined;
let retryCount = 0;
let shouldAbort = false;

const setCookie = options.cookieJar ? util.promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null;
const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null;
const agents = is.object(options.agent) ? options.agent : null;

const emitError = async error => {
const emitError = async (error: Error) => {
try {
for (const hook of options.hooks.beforeError) {
// eslint-disable-next-line no-await-in-loop
Expand All @@ -46,7 +57,7 @@ export default (options, input?: TransformStream) => {
}
};

const get = async options => {
const get = async (options: Options) => {
const currentUrl = redirectString || requestUrl;

if (options.protocol !== 'http:' && options.protocol !== 'https:') {
Expand All @@ -55,11 +66,11 @@ export default (options, input?: TransformStream) => {

decodeURI(currentUrl);

let fn;
let requestFn: RequestFn;
if (is.function_(options.request)) {
fn = {request: options.request};
requestFn = options.request;
} else {
fn = options.protocol === 'https:' ? https : http;
requestFn = options.protocol === 'https:' ? https.request : http.request;
}

if (agents) {
Expand All @@ -68,11 +79,11 @@ export default (options, input?: TransformStream) => {
}

/* istanbul ignore next: electron.net is broken */
if (options.useElectronNet && (process.versions as any).electron) {
if (options.useElectronNet && (process.versions as ProcessVersionsWithElectron).electron) {
// @ts-ignore
const r = ({x: require})['yx'.slice(1)]; // Trick webpack
const electron = r('electron');
fn = electron.net || electron.remote.net;
requestFn = electron.net || electron.remote.net;
}

if (options.cookieJar) {
Expand All @@ -84,6 +95,7 @@ export default (options, input?: TransformStream) => {
}

let timings;
// TODO: Properly type this.
const handleResponse = async response => {
try {
/* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */
Expand Down Expand Up @@ -160,7 +172,7 @@ export default (options, input?: TransformStream) => {
}
};

const handleRequest = request => {
const handleRequest = (request: http.ClientRequest) => {
if (shouldAbort) {
request.abort();
return;
Expand All @@ -179,8 +191,7 @@ export default (options, input?: TransformStream) => {
error = new RequestError(error, options);
}

// TODO: Properly type this
if ((emitter as any).retry(error) === false) {
if ((emitter as RequestAsEventEmitter).retry(error) === false) {
emitError(error);
}
});
Expand All @@ -190,7 +201,8 @@ export default (options, input?: TransformStream) => {
uploadProgress(request, emitter, uploadBodySize);

if (options.gotTimeout) {
timedOut(request, options.gotTimeout, options);
// TODO: Properly type this. `preNormalizeArguments` coerces `gotTimeout` to `Delays`.
timedOut(request, options.gotTimeout as Delays, options);
}

emitter.emit('request', request);
Expand Down Expand Up @@ -218,8 +230,10 @@ export default (options, input?: TransformStream) => {
};

if (options.cache) {
const cacheableRequest = new CacheableRequest(fn.request, options.cache);
const cacheRequest = cacheableRequest(options, handleResponse);
// TODO: Properly type this.
const cacheableRequest = new CacheableRequest(requestFn, options.cache as any);
// TODO: Properly type this.
const cacheRequest = cacheableRequest(options as https.RequestOptions, handleResponse);

cacheRequest.once('error', error => {
if (error instanceof CacheableRequest.RequestError) {
Expand All @@ -231,21 +245,22 @@ export default (options, input?: TransformStream) => {

cacheRequest.once('request', handleRequest);
} else {
// Catches errors thrown by calling fn.request(...)
// Catches errors thrown by calling requestFn(...)
try {
handleRequest(fn.request(options, handleResponse));
// TODO: Properly type this.
handleRequest(requestFn(options as https.RequestOptions, handleResponse));
} catch (error) {
emitError(new RequestError(error, options));
}
}
};

// TODO: Properly type this
(emitter as any).retry = error => {
let backoff;
(emitter as RequestAsEventEmitter).retry = (error: Error): (boolean | undefined) => {
let backoff: number;

try {
backoff = options.retry.retries(++retryCount, error);
// TODO: Properly type this. Looks like a case handled by `preNormalizeArguments`.
backoff = ((options.retry as RetryOption).retries as RetryFn)(++retryCount, error);
} catch (error2) {
emitError(error2);
return;
Expand All @@ -272,8 +287,7 @@ export default (options, input?: TransformStream) => {
return false;
};

// TODO: Properly type this
(emitter as any).abort = () => {
(emitter as RequestAsEventEmitter).abort = () => {
if (currentRequest) {
currentRequest.abort();
} else {
Expand Down Expand Up @@ -337,13 +351,13 @@ export default (options, input?: TransformStream) => {
options.headers.accept = 'application/json';
}

requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString();
requestUrl = options.href || (new URL(options.path, urlFormat(options))).toString();

await get(options);
} catch (error) {
emitError(error);
}
});

return emitter;
return emitter as RequestAsEventEmitter;
};
2 changes: 1 addition & 1 deletion source/utils/get-body-size.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs';
import * as fs from 'fs';
import {promisify} from 'util';
import is from '@sindresorhus/is';
import isFormData from './is-form-data';
Expand Down
2 changes: 1 addition & 1 deletion source/utils/is-form-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import is from '@sindresorhus/is';
import FormData from 'form-data';
import * as FormData from 'form-data';

export default (body: unknown): body is FormData => is.nodeStream(body) && is.function_((body as FormData).getBoundary);
13 changes: 2 additions & 11 deletions source/utils/timed-out.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import net from 'net';
import * as net from 'net';
import {ClientRequest} from 'http';
import {Delays} from './types';

export class TimeoutError extends Error {
event: string;
Expand All @@ -18,16 +19,6 @@ export class TimeoutError extends Error {
const reentry: symbol = Symbol('reentry');
const noop = (): void => {};

export interface Delays {
lookup?: number;
connect?: number;
secureConnect?: number;
socket?: number;
response?: number;
send?: number;
request?: number;
}

export default (request: ClientRequest, delays: Delays, options: any) => {
/* istanbul ignore next: this makes sure timed-out isn't called twice */
if (Reflect.has(request, reentry)) {
Expand Down
30 changes: 25 additions & 5 deletions source/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {IncomingMessage} from 'http';
import {RequestOptions} from 'https';
import * as https from 'https';
import {Readable as ReadableStream} from 'stream';
import PCancelable from 'p-cancelable';
import {CookieJar} from 'tough-cookie';
import {Hooks} from '../known-hook-events';

export type Method = 'GET' | 'PUT' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'TRACE' | 'get' | 'put' | 'head' | 'delete' | 'options' | 'trace';
export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'TRACE' | 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options' | 'trace';
export type ErrorCode = 'ETIMEDOUT' | 'ECONNRESET' | 'EADDRINUSE' | 'ECONNREFUSED' | 'EPIPE' | 'ENOTFOUND' | 'ENETUNREACH' | 'EAI_AGAIN';
export type StatusCode = 408 | 413 | 429 | 500 | 502 | 503 | 504;

Expand Down Expand Up @@ -50,19 +51,34 @@ export interface InterfaceWithDefaults extends Instance {
};
}

interface RetryOption {
retries?: ((retry: number, error: Error) => number) | number;
export type RetryFn = (retry: number, error: Error) => number;

export interface RetryOption {
retries?: RetryFn | number;
methods?: Method[];
statusCodes?: StatusCode[];
maxRetryAfter?: number;
errorCodes?: ErrorCode[];
}

export type RequestFn = typeof https.request;

export interface MergedOptions extends Options {
retry: RetryOption;
}

export interface Options extends RequestOptions {
export interface Delays {
lookup?: number;
connect?: number;
secureConnect?: number;
socket?: number;
response?: number;
send?: number;
request?: number;
}

// The library overrides the type definition of `agent`.
export interface Options extends Pick<https.RequestOptions, Exclude<keyof https.RequestOptions, 'agent'>> {
host: string;
body: string | Buffer | ReadableStream;
hostname?: string;
Expand All @@ -77,6 +93,10 @@ export interface Options extends RequestOptions {
method?: Method;
retry?: number | RetryOption;
throwHttpErrors?: boolean;
cookieJar?: CookieJar;
request?: RequestFn;
agent: https.Agent | boolean | { [key: string]: https.Agent };
gotTimeout?: number | Delays;
// TODO: Remove this once TS migration is complete and all options are defined.
[key: string]: unknown;
}
Expand Down

0 comments on commit 3696a2e

Please sign in to comment.