1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Wakeable} from 'shared/ReactTypes';
11
+
12
+import * as http from 'http';
13
+import * as https from 'https';
14
+
15
+import {readCache} from 'react/unstable-cache';
16
+
17
+type FetchResponse = {|
18
+ // Properties
19
+ headers: any,
20
+ ok: boolean,
21
+ redirected: boolean,
22
+ status: number,
23
+ statusText: string,
24
+ type: 'basic',
25
+ url: string,
26
+ // Methods
27
+ arrayBuffer(): ArrayBuffer,
28
+ blob(): any,
29
+ json(): any,
30
+ text(): string,
31
+|};
32
+
33
+function nodeFetch(
34
+ url: string,
35
+ options: mixed,
36
+ onResolve: any => void,
37
+ onReject: any => void,
38
+): void {
39
+ const {hostname, pathname, search, port, protocol} = new URL(url);
40
+ const nodeOptions = {
41
+ hostname,
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));
50
+ });
51
+ request.on('error', error => {
52
+ onReject(error);
53
+ });
54
+ request.end();
55
+}
56
+
57
+const Pending = 0;
58
+const Resolved = 1;
59
+const Rejected = 2;
60
+
61
+type PendingResult = {|
62
+ status: 0,
63
+ value: Wakeable,
64
+|};
65
+
66
+type ResolvedResult<V> = {|
67
+ status: 1,
68
+ value: V,
69
+|};
70
+
71
+type RejectedResult = {|
72
+ status: 2,
73
+ value: mixed,
74
+|};
75
+
76
+type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
77
+
78
+const fetchKey = {};
79
+
80
+function readResultMap(): Map<string, Result<FetchResponse>> {
81
+ const resources = readCache().resources;
82
+ let map = resources.get(fetchKey);
83
+ if (map === undefined) {
84
+ map = new Map();
85
+ resources.set(fetchKey, map);
86
+ }
87
+ return map;
88
+}
89
+
90
+function readResult<T>(result: Result<T>): T {
91
+ if (result.status === Resolved) {
92
+ return result.value;
93
+ } else {
94
+ throw result.value;
95
+ }
96
+}
97
+
98
+function Response(nativeResponse) {
99
+ this.headers = nativeResponse.headers;
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';
105
+ this.url = nativeResponse.url;
106
+
107
+ this._response = nativeResponse;
108
+ this._blob = null;
109
+ this._json = null;
110
+ 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
+ });
146
+}
147
+
148
+Response.prototype = {
149
+ constructor: Response,
150
+ arrayBuffer() {
151
+ const buffer = readResult(this._result);
152
+ return buffer;
153
+ },
154
+ blob() {
155
+ // TODO: Is this needed?
156
+ throw new Error('Not implemented.');
157
+ },
158
+ json() {
159
+ const buffer = readResult(this._result);
160
+ return JSON.parse(buffer.toString());
161
+ },
162
+ text() {
163
+ const buffer = readResult(this._result);
164
+ return buffer.toString();
165
+ },
166
+};
167
+
168
+function preloadResult(url: string, options: mixed): Result<FetchResponse> {
169
+ const map = readResultMap();
170
+ let entry = map.get(url);
171
+ if (!entry) {
172
+ if (options) {
173
+ if (options.method || options.body || options.signal) {
174
+ // TODO: wire up our own cancellation mechanism.
175
+ // TODO: figure out what to do with POST.
176
+ throw Error('Unsupported option');
177
+ }
178
+ }
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
+ );
216
+ map.set(url, entry);
217
+ }
218
+ return entry;
219
+}
220
+
221
+export function preload(url: string, options: mixed): void {
222
+ preloadResult(url, options);
223
+ // Don't return anything.
224
+}
225
+
226
+export function fetch(url: string, options: mixed): FetchResponse {
227
+ const result = preloadResult(url, options);
228
+ return readResult(result);
229
+}