site/node_modules/sync-fetch/browser.js
2024-10-14 08:09:33 +02:00

403 lines
8.9 KiB
JavaScript

/* eslint-env browser */
const { Buffer } = require('buffer/')
function syncFetch (...args) {
const request = new syncFetch.Request(...args)
const xhr = new XMLHttpRequest()
xhr.withCredentials = request.credentials === 'include'
xhr.timeout = request[INTERNALS].timeout
// Request
xhr.open(request.method, request.url, false)
let useBinaryEncoding = false
try {
// Only allowed in Worker scope, not available in older browsers
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType#Synchronous_XHR_restrictions
xhr.responseType = 'arraybuffer'
} catch (e) {
// Not in Worker scope; instead, attempt this alternative method
// https://web.archive.org/web/20071103070418/http://mgran.blogspot.com/2006/08/downloading-binary-streams-with.html
xhr.overrideMimeType('text/plain; charset=x-user-defined')
useBinaryEncoding = true
}
for (const header of request.headers) {
xhr.setRequestHeader(...header)
}
xhr.send(request.body || null)
// Response
let headers = xhr.getAllResponseHeaders()
headers = headers && headers.split('\r\n').filter(Boolean).map(header => header.split(': ', 2))
let body = xhr.response
if (useBinaryEncoding) {
const buffer = Buffer.alloc(body.length)
for (let i = 0; i < body.length; i++) {
buffer[i] = body.charCodeAt(i) & 0xff
}
body = buffer
}
const response = new syncFetch.Response(body, {
headers,
status: xhr.status,
statusText: xhr.statusText
})
response[INTERNALS].url = xhr.responseURL
response[INTERNALS].redirected = xhr.responseURL !== request.url
return response
}
const INTERNALS = Symbol('SyncFetch Internals')
const REQ_UNSUPPORTED = ['mode', 'cache', 'redirect', 'referrer', 'integrity']
const HTTP_STATUS = {
100: 'Continue',
101: 'Switching Protocols',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
426: 'Upgrade Required',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported'
}
class SyncRequest {
constructor (resource, init = {}) {
for (const option of REQ_UNSUPPORTED) {
if (option in init) {
throw new TypeError(`option ${option} not supported`)
}
}
if (init.credentials === 'same-origin') {
throw new TypeError('option credentials with value \'same-origin\' not supported')
}
this[INTERNALS] = {
method: init.method || 'GET',
headers: new syncFetch.Headers(init.headers),
body: init.body ? Buffer.from(init.body) : null,
credentials: init.credentials || 'omit',
// Non-spec
timeout: init.timeout || 0
}
if (typeof resource === 'string') {
this[INTERNALS].url = resource
} else if (resource instanceof SyncRequest) {
this[INTERNALS].url = resource.url
if (!init.method) {
this[INTERNALS].method = resource.method
}
if (!init.headers) {
this[INTERNALS].headers = resource.headers
}
if (!init.body) {
this[INTERNALS].body = resource[INTERNALS].body
}
if (!init.credentials) {
this[INTERNALS].credentials = resource.credentials
}
} else {
throw new TypeError('Request input should be a URL string or a Request object')
}
}
get cache () {
return 'default'
}
get credentials () {
return this[INTERNALS].credentials
}
get destination () {
return ''
}
get headers () {
return this[INTERNALS].headers
}
get integrity () {
return ''
}
get method () {
return this[INTERNALS].method
}
get mode () {
return 'cors'
}
get priority () {
return 'auto'
}
get redirect () {
return 'follow'
}
get referrer () {
return 'about:client'
}
get referrerPolicy () {
return ''
}
get url () {
return this[INTERNALS].url
}
clone () {
checkBody(this)
return new SyncRequest(this.url, this[INTERNALS])
}
}
class SyncResponse {
constructor (body, init = {}) {
this[INTERNALS] = {
body: body ? Buffer.from(body) : null,
bodyUsed: false,
headers: new syncFetch.Headers(init.headers),
status: init.status,
statusText: init.statusText
}
}
get headers () {
return this[INTERNALS].headers
}
get ok () {
const status = this[INTERNALS].status
return status >= 200 && status < 300
}
get redirected () {
return this[INTERNALS].redirected
}
get status () {
return this[INTERNALS].status
}
get statusText () {
return this[INTERNALS].statusText
}
get url () {
return this[INTERNALS].url
}
clone () {
return this.redirect(this[INTERNALS].url, this[INTERNALS].status)
}
redirect (url, status) {
checkBody(this)
const response = new SyncResponse(this[INTERNALS].body, {
headers: this[INTERNALS].headers,
status: status || this[INTERNALS].status,
statusText: HTTP_STATUS[status] || this[INTERNALS].statusText
})
response[INTERNALS].url = url || this[INTERNALS].url
response[INTERNALS].redirected = this[INTERNALS].redirected
return response
}
}
class Body {
constructor (body) {
this[INTERNALS] = {
body: Buffer.from(body),
bodyUsed: false
}
}
get bodyUsed () {
return this[INTERNALS].bodyUsed
}
static mixin (prototype) {
for (const name of Object.getOwnPropertyNames(Body.prototype)) {
if (name === 'constructor') { continue }
const desc = Object.getOwnPropertyDescriptor(Body.prototype, name)
Object.defineProperty(prototype, name, { ...desc, enumerable: true })
}
}
arrayBuffer () {
const buffer = consumeBody(this)
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
}
blob () {
const type = this.headers && this.headers.get('content-type')
return new Blob([consumeBody(this)], type && { type })
}
text () {
return consumeBody(this).toString()
}
json () {
try {
return JSON.parse(consumeBody(this).toString())
} catch (err) {
throw new TypeError(`invalid json response body at ${this.url} reason: ${err.message}`, 'invalid-json')
}
}
buffer () {
return consumeBody(this).clone()
}
}
function checkBody (body) {
if (body.bodyUsed) {
throw new TypeError(`body used already for: ${body.url}`)
}
}
function consumeBody (body) {
checkBody(body)
body[INTERNALS].bodyUsed = true
return body[INTERNALS].body || Buffer.alloc(0)
}
Body.mixin(SyncRequest.prototype)
Body.mixin(SyncResponse.prototype)
class Headers {
constructor (headers) {
if (headers instanceof syncFetch.Headers) {
this[INTERNALS] = { ...headers[INTERNALS] }
} else {
this[INTERNALS] = {}
if (Array.isArray(headers)) {
for (const [name, value] of headers) {
this.append(name, value)
}
} else if (typeof headers === 'object') {
for (const name in headers) {
this.set(name, headers[name])
}
}
}
}
// modification
append (name, value) {
name = name.toLowerCase()
if (!this[INTERNALS][name]) {
this[INTERNALS][name] = []
}
this[INTERNALS][name].push(value)
}
delete (name) {
delete this[INTERNALS][name.toLowerCase()]
}
set (name, value) {
this[INTERNALS][name.toLowerCase()] = [value]
}
// access
entries () {
const pairs = []
for (const name in this[INTERNALS]) {
for (const value of this[INTERNALS][name]) {
pairs.push([name, value])
}
}
return pairs
}
get (name) {
name = name.toLowerCase()
return name in this[INTERNALS] ? this[INTERNALS][name].join(', ') : null
}
keys () {
return Object.keys(this[INTERNALS])
}
has (name) {
return name.toLowerCase() in this[INTERNALS]
}
values () {
const values = []
for (const name in this[INTERNALS]) {
for (const value of this[INTERNALS][name]) {
values.push(value)
}
}
return values
}
* [Symbol.iterator] () {
for (const name in this[INTERNALS]) {
for (const value of this[INTERNALS][name]) {
yield [name, value]
}
}
}
}
syncFetch.Headers = Headers
syncFetch.Request = SyncRequest
syncFetch.Response = SyncResponse
module.exports = syncFetch