Skip to content

Commit c8c942d

Browse files
committed
refactor(api/fetch): rework streaming for 'text/*' types
1 parent a0ebfd2 commit c8c942d

2 files changed

Lines changed: 72 additions & 43 deletions

File tree

api/fetch/fetch.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ function normalizeMethod(method) {
348348
return methods.indexOf(upcased) > -1 ? upcased : method
349349
}
350350

351-
export function Request(input, options) {
351+
export function Request(input, options, xhr) {
352352
if (!(this instanceof Request)) {
353353
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
354354
}
@@ -393,7 +393,7 @@ export function Request(input, options) {
393393
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
394394
throw new TypeError('Body not allowed for GET or HEAD requests')
395395
}
396-
this._initBody(body, options)
396+
this._initBody(body, options, xhr)
397397

398398
if (this.method === 'GET' || this.method === 'HEAD') {
399399
if (options.cache === 'no-store' || options.cache === 'no-cache') {
@@ -461,7 +461,7 @@ function parseHeaders(rawHeaders) {
461461

462462
Body.call(Request.prototype)
463463

464-
export function Response(bodyInit, options) {
464+
export function Response(bodyInit, options, xhr) {
465465
if (!(this instanceof Response)) {
466466
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
467467
}
@@ -478,7 +478,7 @@ export function Response(bodyInit, options) {
478478
this.statusText = options.statusText === undefined ? '' : '' + options.statusText
479479
this.headers = new Headers(options.headers)
480480
this.url = options.url || ''
481-
this._initBody(bodyInit, options)
481+
this._initBody(bodyInit, options, xhr)
482482
}
483483

484484
Body.call(Response.prototype)
@@ -526,14 +526,14 @@ try {
526526

527527
export function fetch(input, init) {
528528
return new Promise(function(resolve, reject) {
529-
var request = new Request(input, init)
529+
var xhr = new XMLHttpRequest()
530+
var sent = false
531+
var request = new Request(input, init, xhr)
530532

531533
if (request.signal && request.signal.aborted) {
532534
return reject(new DOMException('Aborted', 'AbortError'))
533535
}
534536

535-
var xhr = new XMLHttpRequest()
536-
537537
function abortXhr() {
538538
xhr.abort()
539539
}
@@ -553,7 +553,9 @@ export function fetch(input, init) {
553553
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
554554
var body = 'response' in xhr ? xhr.response : xhr.responseText
555555
setTimeout(function() {
556-
resolve(new Response(body, options))
556+
if (sent) return
557+
sent = true
558+
resolve(new Response(body, options, xhr))
557559
}, 0)
558560
}
559561

@@ -575,10 +577,6 @@ export function fetch(input, init) {
575577
}, 0)
576578
}
577579

578-
if (init?.onxhr) {
579-
init.onxhr(xhr)
580-
}
581-
582580
function fixUrl(url) {
583581
try {
584582
return url === '' && g.location.href ? g.location.href : url
@@ -633,6 +631,26 @@ export function fetch(input, init) {
633631
}
634632
}
635633

634+
// XXX(@jwerle): introduced to support 'text/event-stream' or any other streaming text sources
635+
xhr.addEventListener('progress', () => {
636+
if (sent) return
637+
var options = {
638+
statusText: xhr.statusText,
639+
headers: parseHeaders(xhr.getAllResponseHeaders() || ''),
640+
status: xhr.status,
641+
url: ''
642+
}
643+
if ((options.headers.get('content-type') || '').startsWith('text/')) {
644+
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
645+
sent = true
646+
resolve(new Response(null, options, xhr))
647+
}
648+
}, { once: true })
649+
650+
if (init?.onxhr) {
651+
init.onxhr(xhr)
652+
}
653+
636654
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
637655
})
638656
}

api/fetch/index.js

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const textEncoder = new TextEncoder()
3030
Response.prototype._initBody = initBody
3131
Request.prototype._initBody = initBody
3232

33-
async function initBody (body) {
33+
async function initBody (body, options, xhr) {
3434
this.body = null
3535

3636
if (
@@ -97,57 +97,68 @@ async function initBody (body) {
9797
if (this._bodyArrayBuffer) {
9898
const arrayBuffer = this._bodyArrayBuffer
9999
this.body = new ReadableStream({
100-
async start (controller) {
100+
start (controller) {
101101
controller.enqueue(arrayBuffer)
102102
controller.close()
103103
}
104104
})
105105
} else if (this._bodyBlob) {
106106
const blob = this._bodyBlob
107107
this.body = new ReadableStream({
108-
type: 'bytes',
109108
async start (controller) {
110-
const stream = await blob.stream()
111-
if (controller.byobRequest) {
112-
const reader = stream.getReader({ mode: 'byob' })
113-
while (true) {
114-
const { done, value } = await reader.read(controller.byobRequest.view)
115-
if (done) {
116-
break
117-
}
118-
119-
if (value?.byteLength > 0) {
120-
controller.byobRequest.respond(value.byteLength)
121-
}
122-
}
123-
} else {
124-
const reader = stream.getReader()
125-
while (true) {
126-
const { done, value } = await reader.read()
127-
if (done) {
128-
break
129-
}
130-
131-
controller.enqueue(value)
132-
}
133-
}
134-
109+
controller.enqueue(await blob.arrayBuffer())
135110
controller.close()
136111
}
137112
})
138113
} else if (this._bodyText) {
139114
const text = this._bodyText
140-
const encoded = textEncoder.encode(text)
141115
this.body = new ReadableStream({
142-
async start (controller) {
143-
controller.enqueue(encoded)
116+
start (controller) {
117+
controller.enqueue(textEncoder.encode(text))
144118
controller.close()
145119
}
146120
})
147121
} else {
148122
this.body = null
149123
}
150124
}
125+
126+
if (this instanceof Request) {
127+
options.onxhr = (xhr) => {
128+
const accept = this.headers.get('accept') || ''
129+
if (accept.startsWith('text/')) {
130+
try {
131+
xhr.responseType = 'text'
132+
} catch {}
133+
}
134+
}
135+
}
136+
137+
const contentType = this.headers.get('content-type') || ''
138+
if (xhr && contentType.startsWith('text/')) {
139+
let controller
140+
let byteOffset = 0
141+
this.body = new ReadableStream({
142+
type: 'bytes',
143+
start (c) {
144+
controller = c
145+
}
146+
})
147+
xhr.addEventListener('load', () => {
148+
if (controller) {
149+
controller.close()
150+
}
151+
}, { once: true })
152+
xhr.onprogress = () => {
153+
try {
154+
if (controller) {
155+
const buffer = textEncoder.encode(xhr.responseText.slice(byteOffset))
156+
byteOffset = xhr.responseText.length
157+
controller.enqueue(buffer)
158+
}
159+
} catch {}
160+
}
161+
}
151162
}
152163

153164
Object.defineProperties(Request.prototype, {

0 commit comments

Comments
 (0)