Skip to content

Commit 67d00de

Browse files
committed
Simplify and add a test
1 parent 58c9329 commit 67d00de

File tree

2 files changed

+204
-185
lines changed

2 files changed

+204
-185
lines changed

packages/react-fetch/src/ReactFetchNode.js

Lines changed: 112 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,19 @@
99

1010
import type {Wakeable} from 'shared/ReactTypes';
1111

12+
import * as http from 'http';
1213
import * as https from 'https';
1314

1415
import {readCache} from 'react/unstable-cache';
1516

16-
type FetchOptions = {|
17-
method?: string,
18-
headers?: any,
19-
redirect?: 'follow' | 'manual' | 'error',
20-
// Not yet implemented
21-
signal?: any,
22-
body?: any,
23-
|};
24-
2517
type FetchResponse = {|
2618
// Properties
2719
headers: any,
2820
ok: boolean,
2921
redirected: boolean,
3022
status: number,
3123
statusText: string,
32-
type: 'basic' | 'opaqueredirect',
24+
type: 'basic',
3325
url: string,
3426
// Methods
3527
arrayBuffer(): ArrayBuffer,
@@ -38,131 +30,30 @@ type FetchResponse = {|
3830
text(): string,
3931
|};
4032

41-
const defaultOptions: FetchOptions = {
42-
method: 'GET',
43-
headers: {},
44-
redirect: 'follow',
45-
};
46-
47-
const assign = Object['as' + 'sign'];
48-
49-
function makeRequest(
33+
function nodeFetch(
5034
url: string,
51-
fetchOptions: FetchOptions,
52-
done: any => void,
53-
err: any => void,
54-
redirects: number = 0,
35+
options: mixed,
36+
onResolve: any => void,
37+
onReject: any => void,
5538
): void {
56-
const {hostname, pathname} = new URL(url);
57-
58-
const options = assign({}, defaultOptions, fetchOptions, {
39+
const {hostname, pathname, search, port, protocol} = new URL(url);
40+
const nodeOptions = {
5941
hostname,
60-
path: pathname,
61-
});
62-
63-
const request = https.request(options, response => {
64-
if (isRedirect(response.statusCode)) {
65-
if (options.redirect === 'error') {
66-
throw new Error('Failed to fetch');
67-
}
68-
69-
let nextUrl = new URL(response.headers.location, url);
70-
nextUrl = nextUrl.href;
71-
72-
if (options.redirect === 'manual') {
73-
createOpaqueRedirectResponse(url);
74-
} else {
75-
makeRequest(nextUrl, fetchOptions, done, err, redirects + 1);
76-
}
77-
78-
return;
79-
}
80-
81-
response.on('data', data => {
82-
done(createResponse(url, redirects > 0, response, data));
83-
});
42+
port,
43+
path: pathname + search,
44+
// TODO: cherry-pick supported user-passed options.
45+
};
46+
const nodeImpl = protocol === 'https:' ? https : http;
47+
const request = nodeImpl.request(nodeOptions, response => {
48+
// TODO: support redirects.
49+
onResolve(new Response(response));
8450
});
85-
8651
request.on('error', error => {
87-
err(error);
52+
onReject(error);
8853
});
89-
9054
request.end();
9155
}
9256

93-
function isRedirect(code: number): boolean {
94-
switch (code) {
95-
case 301:
96-
case 302:
97-
case 303:
98-
case 307:
99-
case 308:
100-
return true;
101-
default:
102-
return false;
103-
}
104-
}
105-
106-
function createOpaqueRedirectResponse(url: string): FetchResponse {
107-
return {
108-
headers: {},
109-
ok: false,
110-
redirected: false,
111-
status: 0,
112-
statusText: '',
113-
type: 'opaqueredirect',
114-
url,
115-
arrayBuffer() {
116-
throw new Error('TODO');
117-
},
118-
blob() {
119-
throw new Error('TODO');
120-
},
121-
json() {
122-
throw new Error('TODO');
123-
},
124-
text() {
125-
throw new Error('TODO');
126-
},
127-
};
128-
}
129-
130-
function createResponse(
131-
url: string,
132-
redirected: boolean,
133-
response: any,
134-
data: Buffer,
135-
): FetchResponse {
136-
return {
137-
headers: response.headers,
138-
ok: response.statusCode >= 200 && response.statusCode < 300,
139-
redirected,
140-
status: response.statusCode,
141-
statusText: response.statusMessage,
142-
type: 'basic',
143-
url,
144-
arrayBuffer() {
145-
return Uint8Array.from(data).buffer;
146-
},
147-
blob() {
148-
// TODO: Not sure how to handle this just yet.
149-
throw new Error('TODO');
150-
},
151-
json() {
152-
return JSON.parse(data.toString());
153-
},
154-
text() {
155-
return data.toString();
156-
},
157-
};
158-
}
159-
160-
function nodeFetch(url: string, options: FetchOptions): Promise<any> {
161-
return new Promise((resolve, reject) => {
162-
return makeRequest(url, options, resolve, reject);
163-
});
164-
}
165-
16657
const Pending = 0;
16758
const Resolved = 1;
16859
const Rejected = 2;
@@ -172,21 +63,21 @@ type PendingResult = {|
17263
value: Wakeable,
17364
|};
17465

175-
type ResolvedResult = {|
66+
type ResolvedResult<V> = {|
17667
status: 1,
177-
value: mixed,
68+
value: V,
17869
|};
17970

18071
type RejectedResult = {|
18172
status: 2,
18273
value: mixed,
18374
|};
18475

185-
type Result = PendingResult | ResolvedResult | RejectedResult;
76+
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
18677

18778
const fetchKey = {};
18879

189-
function readResultMap(): Map<string, Result> {
80+
function readResultMap(): Map<string, Result<FetchResponse>> {
19081
const resources = readCache().resources;
19182
let map = resources.get(fetchKey);
19283
if (map === undefined) {
@@ -196,31 +87,7 @@ function readResultMap(): Map<string, Result> {
19687
return map;
19788
}
19889

199-
function toResult(thenable): Result {
200-
const result: Result = {
201-
status: Pending,
202-
value: thenable,
203-
};
204-
thenable.then(
205-
value => {
206-
if (result.status === Pending) {
207-
const resolvedResult = ((result: any): ResolvedResult);
208-
resolvedResult.status = Resolved;
209-
resolvedResult.value = value;
210-
}
211-
},
212-
err => {
213-
if (result.status === Pending) {
214-
const rejectedResult = ((result: any): RejectedResult);
215-
rejectedResult.status = Rejected;
216-
rejectedResult.value = err;
217-
}
218-
},
219-
);
220-
return result;
221-
}
222-
223-
function readResult(result: Result) {
90+
function readResult<T>(result: Result<T>) {
22491
if (result.status === Resolved) {
22592
return result.value;
22693
} else {
@@ -230,46 +97,75 @@ function readResult(result: Result) {
23097

23198
function Response(nativeResponse) {
23299
this.headers = nativeResponse.headers;
233-
this.ok = nativeResponse.ok;
234-
this.redirected = nativeResponse.redirected;
235-
this.status = nativeResponse.status;
236-
this.statusText = nativeResponse.statusText;
237-
this.type = nativeResponse.type;
100+
this.ok = nativeResponse.statusCode >= 200 && nativeResponse.statusCode < 300;
101+
this.redirected = false; // TODO
102+
this.status = nativeResponse.statusCode;
103+
this.statusText = nativeResponse.statusMessage;
104+
this.type = 'basic';
238105
this.url = nativeResponse.url;
239106

240107
this._response = nativeResponse;
241-
this._arrayBuffer = null;
242108
this._blob = null;
243109
this._json = null;
244110
this._text = null;
111+
112+
const callbacks = [];
113+
function wake() {
114+
// This assumes they won't throw.
115+
while (callbacks.length > 0) {
116+
const cb = callbacks.pop();
117+
cb();
118+
}
119+
}
120+
const result: PendingResult = (this._result = {
121+
status: Pending,
122+
value: {
123+
then(cb) {
124+
callbacks.push(cb);
125+
},
126+
},
127+
});
128+
const data = [];
129+
nativeResponse.on('data', chunk => data.push(chunk));
130+
nativeResponse.on('end', () => {
131+
if (result.status === Pending) {
132+
const resolvedResult = ((result: any): ResolvedResult<Buffer>);
133+
resolvedResult.status = Resolved;
134+
resolvedResult.value = Buffer.concat(data);
135+
wake();
136+
}
137+
});
138+
nativeResponse.on('error', err => {
139+
if (result.status === Pending) {
140+
const rejectedResult = ((result: any): RejectedResult);
141+
rejectedResult.status = Rejected;
142+
rejectedResult.value = err;
143+
wake();
144+
}
145+
});
245146
}
246147

247148
Response.prototype = {
248149
constructor: Response,
249150
arrayBuffer() {
250-
return readResult(
251-
this._arrayBuffer ||
252-
(this._arrayBuffer = toResult(this._response.arrayBuffer())),
253-
);
151+
const buffer = readResult(this._result);
152+
return buffer;
254153
},
255154
blob() {
256-
return readResult(
257-
this._blob || (this._blob = toResult(this._response.blob())),
258-
);
155+
// TODO: Is this needed?
156+
throw new Error('Not implemented.');
259157
},
260158
json() {
261-
return readResult(
262-
this._json || (this._json = toResult(this._response.json())),
263-
);
159+
const buffer: Buffer = (readResult(this._result): any);
160+
return JSON.parse(buffer.toString());
264161
},
265162
text() {
266-
return readResult(
267-
this._text || (this._text = toResult(this._response.text())),
268-
);
163+
const buffer: Buffer = (readResult(this._result): any);
164+
return buffer.toString();
269165
},
270166
};
271167

272-
function preloadResult(url: string, options: FetchOptions): Result {
168+
function preloadResult(url: string, options: mixed): Result<FetchResponse> {
273169
const map = readResultMap();
274170
let entry = map.get(url);
275171
if (!entry) {
@@ -280,23 +176,54 @@ function preloadResult(url: string, options: FetchOptions): Result {
280176
throw Error('Unsupported option');
281177
}
282178
}
283-
const thenable = nodeFetch(url, options);
284-
entry = toResult(thenable);
179+
const callbacks = [];
180+
const wakeable = {
181+
then(cb) {
182+
callbacks.push(cb);
183+
},
184+
};
185+
const wake = () => {
186+
// This assumes they won't throw.
187+
while (callbacks.length > 0) {
188+
const cb = callbacks.pop();
189+
cb();
190+
}
191+
};
192+
const result: Result<FetchResponse> = (entry = {
193+
status: Pending,
194+
value: wakeable,
195+
});
196+
nodeFetch(
197+
url,
198+
options,
199+
response => {
200+
if (result.status === Pending) {
201+
const resolvedResult = ((result: any): ResolvedResult<FetchResponse>);
202+
resolvedResult.status = Resolved;
203+
resolvedResult.value = response;
204+
wake();
205+
}
206+
},
207+
err => {
208+
if (result.status === Pending) {
209+
const rejectedResult = ((result: any): RejectedResult);
210+
rejectedResult.status = Rejected;
211+
rejectedResult.value = err;
212+
wake();
213+
}
214+
},
215+
);
285216
map.set(url, entry);
286217
}
287218
return entry;
288219
}
289220

290-
export function preload(url: string, options: FetchOptions): void {
221+
export function preload(url: string, options: mixed): void {
222+
preloadResult(url, options);
291223
// Don't return anything.
292224
}
293225

294-
export function fetch(url: string, options: FetchOptions): Object {
226+
export function fetch(url: string, options: mixed): Object {
295227
const result = preloadResult(url, options);
296-
const nativeResponse = (readResult(result): any);
297-
if (nativeResponse._reactResponse) {
298-
return nativeResponse._reactResponse;
299-
} else {
300-
return (nativeResponse._reactResponse = new Response(nativeResponse));
301-
}
228+
return (readResult(result): any);
302229
}

0 commit comments

Comments
 (0)