Skip to content

Commit 0497de0

Browse files
Add fetch() polyfill for Playground
Implements a minimal `fetch(input, init)` polyfill in Apps/Playground that wraps XMLHttpRequest. Provides a Response-like result with `.ok`, `.status`, `.statusText`, `.url`, `.text()`, `.arrayBuffer()`, `.json()`, `.blob()`, and a `headers` stub. Self-detecting: no-op if a global `fetch` already exists; bails if XMLHttpRequest is unavailable. Wired into the Playground SCRIPTS list and loaded by AppContext before `ammo.js` / `babylon.max.js` so playground snippets that use the modern `fetch` API can run on Babylon Native's host environments (Chakra and V8), which do not provide `fetch` by default. Babylon Native's `XMLHttpRequest` only dispatches events through `addEventListener` (no `onload`/`onerror` properties) and fires `loadend` rather than `load`; the polyfill listens on `loadend` and inspects `status` to decide resolve/reject. When `responseType` is `arraybuffer`, BN's `XHR.responseText` is empty, so `.text()` decodes the array buffer directly rather than reading `responseText`.
1 parent 8ad6e3a commit 0497de0

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

Apps/Playground/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ set(DEPENDENCIES
1515
set(SCRIPTS
1616
"Scripts/dom_polyfill.js"
1717
"Scripts/experience.js"
18+
"Scripts/fetch_polyfill.js"
1819
"Scripts/playground_runner.js"
1920
"Scripts/validation_native.js"
2021
"Scripts/config.json")
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Minimal `fetch` polyfill for Babylon Native.
2+
//
3+
// Provides global `fetch(url, options)` returning a Promise that resolves to a
4+
// Response-like object with `.ok`, `.status`, `.statusText`, `.url`,
5+
// `.text()`, `.arrayBuffer()`, `.json()`, `.blob()`, and a `headers`
6+
// stub (always empty). Internally wraps XMLHttpRequest so any URL scheme
7+
// the host XHR can resolve (http:, https:, app:, data:, ...) works.
8+
//
9+
// Skipped if a `fetch` global is already defined.
10+
11+
(function () {
12+
var g = (new Function('return this'))();
13+
if (typeof g.fetch === 'function') {
14+
return;
15+
}
16+
17+
var XHR = g.XMLHttpRequest;
18+
if (typeof XHR !== 'function') {
19+
// Nothing we can do without XHR.
20+
return;
21+
}
22+
23+
function HeadersStub() {}
24+
HeadersStub.prototype.get = function () { return null; };
25+
HeadersStub.prototype.has = function () { return false; };
26+
HeadersStub.prototype.forEach = function () {};
27+
28+
function arrayBufferToString(buf) {
29+
var bytes = new Uint8Array(buf);
30+
// Chunk to avoid call stack limits on large buffers.
31+
var chunkSize = 0x8000;
32+
var chars = [];
33+
for (var i = 0; i < bytes.length; i += chunkSize) {
34+
chars.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)));
35+
}
36+
return chars.join('');
37+
}
38+
39+
function makeResponse(xhr, requestUrl, effectiveResponseType) {
40+
var bodyUsed = false;
41+
var cachedArrayBuffer = null;
42+
var cachedText = null;
43+
44+
function ensureNotConsumed() {
45+
if (bodyUsed) {
46+
throw new TypeError('Body has already been consumed.');
47+
}
48+
bodyUsed = true;
49+
}
50+
51+
function getArrayBuffer() {
52+
if (cachedArrayBuffer !== null) {
53+
return cachedArrayBuffer;
54+
}
55+
var resp = xhr.response;
56+
if (resp instanceof ArrayBuffer) {
57+
cachedArrayBuffer = resp;
58+
} else if (typeof resp === 'string') {
59+
var len = resp.length;
60+
var buf = new ArrayBuffer(len);
61+
var view = new Uint8Array(buf);
62+
for (var i = 0; i < len; i++) {
63+
view[i] = resp.charCodeAt(i) & 0xFF;
64+
}
65+
cachedArrayBuffer = buf;
66+
} else if (resp && resp.byteLength !== undefined) {
67+
cachedArrayBuffer = resp;
68+
} else {
69+
cachedArrayBuffer = new ArrayBuffer(0);
70+
}
71+
return cachedArrayBuffer;
72+
}
73+
74+
function getText() {
75+
if (cachedText !== null) {
76+
return cachedText;
77+
}
78+
// Prefer responseText only when the request actually used the
79+
// String response type; otherwise BN's XHR returns an empty
80+
// string and we must decode the arraybuffer ourselves.
81+
if (effectiveResponseType === 'text' && typeof xhr.responseText === 'string') {
82+
cachedText = xhr.responseText;
83+
} else {
84+
cachedText = arrayBufferToString(getArrayBuffer());
85+
}
86+
return cachedText;
87+
}
88+
89+
var status = xhr.status || 0;
90+
return {
91+
ok: status >= 200 && status < 300,
92+
status: status,
93+
statusText: xhr.statusText || '',
94+
url: requestUrl,
95+
redirected: false,
96+
type: 'basic',
97+
headers: new HeadersStub(),
98+
get bodyUsed() { return bodyUsed; },
99+
arrayBuffer: function () {
100+
ensureNotConsumed();
101+
return Promise.resolve(getArrayBuffer());
102+
},
103+
text: function () {
104+
ensureNotConsumed();
105+
return Promise.resolve(getText());
106+
},
107+
json: function () {
108+
ensureNotConsumed();
109+
try {
110+
return Promise.resolve(JSON.parse(getText()));
111+
} catch (e) {
112+
return Promise.reject(e);
113+
}
114+
},
115+
blob: function () {
116+
ensureNotConsumed();
117+
var buf = getArrayBuffer();
118+
if (typeof g.Blob === 'function') {
119+
return Promise.resolve(new g.Blob([buf]));
120+
}
121+
return Promise.resolve(buf);
122+
},
123+
clone: function () {
124+
throw new TypeError('Response.clone() is not supported by this polyfill.');
125+
}
126+
};
127+
}
128+
129+
g.fetch = function (input, init) {
130+
init = init || {};
131+
var url;
132+
var method = (init.method || 'GET').toUpperCase();
133+
var body = init.body;
134+
var headers = init.headers;
135+
var responseType = init.responseType;
136+
137+
if (typeof input === 'string') {
138+
url = input;
139+
} else if (input && typeof input.url === 'string') {
140+
url = input.url;
141+
method = (input.method || method).toUpperCase();
142+
body = input.body || body;
143+
headers = input.headers || headers;
144+
} else {
145+
return Promise.reject(new TypeError('fetch: invalid input.'));
146+
}
147+
148+
return new Promise(function (resolve, reject) {
149+
var xhr;
150+
try {
151+
xhr = new XHR();
152+
} catch (e) {
153+
reject(e);
154+
return;
155+
}
156+
157+
try {
158+
xhr.open(method, url, true);
159+
} catch (e) {
160+
reject(e);
161+
return;
162+
}
163+
164+
var effectiveResponseType = responseType === 'text' ? 'text' : 'arraybuffer';
165+
try {
166+
xhr.responseType = effectiveResponseType;
167+
} catch (e) {}
168+
169+
if (headers) {
170+
if (typeof headers.forEach === 'function') {
171+
headers.forEach(function (value, name) {
172+
try { xhr.setRequestHeader(name, value); } catch (e) {}
173+
});
174+
} else {
175+
for (var k in headers) {
176+
if (Object.prototype.hasOwnProperty.call(headers, k)) {
177+
try { xhr.setRequestHeader(k, headers[k]); } catch (e) {}
178+
}
179+
}
180+
}
181+
}
182+
183+
// Babylon Native's XMLHttpRequest only dispatches via addEventListener
184+
// and only fires 'readystatechange', 'loadend', and 'error' (no 'load').
185+
// Inspect status in loadend to decide success/failure.
186+
var settled = false;
187+
function settleFromLoadEnd() {
188+
if (settled) {
189+
return;
190+
}
191+
settled = true;
192+
var status = xhr.status || 0;
193+
if (status >= 200 && status < 300) {
194+
resolve(makeResponse(xhr, url, effectiveResponseType));
195+
} else {
196+
reject(new TypeError('fetch: request failed with status ' + status + ' for ' + url));
197+
}
198+
}
199+
function settleAsError() {
200+
if (settled) {
201+
return;
202+
}
203+
settled = true;
204+
reject(new TypeError('Network request failed: ' + url));
205+
}
206+
207+
if (typeof xhr.addEventListener === 'function') {
208+
xhr.addEventListener('loadend', settleFromLoadEnd);
209+
xhr.addEventListener('error', settleAsError);
210+
} else {
211+
xhr.onload = settleFromLoadEnd;
212+
xhr.onloadend = settleFromLoadEnd;
213+
xhr.onerror = settleAsError;
214+
xhr.onabort = settleAsError;
215+
}
216+
217+
try {
218+
xhr.send(body || null);
219+
} catch (e) {
220+
if (!settled) {
221+
settled = true;
222+
reject(e);
223+
}
224+
}
225+
});
226+
};
227+
})();

Apps/Playground/Shared/AppContext.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ AppContext::AppContext(
217217

218218
m_scriptLoader.emplace(*m_runtime);
219219
m_scriptLoader->LoadScript("app:///Scripts/dom_polyfill.js");
220+
m_scriptLoader->LoadScript("app:///Scripts/fetch_polyfill.js");
220221
m_scriptLoader->LoadScript("app:///Scripts/ammo.js");
221222
// Commenting out recast.js for now because v8jsi is incompatible with asm.js.
222223
// m_scriptLoader->LoadScript("app:///Scripts/recast.js");

0 commit comments

Comments
 (0)