Skip to content

Commit 2c85741

Browse files
committed
polyfills:Implement streamable fetch request/response
1 parent e26e97d commit 2c85741

File tree

7 files changed

+23380
-23267
lines changed

7 files changed

+23380
-23267
lines changed

src/bundles/c/core/polyfills.c

Lines changed: 23172 additions & 23150 deletions
Large diffs are not rendered by default.

src/js/polyfills/fetch/body.js

Lines changed: 53 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ function isDataView(obj) {
33
}
44

55
function consumed(body) {
6-
if (body._noBody) {
6+
if (body._bodySize===0) {
77
return;
88
}
99

@@ -26,36 +26,6 @@ function fileReaderReady(reader) {
2626
});
2727
}
2828

29-
function readBlobAsArrayBuffer(blob) {
30-
var reader = new FileReader();
31-
var promise = fileReaderReady(reader);
32-
33-
reader.readAsArrayBuffer(blob);
34-
35-
return promise;
36-
}
37-
38-
function readBlobAsText(blob) {
39-
var reader = new FileReader();
40-
var promise = fileReaderReady(reader);
41-
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
42-
var encoding = match ? match[1] : 'utf-8';
43-
44-
reader.readAsText(blob, encoding);
45-
46-
return promise;
47-
}
48-
49-
function readArrayBufferAsText(buf) {
50-
var view = new Uint8Array(buf);
51-
var chars = new Array(view.length);
52-
53-
for (var i = 0; i < view.length; i++) {
54-
chars[i] = String.fromCharCode(view[i]);
55-
}
56-
57-
return chars.join('');
58-
}
5929

