* change(android): added android support * change(git): Remove .yarn from version control * change(git): Remove .yarn from version control * change(rn-tracker): android view fixes * change(tracker): yarn * fix rn: fix ios config for react native * ios source changes * change(lib): tracker manager * change(lib): layout fixes * change(lib): layout fixes * fix default api endp --------- Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
335 lines
9.7 KiB
TypeScript
335 lines
9.7 KiB
TypeScript
// @ts-nocheck
|
|
/**
|
|
* I took inspiration in few stack exchange posts
|
|
* and Tencent vConsole library (MIT)
|
|
* by wrapping the XMLHttpRequest object in a Proxy
|
|
* we can intercept the network requests
|
|
* in not-so-hacky way
|
|
* */
|
|
import NetworkMessage, { RequestState } from './networkMessage';
|
|
import {
|
|
formatByteSize,
|
|
genStringBody,
|
|
getStringResponseByType,
|
|
getURL,
|
|
} from './utils';
|
|
import { RequestResponseData } from './types';
|
|
|
|
export class ResponseProxyHandler<T extends Response>
|
|
implements ProxyHandler<T>
|
|
{
|
|
public resp: Response;
|
|
public item: NetworkMessage;
|
|
|
|
constructor(resp: T, item: NetworkMessage) {
|
|
this.resp = resp;
|
|
this.item = item;
|
|
this.mockReader();
|
|
}
|
|
|
|
public set(target: T, key: string, value: (args: any[]) => any) {
|
|
return Reflect.set(target, key, value);
|
|
}
|
|
|
|
public get(target: T, key: string) {
|
|
const value = Reflect.get(target, key);
|
|
switch (key) {
|
|
case 'arrayBuffer':
|
|
case 'blob':
|
|
case 'formData':
|
|
case 'json':
|
|
case 'text':
|
|
return () => {
|
|
this.item.responseType = <any>key.toLowerCase();
|
|
// @ts-ignore
|
|
return value.apply(target).then((resp: any) => {
|
|
this.item.response = getStringResponseByType(
|
|
this.item.responseType,
|
|
resp
|
|
);
|
|
return resp;
|
|
});
|
|
};
|
|
}
|
|
if (typeof value === 'function') {
|
|
return value.bind(target);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
protected mockReader() {
|
|
let readerReceivedValue: Uint8Array;
|
|
if (!this.resp.body) {
|
|
// some browsers do not return `body` in some cases, like `OPTIONS` method
|
|
return;
|
|
}
|
|
if (typeof this.resp.body.getReader !== 'function') {
|
|
return;
|
|
}
|
|
const _getReader = this.resp.body.getReader;
|
|
// @ts-ignore
|
|
this.resp.body.getReader = () => {
|
|
const reader = <ReturnType<typeof _getReader>>(
|
|
_getReader.apply(this.resp.body)
|
|
);
|
|
|
|
// when readyState is already 4,
|
|
// it's not a chunked stream, or it had already been read.
|
|
// so should not update status.
|
|
if (this.item.readyState === RequestState.DONE) {
|
|
return reader;
|
|
}
|
|
|
|
const _read = reader.read;
|
|
const _cancel = reader.cancel;
|
|
this.item.responseType = 'arraybuffer';
|
|
|
|
// @ts-ignore
|
|
reader.read = () => {
|
|
return (<ReturnType<typeof _read>>_read.apply(reader)).then(
|
|
(result: any) => {
|
|
if (!readerReceivedValue) {
|
|
// @ts-ignore
|
|
readerReceivedValue = new Uint8Array(result.value);
|
|
} else {
|
|
const newValue = new Uint8Array(
|
|
readerReceivedValue.length + result.value!.length
|
|
);
|
|
newValue.set(readerReceivedValue);
|
|
|
|
newValue.set(result.value!, readerReceivedValue.length);
|
|
readerReceivedValue = newValue;
|
|
}
|
|
this.item.endTime = performance.now();
|
|
this.item.duration =
|
|
this.item.endTime - (this.item.startTime || this.item.endTime);
|
|
this.item.readyState = result.done ? 4 : 3;
|
|
this.item.statusText = result.done
|
|
? String(this.item.status)
|
|
: 'Loading';
|
|
this.item.responseSize = readerReceivedValue.length;
|
|
this.item.responseSizeText = formatByteSize(this.item.responseSize);
|
|
if (result.done) {
|
|
this.item.response = getStringResponseByType(
|
|
this.item.responseType,
|
|
readerReceivedValue
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
);
|
|
};
|
|
|
|
reader.cancel = (...args) => {
|
|
this.item.cancelState = 2;
|
|
this.item.statusText = 'Cancel';
|
|
this.item.endTime = performance.now();
|
|
this.item.duration =
|
|
this.item.endTime - (this.item.startTime || this.item.endTime);
|
|
this.item.response = getStringResponseByType(
|
|
this.item.responseType,
|
|
readerReceivedValue
|
|
);
|
|
return _cancel.apply(reader, args);
|
|
};
|
|
return reader;
|
|
};
|
|
}
|
|
}
|
|
|
|
export class FetchProxyHandler<T extends typeof fetch>
|
|
implements ProxyHandler<T>
|
|
{
|
|
constructor(
|
|
private readonly ignoredHeaders: boolean | string[],
|
|
private readonly sanitize: (
|
|
data: RequestResponseData
|
|
) => RequestResponseData,
|
|
private readonly sendMessage: (item: any) => void,
|
|
private readonly isServiceUrl: (url: string) => boolean
|
|
) {}
|
|
|
|
public apply(
|
|
target: T,
|
|
_: typeof global,
|
|
argsList: [RequestInfo | URL, RequestInit]
|
|
) {
|
|
const input = argsList[0];
|
|
const init = argsList[1];
|
|
if (
|
|
!input ||
|
|
// @ts-ignore
|
|
(typeof input !== 'string' && !input?.url)
|
|
) {
|
|
return <ReturnType<T>>target.apply(global, argsList);
|
|
}
|
|
|
|
const isORUrl =
|
|
input instanceof URL || typeof input === 'string'
|
|
? this.isServiceUrl(String(input))
|
|
: this.isServiceUrl(String(input.url));
|
|
|
|
if (isORUrl) {
|
|
return target.apply(global, argsList);
|
|
}
|
|
|
|
const item = new NetworkMessage(this.ignoredHeaders, this.sanitize);
|
|
this.beforeFetch(item, input as RequestInfo, init);
|
|
|
|
return (<ReturnType<T>>target.apply(global, argsList))
|
|
.then(this.afterFetch(item))
|
|
.catch((e) => {
|
|
// mock finally
|
|
item.endTime = performance.now();
|
|
item.duration = item.endTime - (item.startTime || item.endTime);
|
|
throw e;
|
|
});
|
|
}
|
|
|
|
protected beforeFetch(
|
|
item: NetworkMessage,
|
|
input: RequestInfo | string,
|
|
init?: RequestInit
|
|
) {
|
|
let url: URL,
|
|
method = 'GET',
|
|
requestHeader: any = {};
|
|
|
|
// handle `input` content
|
|
if (typeof input === 'string') {
|
|
// when `input` is a string
|
|
method = init?.method || 'GET';
|
|
url = getURL(input);
|
|
requestHeader = init?.headers || {};
|
|
} else {
|
|
// when `input` is a `Request` object
|
|
method = input.method || 'GET';
|
|
url = getURL(input.url);
|
|
requestHeader = input.headers;
|
|
}
|
|
|
|
item.method = <NetworkMessage['method']>method;
|
|
item.requestType = 'fetch';
|
|
item.requestHeader = requestHeader;
|
|
item.url = url.toString();
|
|
item.name = (item.url.split('/')[3] || '') + item.url.split('?')[1];
|
|
item.status = 0;
|
|
item.statusText = 'Pending';
|
|
item.readyState = 1;
|
|
if (!item.startTime) {
|
|
// UNSENT
|
|
item.startTime = performance.now();
|
|
}
|
|
|
|
if (Object.prototype.toString.call(requestHeader) === '[object Headers]') {
|
|
item.requestHeader = {};
|
|
for (const [key, value] of <Headers>requestHeader) {
|
|
item.requestHeader[key] = value;
|
|
}
|
|
} else {
|
|
item.requestHeader = requestHeader;
|
|
}
|
|
|
|
// save GET data
|
|
const search = url.toString().split('?')[1];
|
|
const searchParams = new URLSearchParams(search);
|
|
if (search && searchParams) {
|
|
item.getData = {};
|
|
for (const [key, value] of searchParams) {
|
|
item.getData[key] = value;
|
|
}
|
|
}
|
|
|
|
// save POST data
|
|
if (init?.body) {
|
|
item.requestData = genStringBody(init.body);
|
|
}
|
|
}
|
|
|
|
protected afterFetch(item: NetworkMessage) {
|
|
return (resp: Response) => {
|
|
item.endTime = performance.now();
|
|
item.duration = item.endTime - (item.startTime || item.endTime);
|
|
item.status = resp.status;
|
|
item.statusText = String(resp.status);
|
|
|
|
let isChunked = false;
|
|
item.header = {};
|
|
for (const [key, value] of resp.headers) {
|
|
item.header[key] = value;
|
|
isChunked =
|
|
value.toLowerCase().indexOf('chunked') > -1 ? true : isChunked;
|
|
}
|
|
|
|
if (isChunked) {
|
|
// when `transfer-encoding` is chunked, the response is a stream which is under loading,
|
|
// so the `readyState` should be 3 (Loading),
|
|
// and the response should NOT be `clone()` which will affect stream reading.
|
|
item.readyState = 3;
|
|
} else {
|
|
// Otherwise, not chunked, the response is not a stream,
|
|
// so it's completed and can be cloned for `text()` calling.
|
|
item.readyState = 4;
|
|
|
|
this.handleResponseBody(resp.clone(), item).then(
|
|
(responseValue: string | ArrayBuffer) => {
|
|
item.responseSize =
|
|
typeof responseValue === 'string'
|
|
? responseValue.length
|
|
: responseValue.byteLength;
|
|
item.responseSizeText = formatByteSize(item.responseSize);
|
|
item.response = getStringResponseByType(
|
|
item.responseType,
|
|
responseValue
|
|
);
|
|
|
|
const msg = item.getMessage();
|
|
this.sendMessage(msg[0], msg[1], msg[2], msg[3], msg[4], msg[5]);
|
|
}
|
|
);
|
|
}
|
|
|
|
return new Proxy(resp, new ResponseProxyHandler(resp, item));
|
|
};
|
|
}
|
|
|
|
protected handleResponseBody(resp: Response, item: NetworkMessage) {
|
|
// parse response body by Content-Type
|
|
const contentType = resp.headers.get('content-type');
|
|
if (contentType && contentType.includes('application/json')) {
|
|
item.responseType = 'json';
|
|
return resp.text();
|
|
} else if (
|
|
contentType &&
|
|
(contentType.includes('text/html') || contentType.includes('text/plain'))
|
|
) {
|
|
item.responseType = 'text';
|
|
return resp.text();
|
|
} else {
|
|
item.responseType = 'arraybuffer';
|
|
return resp.arrayBuffer();
|
|
}
|
|
}
|
|
}
|
|
|
|
export default class FetchProxy {
|
|
public static create(
|
|
ignoredHeaders: boolean | string[],
|
|
sanitize: (data: RequestResponseData) => RequestResponseData,
|
|
sendMessage: (item: any) => void,
|
|
isServiceUrl: (url: string) => boolean,
|
|
tokenUrlMatcher?: (url: string) => boolean
|
|
) {
|
|
return new Proxy(
|
|
fetch,
|
|
new FetchProxyHandler(
|
|
ignoredHeaders,
|
|
sanitize,
|
|
sendMessage,
|
|
isServiceUrl,
|
|
tokenUrlMatcher
|
|
)
|
|
);
|
|
}
|
|
}
|