Skip to content

Commit

Permalink
Make TypeScript types conforms to strict mode (#928)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
pmmmwh and sindresorhus committed Nov 25, 2019
1 parent d9a3273 commit c537dee
Show file tree
Hide file tree
Showing 48 changed files with 580 additions and 466 deletions.
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -41,7 +41,6 @@
"@sindresorhus/is": "^1.0.0",
"@szmarczak/http-timer": "^3.1.0",
"@types/cacheable-request": "^6.0.1",
"@types/tough-cookie": "^2.3.5",
"cacheable-lookup": "^0.2.1",
"cacheable-request": "^7.0.0",
"decompress-response": "^5.0.0",
Expand All @@ -55,10 +54,14 @@
"type-fest": "^0.8.0"
},
"devDependencies": {
"@sindresorhus/tsconfig": "^0.5.0",
"@sindresorhus/tsconfig": "^0.6.0",
"@types/duplexer3": "^0.1.0",
"@types/express": "^4.17.2",
"@types/lolex": "^3.1.1",
"@types/node": "^12.12.8",
"@types/proxyquire": "^1.3.28",
"@types/sinon": "^7.0.13",
"@types/tough-cookie": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"ava": "^2.4.0",
Expand All @@ -67,6 +70,7 @@
"del-cli": "^3.0.0",
"delay": "^4.3.0",
"eslint-config-xo-typescript": "^0.21.0",
"express": "^4.17.1",
"form-data": "^3.0.0",
"get-port": "^5.0.0",
"keyv": "^4.0.0",
Expand Down
19 changes: 9 additions & 10 deletions source/as-promise.ts
@@ -1,12 +1,11 @@
import {IncomingMessage} from 'http';
import EventEmitter = require('events');
import getStream = require('get-stream');
import is from '@sindresorhus/is';
import PCancelable = require('p-cancelable');
import {NormalizedOptions, Response, CancelableRequest} from './utils/types';
import is from '@sindresorhus/is';
import {ParseError, ReadError, HTTPError} from './errors';
import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
import {normalizeArguments, mergeOptions} from './normalize-arguments';
import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
import {CancelableRequest, GeneralError, NormalizedOptions, Response} from './utils/types';

const parseBody = (body: Response['body'], responseType: NormalizedOptions['responseType'], statusCode: Response['statusCode']) => {
if (responseType === 'json' && is.string(body)) {
Expand All @@ -28,16 +27,16 @@ const parseBody = (body: Response['body'], responseType: NormalizedOptions['resp
throw new Error(`Failed to parse body of type '${typeof body}' as '${responseType}'`);
};

export default function asPromise(options: NormalizedOptions) {
export default function asPromise<T>(options: NormalizedOptions): CancelableRequest<T> {
const proxy = new EventEmitter();
let finalResponse: Pick<Response, 'body' | 'statusCode'>;

// @ts-ignore `.json()`, `.buffer()` and `.text()` are added later
const promise = new PCancelable<IncomingMessage | Response['body']>((resolve, reject, onCancel) => {
const promise = new PCancelable<Response | Response['body']>((resolve, reject, onCancel) => {
const emitter = requestAsEventEmitter(options);
onCancel(emitter.abort);

const emitError = async (error: Error): Promise<void> => {
const emitError = async (error: GeneralError): Promise<void> => {
try {
for (const hook of options.hooks.beforeError) {
// eslint-disable-next-line no-await-in-loop
Expand Down Expand Up @@ -80,7 +79,7 @@ export default function asPromise(options: NormalizedOptions) {
resolveBodyOnly: false
}));

// Remove any further hooks for that request, because we we'll call them anyway.
// Remove any further hooks for that request, because we'll call them anyway.
// The loop continues. We don't want duplicates (asPromise recursion).
updatedOptions.hooks.afterResponse = options.hooks.afterResponse.slice(0, index);

Expand Down Expand Up @@ -124,7 +123,7 @@ export default function asPromise(options: NormalizedOptions) {
const limitStatusCode = options.followRedirect ? 299 : 399;
if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) {
const error = new HTTPError(response, options);
if (emitter.retry(error) === false) {
if (!emitter.retry(error)) {
if (options.throwHttpErrors) {
emitError(error);
return;
Expand All @@ -149,7 +148,7 @@ export default function asPromise(options: NormalizedOptions) {
return promise;
};

const shortcut = (responseType: NormalizedOptions['responseType']): CancelableRequest<any> => {
const shortcut = <T>(responseType: NormalizedOptions['responseType']): CancelableRequest<T> => {
// eslint-disable-next-line promise/prefer-await-to-then
const newPromise = promise.then(() => parseBody(finalResponse.body, responseType, finalResponse.statusCode));

Expand Down
16 changes: 8 additions & 8 deletions source/as-stream.ts
@@ -1,16 +1,16 @@
import {PassThrough as PassThroughStream, Duplex as DuplexStream} from 'stream';
import duplexer3 = require('duplexer3');
import stream = require('stream');
import {IncomingMessage} from 'http';
import duplexer3 = require('duplexer3');
import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
import {Duplex as DuplexStream, PassThrough as PassThroughStream} from 'stream';
import {HTTPError, ReadError} from './errors';
import {NormalizedOptions, Response, GotEvents} from './utils/types';
import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
import {GeneralError, GotEvents, NormalizedOptions, Response} from './utils/types';

export class ProxyStream extends DuplexStream implements GotEvents<ProxyStream> {
export class ProxyStream<T = unknown> extends DuplexStream implements GotEvents<ProxyStream<T>> {
isFromCache?: boolean;
}

export default function asStream(options: NormalizedOptions): ProxyStream {
export default function asStream<T>(options: NormalizedOptions): ProxyStream<T> {
const input = new PassThroughStream();
const output = new PassThroughStream();
const proxy = duplexer3(input, output) as ProxyStream;
Expand All @@ -35,7 +35,7 @@ export default function asStream(options: NormalizedOptions): ProxyStream {

const emitter = requestAsEventEmitter(options);

const emitError = async (error: Error): Promise<void> => {
const emitError = async (error: GeneralError): Promise<void> => {
try {
for (const hook of options.hooks.beforeError) {
// eslint-disable-next-line no-await-in-loop
Expand Down Expand Up @@ -102,7 +102,7 @@ export default function asStream(options: NormalizedOptions): ProxyStream {
});

proxyEvents(proxy, emitter);
emitter.on('error', (error: Error) => proxy.emit('error', error));
emitter.on('error', (error: GeneralError) => proxy.emit('error', error));

const pipe = proxy.pipe.bind(proxy);
const unpipe = proxy.unpipe.bind(proxy);
Expand Down
44 changes: 25 additions & 19 deletions source/calculate-retry-delay.ts
@@ -1,39 +1,45 @@
import is from '@sindresorhus/is';
import {HTTPError, ParseError, MaxRedirectsError, GotError} from './errors';
import {RetryFunction} from './utils/types';
import {HTTPError, ParseError, MaxRedirectsError} from './errors';
import {RetryFunction, RetryObject} from './utils/types';

const retryAfterStatusCodes: ReadonlySet<number> = new Set([413, 429, 503]);

const isErrorWithResponse = (error: RetryObject['error']): error is HTTPError | ParseError | MaxRedirectsError => (
error instanceof HTTPError || error instanceof ParseError || error instanceof MaxRedirectsError
);

const calculateRetryDelay: RetryFunction = ({attemptCount, retryOptions, error}) => {
if (attemptCount > retryOptions.limit) {
return 0;
}

const hasMethod = retryOptions.methods.includes((error as GotError).options.method);
const hasErrorCode = Reflect.has(error, 'code') && retryOptions.errorCodes.includes((error as GotError).code);
const hasStatusCode = Reflect.has(error, 'response') && retryOptions.statusCodes.includes((error as HTTPError | ParseError | MaxRedirectsError).response.statusCode);
const hasMethod = retryOptions.methods.includes(error.options.method);
const hasErrorCode = Reflect.has(error, 'code') && retryOptions.errorCodes.includes(error.code);
const hasStatusCode = isErrorWithResponse(error) && Reflect.has(error, 'response') && retryOptions.statusCodes.includes(error.response?.statusCode);
if (!hasMethod || (!hasErrorCode && !hasStatusCode)) {
return 0;
}

const {response} = error as HTTPError | ParseError | MaxRedirectsError | undefined;
if (response && Reflect.has(response.headers, 'retry-after') && retryAfterStatusCodes.has(response.statusCode)) {
let after = Number(response.headers['retry-after']);
if (is.nan(after)) {
after = Date.parse(response.headers['retry-after']) - Date.now();
} else {
after *= 1000;
if (isErrorWithResponse(error)) {
const {response} = error;
if (response && Reflect.has(response.headers, 'retry-after') && retryAfterStatusCodes.has(response.statusCode)) {
let after = Number(response.headers['retry-after']);
if (is.nan(after)) {
after = Date.parse(response.headers['retry-after']!) - Date.now();
} else {
after *= 1000;
}

if (after > retryOptions.maxRetryAfter) {
return 0;
}

return after;
}

if (after > retryOptions.maxRetryAfter) {
if (response?.statusCode === 413) {
return 0;
}

return after;
}

if (response?.statusCode === 413) {
return 0;
}

const noise = Math.random() * 100;
Expand Down
94 changes: 47 additions & 47 deletions source/create.ts
@@ -1,20 +1,19 @@
import {Merge} from 'type-fest';
import {Merge, PartialDeep} from 'type-fest';
import asPromise from './as-promise';
import asStream, {ProxyStream} from './as-stream';
import * as errors from './errors';
import {normalizeArguments, mergeOptions} from './normalize-arguments';
import deepFreeze from './utils/deep-freeze';
import {
Options,
Defaults,
NormalizedOptions,
Response,
CancelableRequest,
URLOrOptions,
ExtendOptions,
HandlerFunction,
ExtendedOptions
NormalizedDefaults,
NormalizedOptions,
Options,
Response,
URLOrOptions
} from './utils/types';
import deepFreeze from './utils/deep-freeze';
import asPromise from './as-promise';
import asStream, {ProxyStream} from './as-stream';
import {normalizeArguments, mergeOptions} from './normalize-arguments';
import {Hooks} from './known-hook-events';

export type HTTPAlias =
| 'get'
Expand All @@ -24,36 +23,39 @@ export type HTTPAlias =
| 'head'
| 'delete';

export type ReturnStream = (url: string | Options & {isStream: true}, options?: Options & {isStream: true}) => ProxyStream;
export type GotReturn = ProxyStream | CancelableRequest<Response>;
export type ReturnStream = <T>(url: string | Merge<Options, {isStream?: true}>, options?: Merge<Options, {isStream?: true}>) => ProxyStream<T>;
export type GotReturn<T = unknown> = CancelableRequest<T> | ProxyStream<T>;

const getPromiseOrStream = (options: NormalizedOptions): GotReturn => options.isStream ? asStream(options) : asPromise(options);

type OptionsOfDefaultResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType?: 'default'};
type OptionsOfTextResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType: 'text'};
type OptionsOfJSONResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType: 'json'};
type OptionsOfBufferResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType: 'buffer'};
const isGotInstance = (value: any): value is Got => (
Reflect.has(value, 'defaults') && Reflect.has(value.defaults, 'options')
);

export type OptionsOfDefaultResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false; responseType?: 'default'}>;
type OptionsOfTextResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false; responseType: 'text'}>;
type OptionsOfJSONResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false; responseType: 'json'}>;
type OptionsOfBufferResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false; responseType: 'buffer'}>;
type ResponseBodyOnly = {resolveBodyOnly: true};

interface GotFunctions {
// `asPromise` usage
(url: string | OptionsOfDefaultResponseBody, options?: OptionsOfDefaultResponseBody): CancelableRequest<Response>;
<T = string>(url: string | OptionsOfDefaultResponseBody, options?: OptionsOfDefaultResponseBody): CancelableRequest<Response<T>>;
(url: string | OptionsOfTextResponseBody, options?: OptionsOfTextResponseBody): CancelableRequest<Response<string>>;
(url: string | OptionsOfJSONResponseBody, options?: OptionsOfJSONResponseBody): CancelableRequest<Response<object>>;
<T>(url: string | OptionsOfJSONResponseBody, options?: OptionsOfJSONResponseBody): CancelableRequest<Response<T>>;
(url: string | OptionsOfBufferResponseBody, options?: OptionsOfBufferResponseBody): CancelableRequest<Response<Buffer>>;

(url: string | OptionsOfDefaultResponseBody & ResponseBodyOnly, options?: OptionsOfDefaultResponseBody & ResponseBodyOnly): CancelableRequest<any>;
(url: string | OptionsOfTextResponseBody & ResponseBodyOnly, options?: OptionsOfTextResponseBody & ResponseBodyOnly): CancelableRequest<string>;
(url: string | OptionsOfJSONResponseBody & ResponseBodyOnly, options?: OptionsOfJSONResponseBody & ResponseBodyOnly): CancelableRequest<object>;
(url: string | OptionsOfBufferResponseBody & ResponseBodyOnly, options?: OptionsOfBufferResponseBody & ResponseBodyOnly): CancelableRequest<Buffer>;

// `resolveBodyOnly` usage
<T = string>(url: string | Merge<OptionsOfDefaultResponseBody, ResponseBodyOnly>, options?: Merge<OptionsOfDefaultResponseBody, ResponseBodyOnly>): CancelableRequest<T>;
(url: string | Merge<OptionsOfTextResponseBody, ResponseBodyOnly>, options?: Merge<OptionsOfTextResponseBody, ResponseBodyOnly>): CancelableRequest<string>;
<T>(url: string | Merge<OptionsOfJSONResponseBody, ResponseBodyOnly>, options?: Merge<OptionsOfJSONResponseBody, ResponseBodyOnly>): CancelableRequest<T>;
(url: string | Merge<OptionsOfBufferResponseBody, ResponseBodyOnly>, options?: Merge<OptionsOfBufferResponseBody, ResponseBodyOnly>): CancelableRequest<Buffer>;
// `asStream` usage
(url: string | Options & {isStream: true}, options?: Options & {isStream: true}): ProxyStream;
<T>(url: string | Merge<Options, {isStream: true}>, options?: Merge<Options, {isStream: true}>): ProxyStream<T>;
}

export interface Got extends Merge<Record<HTTPAlias, GotFunctions>, GotFunctions> {
export interface Got extends Record<HTTPAlias, GotFunctions>, GotFunctions {
stream: GotStream;
defaults: Defaults | Readonly<Defaults>;
defaults: NormalizedDefaults | Readonly<NormalizedDefaults>;
GotError: typeof errors.GotError;
CacheError: typeof errors.CacheError;
RequestError: typeof errors.RequestError;
Expand All @@ -65,9 +67,9 @@ export interface Got extends Merge<Record<HTTPAlias, GotFunctions>, GotFunctions
TimeoutError: typeof errors.TimeoutError;
CancelError: typeof errors.CancelError;

extend(...instancesOrOptions: Array<Got | ExtendedOptions>): Got;
extend(...instancesOrOptions: Array<Got | ExtendOptions>): Got;
mergeInstances(parent: Got, ...instances: Got[]): Got;
mergeOptions<T extends Options>(...sources: T[]): T & {hooks: Partial<Hooks>};
mergeOptions<T extends PartialDeep<Options>>(...sources: T[]): T;
}

export interface GotStream extends Record<HTTPAlias, ReturnStream> {
Expand All @@ -85,11 +87,12 @@ const aliases: readonly HTTPAlias[] = [

export const defaultHandler: HandlerFunction = (options, next) => next(options);

const create = (defaults: Defaults): Got => {
const create = (defaults: NormalizedDefaults & {_rawHandlers?: HandlerFunction[]}): Got => {
// Proxy properties from next handlers
defaults._rawHandlers = defaults.handlers;
defaults.handlers = defaults.handlers.map(fn => ((options, next) => {
let root: GotReturn;
// This will be assigned by assigning result
let root!: GotReturn;

const result = fn(options, newOptions => {
root = next(newOptions);
Expand All @@ -107,7 +110,7 @@ const create = (defaults: Defaults): Got => {
// @ts-ignore Because the for loop handles it for us, as well as the other Object.defines
const got: Got = (url: URLOrOptions, options?: Options): GotReturn => {
let iteration = 0;
const iterateHandlers: HandlerFunction = newOptions => {
const iterateHandlers = (newOptions: Parameters<HandlerFunction>[0]): ReturnType<HandlerFunction> => {
return defaults.handlers[iteration++](
newOptions,
// @ts-ignore TS doesn't know that it calls `getPromiseOrStream` at the end
Expand All @@ -116,7 +119,6 @@ const create = (defaults: Defaults): Got => {
};

try {
// @ts-ignore This handler takes only one parameter.
return iterateHandlers(normalizeArguments(url, options, defaults));
} catch (error) {
if (options?.isStream) {
Expand All @@ -130,24 +132,22 @@ const create = (defaults: Defaults): Got => {

got.extend = (...instancesOrOptions) => {
const optionsArray: Options[] = [defaults.options];
let handlers: HandlerFunction[] = [...defaults._rawHandlers];
let mutableDefaults: boolean;
let handlers: HandlerFunction[] = [...defaults._rawHandlers!];
let mutableDefaults: boolean | undefined;

for (const value of instancesOrOptions) {
if (Reflect.has(value, 'defaults')) {
optionsArray.push((value as Got).defaults.options);

handlers.push(...(value as Got).defaults._rawHandlers);

mutableDefaults = (value as Got).defaults.mutableDefaults;
if (isGotInstance(value)) {
optionsArray.push(value.defaults.options);
handlers.push(...value.defaults._rawHandlers!);
mutableDefaults = value.defaults.mutableDefaults;
} else {
optionsArray.push(value as ExtendedOptions);
optionsArray.push(value);

if (Reflect.has(value, 'handlers')) {
handlers.push(...(value as ExtendedOptions).handlers);
handlers.push(...value.handlers);
}

mutableDefaults = (value as ExtendedOptions).mutableDefaults;
mutableDefaults = value.mutableDefaults;
}
}

Expand All @@ -160,7 +160,7 @@ const create = (defaults: Defaults): Got => {
return create({
options: mergeOptions(...optionsArray),
handlers,
mutableDefaults
mutableDefaults: Boolean(mutableDefaults)

This comment has been minimized.

Copy link
@szmarczak

szmarczak Nov 25, 2019

Collaborator

Actually Boolean is not needed. We can set mutableDefaults to false first.

});
};

Expand Down

0 comments on commit c537dee

Please sign in to comment.