refactor: tsify net module (#23618)

This commit is contained in:
Jeremy Apthorp 2020-05-18 10:22:48 -07:00 committed by GitHub
parent 8879a3db58
commit 7e841ceb5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 54 deletions

View file

@ -206,7 +206,7 @@ auto_filenames = {
"lib/browser/api/module-list.ts", "lib/browser/api/module-list.ts",
"lib/browser/api/native-theme.ts", "lib/browser/api/native-theme.ts",
"lib/browser/api/net-log.js", "lib/browser/api/net-log.js",
"lib/browser/api/net.js", "lib/browser/api/net.ts",
"lib/browser/api/notification.js", "lib/browser/api/notification.js",
"lib/browser/api/power-monitor.ts", "lib/browser/api/power-monitor.ts",
"lib/browser/api/power-save-blocker.js", "lib/browser/api/power-save-blocker.js",

View file

@ -1,9 +1,7 @@
'use strict'; import * as url from 'url';
import { Readable, Writable } from 'stream';
const url = require('url'); import { app } from 'electron';
const { EventEmitter } = require('events'); import { ClientRequestConstructorOptions, UploadProgress } from 'electron/main';
const { Readable, Writable } = require('stream');
const { app } = require('electron');
const { Session } = process.electronBinding('session'); const { Session } = process.electronBinding('session');
const { net, Net, isValidHeaderName, isValidHeaderValue, createURLLoader } = process.electronBinding('net'); const { net, Net, isValidHeaderName, isValidHeaderValue, createURLLoader } = process.electronBinding('net');
@ -33,7 +31,11 @@ const discardableDuplicateHeaders = new Set([
]); ]);
class IncomingMessage extends Readable { class IncomingMessage extends Readable {
constructor (responseHead) { _shouldPush: boolean;
_data: (Buffer | null)[];
_responseHead: NodeJS.ResponseHead;
constructor (responseHead: NodeJS.ResponseHead) {
super(); super();
this._shouldPush = false; this._shouldPush = false;
this._data = []; this._data = [];
@ -49,7 +51,7 @@ class IncomingMessage extends Readable {
} }
get headers () { get headers () {
const filteredHeaders = {}; const filteredHeaders: Record<string, string | string[]> = {};
const { rawHeaders } = this._responseHead; const { rawHeaders } = this._responseHead;
rawHeaders.forEach(header => { rawHeaders.forEach(header => {
if (Object.prototype.hasOwnProperty.call(filteredHeaders, header.key) && if (Object.prototype.hasOwnProperty.call(filteredHeaders, header.key) &&
@ -60,7 +62,7 @@ class IncomingMessage extends Readable {
// keep set-cookie as an array per Node.js rules // keep set-cookie as an array per Node.js rules
// see https://nodejs.org/api/http.html#http_message_headers // see https://nodejs.org/api/http.html#http_message_headers
if (Object.prototype.hasOwnProperty.call(filteredHeaders, header.key)) { if (Object.prototype.hasOwnProperty.call(filteredHeaders, header.key)) {
filteredHeaders[header.key].push(header.value); (filteredHeaders[header.key] as string[]).push(header.value);
} else { } else {
filteredHeaders[header.key] = [header.value]; filteredHeaders[header.key] = [header.value];
} }
@ -97,7 +99,7 @@ class IncomingMessage extends Readable {
throw new Error('HTTP trailers are not supported'); throw new Error('HTTP trailers are not supported');
} }
_storeInternalData (chunk) { _storeInternalData (chunk: Buffer | null) {
this._data.push(chunk); this._data.push(chunk);
this._pushInternalData(); this._pushInternalData();
} }
@ -117,12 +119,13 @@ class IncomingMessage extends Readable {
/** Writable stream that buffers up everything written to it. */ /** Writable stream that buffers up everything written to it. */
class SlurpStream extends Writable { class SlurpStream extends Writable {
_data: Buffer;
constructor () { constructor () {
super(); super();
this._data = Buffer.alloc(0); this._data = Buffer.alloc(0);
} }
_write (chunk, encoding, callback) { _write (chunk: Buffer, encoding: string, callback: () => void) {
this._data = Buffer.concat([this._data, chunk]); this._data = Buffer.concat([this._data, chunk]);
callback(); callback();
} }
@ -130,12 +133,17 @@ class SlurpStream extends Writable {
} }
class ChunkedBodyStream extends Writable { class ChunkedBodyStream extends Writable {
constructor (clientRequest) { _pendingChunk: Buffer | undefined;
_downstream?: NodeJS.DataPipe;
_pendingCallback?: (error?: Error) => void;
_clientRequest: ClientRequest;
constructor (clientRequest: ClientRequest) {
super(); super();
this._clientRequest = clientRequest; this._clientRequest = clientRequest;
} }
_write (chunk, encoding, callback) { _write (chunk: Buffer, encoding: string, callback: () => void) {
if (this._downstream) { if (this._downstream) {
this._downstream.write(chunk).then(callback, callback); this._downstream.write(chunk).then(callback, callback);
} else { } else {
@ -149,40 +157,37 @@ class ChunkedBodyStream extends Writable {
} }
} }
_final (callback) { _final (callback: () => void) {
this._downstream.done(); this._downstream!.done();
callback(); callback();
} }
startReading (pipe) { startReading (pipe: NodeJS.DataPipe) {
if (this._downstream) { if (this._downstream) {
throw new Error('two startReading calls???'); throw new Error('two startReading calls???');
} }
this._downstream = pipe; this._downstream = pipe;
if (this._pendingChunk) { if (this._pendingChunk) {
const doneWriting = (maybeError) => { const doneWriting = (maybeError: Error | void) => {
const cb = this._pendingCallback; const cb = this._pendingCallback!;
delete this._pendingCallback; delete this._pendingCallback;
delete this._pendingChunk; delete this._pendingChunk;
cb(maybeError); cb(maybeError || undefined);
}; };
this._downstream.write(this._pendingChunk).then(doneWriting, doneWriting); this._downstream.write(this._pendingChunk).then(doneWriting, doneWriting);
} }
} }
} }
function parseOptions (options) { type RedirectPolicy = 'manual' | 'follow' | 'error';
if (typeof options === 'string') {
options = url.parse(options);
} else {
options = { ...options };
}
const method = (options.method || 'GET').toUpperCase(); function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, extraHeaders: Record<string, string> } {
let urlStr = options.url; const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
let urlStr: string = options.url;
if (!urlStr) { if (!urlStr) {
const urlObj = {}; const urlObj: url.UrlObject = {};
const protocol = options.protocol || 'http:'; const protocol = options.protocol || 'http:';
if (!kSupportedProtocols.has(protocol)) { if (!kSupportedProtocols.has(protocol)) {
throw new Error('Protocol "' + protocol + '" not supported'); throw new Error('Protocol "' + protocol + '" not supported');
@ -228,14 +233,15 @@ function parseOptions (options) {
throw new TypeError('headers must be an object'); throw new TypeError('headers must be an object');
} }
const urlLoaderOptions = { const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, extraHeaders: Record<string, string | string[]> } = {
method: method, method: (options.method || 'GET').toUpperCase(),
url: urlStr, url: urlStr,
redirectPolicy, redirectPolicy,
extraHeaders: options.headers || {}, extraHeaders: options.headers || {},
body: null as any,
useSessionCookies: options.useSessionCookies || false useSessionCookies: options.useSessionCookies || false
}; };
for (const [name, value] of Object.entries(urlLoaderOptions.extraHeaders)) { for (const [name, value] of Object.entries(urlLoaderOptions.extraHeaders!)) {
if (!isValidHeaderName(name)) { if (!isValidHeaderName(name)) {
throw new Error(`Invalid header name: '${name}'`); throw new Error(`Invalid header name: '${name}'`);
} }
@ -259,8 +265,20 @@ function parseOptions (options) {
return urlLoaderOptions; return urlLoaderOptions;
} }
class ClientRequest extends Writable { class ClientRequest extends Writable implements Electron.ClientRequest {
constructor (options, callback) { _started: boolean = false;
_firstWrite: boolean = false;
_aborted: boolean = false;
_chunkedEncoding: boolean | undefined;
_body: Writable | undefined;
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { extraHeaders: Record<string, string> };
_redirectPolicy: RedirectPolicy;
_followRedirectCb?: () => void;
_uploadProgress?: { active: boolean, started: boolean, current: number, total: number };
_urlLoader?: NodeJS.URLLoader;
_response?: IncomingMessage;
constructor (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
super({ autoDestroy: true }); super({ autoDestroy: true });
if (!app.isReady()) { if (!app.isReady()) {
@ -274,10 +292,9 @@ class ClientRequest extends Writable {
const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options); const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options);
this._urlLoaderOptions = urlLoaderOptions; this._urlLoaderOptions = urlLoaderOptions;
this._redirectPolicy = redirectPolicy; this._redirectPolicy = redirectPolicy;
this._started = false;
} }
set chunkedEncoding (value) { set chunkedEncoding (value: boolean) {
if (this._started) { if (this._started) {
throw new Error('chunkedEncoding can only be set before the request is started'); throw new Error('chunkedEncoding can only be set before the request is started');
} }
@ -287,13 +304,13 @@ class ClientRequest extends Writable {
this._chunkedEncoding = !!value; this._chunkedEncoding = !!value;
if (this._chunkedEncoding) { if (this._chunkedEncoding) {
this._body = new ChunkedBodyStream(this); this._body = new ChunkedBodyStream(this);
this._urlLoaderOptions.body = (pipe) => { this._urlLoaderOptions.body = (pipe: NodeJS.DataPipe) => {
this._body.startReading(pipe); (this._body! as ChunkedBodyStream).startReading(pipe);
}; };
} }
} }
setHeader (name, value) { setHeader (name: string, value: string) {
if (typeof name !== 'string') { if (typeof name !== 'string') {
throw new TypeError('`name` should be a string in setHeader(name, value)'); throw new TypeError('`name` should be a string in setHeader(name, value)');
} }
@ -314,7 +331,7 @@ class ClientRequest extends Writable {
this._urlLoaderOptions.extraHeaders[key] = value; this._urlLoaderOptions.extraHeaders[key] = value;
} }
getHeader (name) { getHeader (name: string) {
if (name == null) { if (name == null) {
throw new Error('`name` is required for getHeader(name)'); throw new Error('`name` is required for getHeader(name)');
} }
@ -323,7 +340,7 @@ class ClientRequest extends Writable {
return this._urlLoaderOptions.extraHeaders[key]; return this._urlLoaderOptions.extraHeaders[key];
} }
removeHeader (name) { removeHeader (name: string) {
if (name == null) { if (name == null) {
throw new Error('`name` is required for removeHeader(name)'); throw new Error('`name` is required for removeHeader(name)');
} }
@ -336,12 +353,12 @@ class ClientRequest extends Writable {
delete this._urlLoaderOptions.extraHeaders[key]; delete this._urlLoaderOptions.extraHeaders[key];
} }
_write (chunk, encoding, callback) { _write (chunk: Buffer, encoding: string, callback: () => void) {
this._firstWrite = true; this._firstWrite = true;
if (!this._body) { if (!this._body) {
this._body = new SlurpStream(); this._body = new SlurpStream();
this._body.on('finish', () => { this._body.on('finish', () => {
this._urlLoaderOptions.body = this._body.data(); this._urlLoaderOptions.body = (this._body as SlurpStream).data();
this._startRequest(); this._startRequest();
}); });
} }
@ -349,7 +366,7 @@ class ClientRequest extends Writable {
this._body.write(chunk, encoding, callback); this._body.write(chunk, encoding, callback);
} }
_final (callback) { _final (callback: () => void) {
if (this._body) { if (this._body) {
// TODO: is this the right way to forward to another stream? // TODO: is this the right way to forward to another stream?
this._body.end(callback); this._body.end(callback);
@ -362,8 +379,8 @@ class ClientRequest extends Writable {
_startRequest () { _startRequest () {
this._started = true; this._started = true;
const stringifyValues = (obj) => { const stringifyValues = (obj: Record<string, any>) => {
const ret = {}; const ret: Record<string, string> = {};
for (const k of Object.keys(obj)) { for (const k of Object.keys(obj)) {
ret[k] = obj[k].toString(); ret[k] = obj[k].toString();
} }
@ -376,7 +393,7 @@ class ClientRequest extends Writable {
this.emit('response', response); this.emit('response', response);
}); });
this._urlLoader.on('data', (event, data) => { this._urlLoader.on('data', (event, data) => {
this._response._storeInternalData(Buffer.from(data)); this._response!._storeInternalData(Buffer.from(data));
}); });
this._urlLoader.on('complete', () => { this._urlLoader.on('complete', () => {
if (this._response) { this._response._storeInternalData(null); } if (this._response) { this._response._storeInternalData(null); }
@ -405,7 +422,7 @@ class ClientRequest extends Writable {
try { try {
this.emit('redirect', statusCode, newMethod, newUrl, headers); this.emit('redirect', statusCode, newMethod, newUrl, headers);
} finally { } finally {
this._followRedirectCb = null; this._followRedirectCb = undefined;
if (!_followRedirect && !this._aborted) { if (!_followRedirect && !this._aborted) {
this._die(new Error('Redirect was cancelled')); this._die(new Error('Redirect was cancelled'));
} }
@ -418,7 +435,7 @@ class ClientRequest extends Writable {
this._followRedirectCb = () => {}; this._followRedirectCb = () => {};
this.emit('redirect', statusCode, newMethod, newUrl, headers); this.emit('redirect', statusCode, newMethod, newUrl, headers);
} finally { } finally {
this._followRedirectCb = null; this._followRedirectCb = undefined;
} }
} else { } else {
this._die(new Error(`Unexpected redirect policy '${this._redirectPolicy}'`)); this._die(new Error(`Unexpected redirect policy '${this._redirectPolicy}'`));
@ -453,7 +470,7 @@ class ClientRequest extends Writable {
this._die(); this._die();
} }
_die (err) { _die (err?: Error) {
this.destroy(err); this.destroy(err);
if (this._urlLoader) { if (this._urlLoader) {
this._urlLoader.cancel(); this._urlLoader.cancel();
@ -461,12 +478,12 @@ class ClientRequest extends Writable {
} }
} }
getUploadProgress () { getUploadProgress (): UploadProgress {
return this._uploadProgress ? { ...this._uploadProgress } : { active: false }; return this._uploadProgress ? { ...this._uploadProgress } : { active: false, started: false, current: 0, total: 0 };
} }
} }
Net.prototype.request = function (options, callback) { Net.prototype.request = function (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
return new ClientRequest(options, callback); return new ClientRequest(options, callback);
}; };

View file

@ -1208,7 +1208,7 @@ describe('net module', () => {
response.end(); response.end();
}); });
const netRequest = net.request({ url: serverUrl, method: 'POST' }); const netRequest = net.request({ url: serverUrl, method: 'POST' });
expect(netRequest.getUploadProgress()).to.deep.equal({ active: false }); expect(netRequest.getUploadProgress()).to.have.property('active', false);
netRequest.end(Buffer.from('hello')); netRequest.end(Buffer.from('hello'));
const [position, total] = await emittedOnce(netRequest, 'upload-progress'); const [position, total] = await emittedOnce(netRequest, 'upload-progress');
expect(netRequest.getUploadProgress()).to.deep.equal({ active: true, started: true, current: position, total }); expect(netRequest.getUploadProgress()).to.deep.equal({ active: true, started: true, current: position, total });

View file

@ -45,6 +45,49 @@ declare namespace NodeJS {
getWeaklyTrackedValues(): any[]; getWeaklyTrackedValues(): any[];
} }
type DataPipe = {
write: (buf: Uint8Array) => Promise<void>;
done: () => void;
};
type BodyFunc = (pipe: DataPipe) => void;
type CreateURLLoaderOptions = {
method: string;
url: string;
extraHeaders?: Record<string, string>;
useSessionCookies?: boolean;
body: Uint8Array | BodyFunc;
session?: Electron.Session;
partition?: string;
}
type ResponseHead = {
statusCode: number;
statusMessage: string;
httpVersion: { major: number, minor: number };
rawHeaders: { key: string, value: string }[];
};
type RedirectInfo = {
statusCode: number;
newMethod: string;
newUrl: string;
newSiteForCookies: string;
newReferrer: string;
insecureSchemeWasUpgraded: boolean;
isSignedExchangeFallbackRedirect: boolean;
}
interface URLLoader extends EventEmitter {
cancel(): void;
on(eventName: 'data', listener: (event: any, data: ArrayBuffer) => void): this;
on(eventName: 'response-started', listener: (event: any, finalUrl: string, responseHead: ResponseHead) => void): this;
on(eventName: 'complete', listener: (event: any) => void): this;
on(eventName: 'error', listener: (event: any, netErrorString: string) => void): this;
on(eventName: 'login', listener: (event: any, authInfo: Electron.AuthInfo, callback: (username?: string, password?: string) => void) => void): this;
on(eventName: 'redirect', listener: (event: any, redirectInfo: RedirectInfo, headers: Record<string, string>) => void): this;
on(eventName: 'upload-progress', listener: (event: any, position: number, total: number) => void): this;
on(eventName: 'download-progress', listener: (event: any, current: number) => void): this;
}
interface Process { interface Process {
/** /**
* DO NOT USE DIRECTLY, USE process.electronBinding * DO NOT USE DIRECTLY, USE process.electronBinding
@ -57,6 +100,13 @@ declare namespace NodeJS {
electronBinding(name: 'app'): { app: Electron.App, App: Function }; electronBinding(name: 'app'): { app: Electron.App, App: Function };
electronBinding(name: 'command_line'): Electron.CommandLine; electronBinding(name: 'command_line'): Electron.CommandLine;
electronBinding(name: 'desktop_capturer'): { createDesktopCapturer(): ElectronInternal.DesktopCapturer }; electronBinding(name: 'desktop_capturer'): { createDesktopCapturer(): ElectronInternal.DesktopCapturer };
electronBinding(name: 'net'): {
isValidHeaderName: (headerName: string) => boolean;
isValidHeaderValue: (headerValue: string) => boolean;
Net: any;
net: any;
createURLLoader(options: CreateURLLoaderOptions): URLLoader;
};
log: NodeJS.WriteStream['write']; log: NodeJS.WriteStream['write'];
activateUvLoop(): void; activateUvLoop(): void;