Files
react/packages/react-fetch/src/ReactFetchNode.js
T
Richard MaisanoandDan Abramov c512aa0081 [Blocks] Scaffolding react-fetch + first pass at node implementation (#18863)
* First pass at scaffolding out the Node implementation of react-data.

While incomplete, this patch contains some changes to the react-data
package in order to start adding support for Node.

The first part of this change accounts for splitting react-data/fetch
into two discrete entries, adding (and defaulting to) the Node
implementation.

The second part is sketching out a rough approximation of `fetch` for
Node. This implementation is not complete by any means, but provides a
starting point.

* Remove NodeFetch module and put it directly into ReactDataFetchNode.

* Replaced react-data with react-fetch.

This patch shuffles around some of the scaffolding that was in
react-data in favor of react-fetch. It also removes the additional
"fetch" package in favor of something flatter.

* Tweak package organization

* Simplify and add a test

Co-authored-by: Dan Abramov <dan.abramov@me.com>
2020-05-12 17:21:45 +01:00

230 lines
5.2 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
import * as http from 'http';
import * as https from 'https';
import {readCache} from 'react/unstable-cache';
type FetchResponse = {|
// Properties
headers: any,
ok: boolean,
redirected: boolean,
status: number,
statusText: string,
type: 'basic',
url: string,
// Methods
arrayBuffer(): ArrayBuffer,
blob(): any,
json(): any,
text(): string,
|};
function nodeFetch(
url: string,
options: mixed,
onResolve: any => void,
onReject: any => void,
): void {
const {hostname, pathname, search, port, protocol} = new URL(url);
const nodeOptions = {
hostname,
port,
path: pathname + search,
// TODO: cherry-pick supported user-passed options.
};
const nodeImpl = protocol === 'https:' ? https : http;
const request = nodeImpl.request(nodeOptions, response => {
// TODO: support redirects.
onResolve(new Response(response));
});
request.on('error', error => {
onReject(error);
});
request.end();
}
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingResult = {|
status: 0,
value: Wakeable,
|};
type ResolvedResult<V> = {|
status: 1,
value: V,
|};
type RejectedResult = {|
status: 2,
value: mixed,
|};
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
const fetchKey = {};
function readResultMap(): Map<string, Result<FetchResponse>> {
const resources = readCache().resources;
let map = resources.get(fetchKey);
if (map === undefined) {
map = new Map();
resources.set(fetchKey, map);
}
return map;
}
function readResult<T>(result: Result<T>): T {
if (result.status === Resolved) {
return result.value;
} else {
throw result.value;
}
}
function Response(nativeResponse) {
this.headers = nativeResponse.headers;
this.ok = nativeResponse.statusCode >= 200 && nativeResponse.statusCode < 300;
this.redirected = false; // TODO
this.status = nativeResponse.statusCode;
this.statusText = nativeResponse.statusMessage;
this.type = 'basic';
this.url = nativeResponse.url;
this._response = nativeResponse;
this._blob = null;
this._json = null;
this._text = null;
const callbacks = [];
function wake() {
// This assumes they won't throw.
while (callbacks.length > 0) {
const cb = callbacks.pop();
cb();
}
}
const result: PendingResult = (this._result = {
status: Pending,
value: {
then(cb) {
callbacks.push(cb);
},
},
});
const data = [];
nativeResponse.on('data', chunk => data.push(chunk));
nativeResponse.on('end', () => {
if (result.status === Pending) {
const resolvedResult = ((result: any): ResolvedResult<Buffer>);
resolvedResult.status = Resolved;
resolvedResult.value = Buffer.concat(data);
wake();
}
});
nativeResponse.on('error', err => {
if (result.status === Pending) {
const rejectedResult = ((result: any): RejectedResult);
rejectedResult.status = Rejected;
rejectedResult.value = err;
wake();
}
});
}
Response.prototype = {
constructor: Response,
arrayBuffer() {
const buffer = readResult(this._result);
return buffer;
},
blob() {
// TODO: Is this needed?
throw new Error('Not implemented.');
},
json() {
const buffer = readResult(this._result);
return JSON.parse(buffer.toString());
},
text() {
const buffer = readResult(this._result);
return buffer.toString();
},
};
function preloadResult(url: string, options: mixed): Result<FetchResponse> {
const map = readResultMap();
let entry = map.get(url);
if (!entry) {
if (options) {
if (options.method || options.body || options.signal) {
// TODO: wire up our own cancellation mechanism.
// TODO: figure out what to do with POST.
throw Error('Unsupported option');
}
}
const callbacks = [];
const wakeable = {
then(cb) {
callbacks.push(cb);
},
};
const wake = () => {
// This assumes they won't throw.
while (callbacks.length > 0) {
const cb = callbacks.pop();
cb();
}
};
const result: Result<FetchResponse> = (entry = {
status: Pending,
value: wakeable,
});
nodeFetch(
url,
options,
response => {
if (result.status === Pending) {
const resolvedResult = ((result: any): ResolvedResult<FetchResponse>);
resolvedResult.status = Resolved;
resolvedResult.value = response;
wake();
}
},
err => {
if (result.status === Pending) {
const rejectedResult = ((result: any): RejectedResult);
rejectedResult.status = Rejected;
rejectedResult.value = err;
wake();
}
},
);
map.set(url, entry);
}
return entry;
}
export function preload(url: string, options: mixed): void {
preloadResult(url, options);
// Don't return anything.
}
export function fetch(url: string, options: mixed): FetchResponse {
const result = preloadResult(url, options);
return readResult(result);
}