6030
function bufferClone(buf) {
6131
if (buf.slice) {
@@ -80,24 +50,43 @@ export const BodyMixin = {
8050
this._bodyInit = body;
8151

8252
if (!body) {
83-
this._noBody = true;
84-
this._bodyText = '';
53+
this._bodySize = 0;
54+
this._bodyReadable = ReadableStream.from([new Uint8Array(0)]);
8555
} else if (typeof body === 'string') {
86-
this._bodyText = body;
56+
const bodyBuffer=new TextEncoder().encode(body);
57+
this._bodySize=bodyBuffer.byteLength;
58+
this._bodyReadable = ReadableStream.from([bodyBuffer]);
8759
} else if (isPrototypeOf(Blob.prototype, body)) {
88-
this._bodyBlob = body;
60+
this._bodySize=body.size;
61+
this._bodyReadable = body.stream;
8962
} else if (isPrototypeOf(FormData.prototype, body)) {
90-
this._bodyFormData = body;
63+
//formdata polyfill
64+
const bodyBuffer = body['_blob']();
65+
this._bodySize=bodyBuffer.byteLength;
66+
this._bodyReadable = ReadableStream.from([bodyBuffer]);
9167
} else if (isPrototypeOf(URLSearchParams.prototype, body)) {
92-
this._bodyText = body.toString();
68+
const bodyBuffer=new TextEncoder().encode(body);
69+
this._bodySize=bodyBuffer.byteLength;
70+
this._bodyReadable = ReadableStream.from([new TextEncoder().encode(body.toString())]);
9371
} else if (isDataView(body)) {
94-
this._bodyArrayBuffer = bufferClone(body.buffer);
72+
const bodyBuffer=new Uint8Array(bufferClone(body.buffer));
73+
this._bodySize=bodyBuffer.byteLength;
74+
this._bodyReadable = ReadableStream.from([bodyBuffer]);
9575
} else if (isPrototypeOf(ArrayBuffer.prototype, body) || ArrayBuffer.isView(body)) {
96-
this._bodyArrayBuffer = bufferClone(body);
76+
const bodyBuffer=new Uint8Array(bufferClone(body.buffer));
77+
this._bodySize=bodyBuffer.byteLength;
78+
this._bodyReadable = ReadableStream.from([bodyBuffer]);
79+
} else if (isPrototypeOf(ReadableStream.prototype, body)){
80+
this._bodySize=-1;
81+
this._bodyReadable = body;
9782
} else {
98-
this._bodyText = body = Object.prototype.toString.call(body);
83+
const bodyBuffer=new TextEncoder().encode(body.toString());
84+
this._bodySize=bodyBuffer.byteLength;
85+
this._bodyReadable = ReadableStream.from([bodyBuffer]);
9986
}
10087

88+
this.body=this._bodyReadable
89+
10190
if (!this.headers.get('content-type')) {
10291
if (typeof body === 'string') {
10392
this.headers.set('content-type', 'text/plain;charset=UTF-8');
@@ -109,70 +98,46 @@ export const BodyMixin = {
10998
}
11099
},
111100

112-
blob() {
101+
async blob() {
113102
const rejected = consumed(this);
114103

115104
if (rejected) {
116-
return rejected;
105+
await rejected;
117106
}
118107

119-
if (this._bodyBlob) {
120-
return Promise.resolve(this._bodyBlob);
121-
} else if (this._bodyArrayBuffer) {
122-
return Promise.resolve(new Blob([ this._bodyArrayBuffer ]));
123-
} else if (this._bodyFormData) {
124-
throw new Error('could not read FormData body as blob');
125-
} else {
126-
return Promise.resolve(new Blob([ this._bodyText ]));
127-
}
128-
},
129-
130-
arrayBuffer() {
131-
if (this._bodyArrayBuffer) {
132-
var isConsumed = consumed(this);
133-
134-
if (isConsumed) {
135-
return isConsumed;
136-
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
137-
return Promise.resolve(
138-
this._bodyArrayBuffer.buffer.slice(
139-
this._bodyArrayBuffer.byteOffset,
140-
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
141-
)
142-
);
143-
} else {
144-
return Promise.resolve(this._bodyArrayBuffer);
108+
if (this._bodyReadable) {
109+
const parts=[];
110+
const reader=this._bodyReadable.getReader();
111+
while(true){
112+
const next=await reader.read();
113+
if(next.done){
114+
break;
115+
}
116+
parts.push(next.value);
145117
}
118+
return new Blob(parts);
146119
} else {
147-
return this.blob().then(readBlobAsArrayBuffer);
120+
throw new Error('Unknown body type');
148121
}
149122
},
150123

151-
text() {
152-
const rejected = consumed(this);
153-
154-
if (rejected) {
155-
return rejected;
156-
}
124+
async arrayBuffer() {
125+
//TODO: expose Blob.parts to reduce memeory copy?
126+
return await (await this.blob()).arrayBuffer();
127+
},
157128

158-
if (this._bodyBlob) {
159-
return readBlobAsText(this._bodyBlob);
160-
} else if (this._bodyArrayBuffer) {
161-
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer));
162-
} else if (this._bodyFormData) {
163-
throw new Error('could not read FormData body as text');
164-
} else {
165-
return Promise.resolve(this._bodyText);
166-
}
129+
async text() {
130+
return new TextDecoder().decode(await this.arrayBuffer());
167131
},
168132

169-
formData() {
170-
return this.text().then(decode);
133+
async formData() {
134+
return decode(await this.text());
171135
},
172136

173-
json() {
174-
return this.text().then(JSON.parse);
137+
async json() {
138+
return JSON.parse(await this.text());
175139
},
140+
176141
};
177142

178143
function decode(body) {

src/js/polyfills/fetch/fetch.js

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@ export function fetch(input, init) {
1616
xhr.abort();
1717
}
1818

19-
xhr.onload = function() {
20-
const options = {
21-
statusText: xhr.statusText,
22-
headers: parseHeaders(xhr.getAllResponseHeaders() || ''),
23-
status: xhr.status,
24-
url: xhr.responseURL
25-
};
26-
const body = xhr.response;
27-
28-
setTimeout(function() {
29-
resolve(new Response(body, options));
30-
}, 0);
31-
};
19+
//tjs only
20+
let controller=null;
21+
const responseReader=new ReadableStream({
22+
start(controller_){controller=controller_;}
23+
});
24+
25+
xhr.onprogress=function(){
26+
if(controller!=null){
27+
controller.enqueue(new Uint8Array(xhr.__tjsGetAndClearResponseBuffer()));
28+
}
29+
}
30+
xhr.onload=function(){
31+
if(controller!=null){
32+
controller.enqueue(new Uint8Array(xhr.__tjsGetAndClearResponseBuffer()));
33+
controller.close();
34+
}
35+
}
3236

3337
xhr.onerror = function() {
3438
setTimeout(function() {
@@ -77,19 +81,43 @@ export function fetch(input, init) {
7781
});
7882
}
7983

80-
if (request.signal) {
81-
request.signal.addEventListener('abort', abortXhr);
82-
83-
xhr.onreadystatechange = function() {
84-
// DONE (success or failure)
85-
if (xhr.readyState === xhr.DONE) {
86-
request.signal.removeEventListener('abort', abortXhr);
87-
}
84+
xhr.onreadystatechange = function() {
85+
// DONE (success or failure)
86+
if (request.signal && xhr.readyState === xhr.DONE) {
87+
request.signal.removeEventListener('abort', abortXhr);
88+
}
89+
if(xhr.readyState === xhr.HEADERS_RECEIVED){
90+
const options = {
91+
statusText: xhr.statusText,
92+
headers: parseHeaders(xhr.getAllResponseHeaders() || ''),
93+
status: xhr.status,
94+
url: xhr.responseURL
95+
};
96+
setTimeout(function() {
97+
resolve(new Response(responseReader, options));
98+
}, 0);
99+
}
100+
};
101+
if(request._bodySize>0){
102+
request.arrayBuffer().then((body)=>{
103+
xhr.send(new Uint8Array(body));
104+
})
105+
}else if(request._bodySize===-1){
106+
const reader=request.body.getReader();
107+
xhr.__ontjsstreamsenddata=function(){
108+
reader.read().then((next)=>{
109+
if(next.done){
110+
xhr.__tjsStreamSend(null);
111+
}else{
112+
xhr.__tjsStreamSend(next.value);
113+
}
114+
});
88115
};
116+
xhr.__tjsStreamSend();
117+
}else{
118+
xhr.send(null);
89119
}
90-
91-
// TODO: why not use the .body property?
92-
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
120+
93121
});
94122
}
95123

src/js/polyfills/fetch/request.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export class Request {
2424
this.mode = input.mode;
2525
this.signal = input.signal;
2626

27-
if (!body && input._bodyInit !== null) {
28-
body = input._bodyInit;
27+
if (!body && input.body !== null) {
28+
body = input.body;
2929
input.bodyUsed = true;
3030
}
3131
} else {

src/js/polyfills/fetch/response.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class Response {
1212
this.status = options.status === undefined ? 200 : options.status;
1313

1414
if (this.status < 200 || this.status > 599) {
15-
throw new RangeError('The status provided (0) is outside the range [200, 599].');
15+
throw new RangeError(`The status provided ${this.status} is outside the range [200, 599].`);
1616
}
1717

1818
this.ok = this.status >= 200 && this.status < 300;

src/js/polyfills/xhr.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class XMLHttpRequest extends EventTarget {
1616
LOADING = XHR.LOADING;
1717
DONE = XHR.DONE;
1818

19+
__ontjsstreamsenddata=null;
20+
1921
constructor() {
2022
super();
2123

@@ -53,6 +55,12 @@ class XMLHttpRequest extends EventTarget {
5355
this.dispatchEvent(new Event('timeout'));
5456
};
5557

58+
xhr.ontjsstreamsenddata = () => {
59+
if(this.__ontjsstreamsenddata!=null){
60+
this.__ontjsstreamsenddata();
61+
}
62+
}
63+
5664
this[kXHR] = xhr;
5765
}
5866

@@ -175,6 +183,14 @@ class XMLHttpRequest extends EventTarget {
175183
setRequestHeader(name, value) {
176184
return this[kXHR].setRequestHeader(name, value);
177185
}
186+
187+
__tjsGetAndClearResponseBuffer() {
188+
return this[kXHR].__tjsGetAndClearResponseBuffer();
189+
}
190+
191+
__tjsStreamSend(buffer){
192+
return this[kXHR].__tjsStreamSend(buffer);
193+
}
178194
}
179195

180196
const xhrProto = XMLHttpRequest.prototype;
@@ -187,6 +203,7 @@ defineEventAttribute(xhrProto, 'loadstart');
187203
defineEventAttribute(xhrProto, 'progress');
188204
defineEventAttribute(xhrProto, 'readystatechange');
189205
defineEventAttribute(xhrProto, 'timeout');
206+
defineEventAttribute(xhrProto, 'tjsstreamsenddata');
190207

191208
Object.defineProperty(window, 'XMLHttpRequest', {
192209
enumerable: true,

0 commit comments

Comments
 (0)