feat(http): better binary support & XHR support (#7707)

* feat(http): binary upload support

* feat(http): better binary support & XHR support

* fix: linting issue

* chore: moved files from old place to the new one

* chore: Updated NativeScript.api.md

* feat(http): support both ByteBuffer and String

Co-authored-by: Vasil Trifonov <v.trifonov@gmail.com>
This commit is contained in:
Stefan Andres Charsley
2020-01-29 02:22:32 +13:00
committed by Vasil Trifonov
parent a311a922b5
commit e293367dfc
23 changed files with 1249 additions and 486 deletions

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014-2015 GitHub, Inc.
Copyright (c) 2014-2016 GitHub, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -1,336 +1,539 @@
(function () {
'use strict';
(function () {
"use strict";
exports.XMLHttpRequest = global.XMLHttpRequest;
exports.FormData = global.FormData;
var support = {
searchParams: "URLSearchParams" in global,
iterable: "Symbol" in global && "iterator" in Symbol,
blob:
"FileReader" in global &&
"Blob" in global &&
(function () {
try {
new Blob();
return true;
} catch (e) {
return false;
}
})(),
formData: "FormData" in global,
arrayBuffer: "ArrayBuffer" in global
};
if (!exports.XMLHttpRequest) {
var xhr = require("../xhr");
exports.XMLHttpRequest = xhr.XMLHttpRequest;
exports.FormData = xhr.FormData;
function isDataView(obj) {
return obj && DataView.prototype.isPrototypeOf(obj);
}
if (support.arrayBuffer) {
var viewClasses = [
"[object Int8Array]",
"[object Uint8Array]",
"[object Uint8ClampedArray]",
"[object Int16Array]",
"[object Uint16Array]",
"[object Int32Array]",
"[object Uint32Array]",
"[object Float32Array]",
"[object Float64Array]"
];
var isArrayBufferView =
ArrayBuffer.isView ||
function (obj) {
return (
obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
);
};
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = name.toString();
if (typeof name !== "string") {
name = String(name);
}
if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
throw new TypeError('Invalid character in header field name')
if (/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(name)) {
throw new TypeError("Invalid character in header field name");
}
return name.toLowerCase()
return name.toLowerCase();
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = value.toString();
if (typeof value !== "string") {
value = String(value);
}
return value
return value;
}
// Build a destructive iterator for the value list
function iteratorFor(items) {
var iterator = {
next: function () {
var value = items.shift();
return { done: value === undefined, value: value };
}
};
if (support.iterable) {
iterator[Symbol.iterator] = function () {
return iterator;
};
}
return iterator;
}
function Headers(headers) {
this.map = {}
this.map = {};
if (headers instanceof Headers) {
headers.forEach(function (value, name) {
this.append(name, value)
}, this)
this.append(name, value);
}, this);
} else if (Array.isArray(headers)) {
headers.forEach(function (header) {
this.append(header[0], header[1]);
}, this);
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function (name) {
this.append(name, headers[name])
}, this)
this.append(name, headers[name]);
}, this);
}
}
Headers.prototype.append = function (name, value) {
name = normalizeName(name)
value = normalizeValue(value)
var list = this.map[name]
if (!list) {
list = []
this.map[name] = list
}
list.push(value)
}
name = normalizeName(name);
value = normalizeValue(value);
var oldValue = this.map[name];
this.map[name] = oldValue ? oldValue + ", " + value : value;
};
Headers.prototype['delete'] = function (name) {
delete this.map[normalizeName(name)]
}
Headers.prototype["delete"] = function (name) {
delete this.map[normalizeName(name)];
};
Headers.prototype.get = function (name) {
var values = this.map[normalizeName(name)]
return values ? values[0] : null
}
Headers.prototype.getAll = function (name) {
return this.map[normalizeName(name)] || []
}
name = normalizeName(name);
return this.has(name) ? this.map[name] : null;
};
Headers.prototype.has = function (name) {
return this.map.hasOwnProperty(normalizeName(name))
}
return this.map.hasOwnProperty(normalizeName(name));
};
Headers.prototype.set = function (name, value) {
this.map[normalizeName(name)] = [normalizeValue(value)]
}
this.map[normalizeName(name)] = normalizeValue(value);
};
Headers.prototype.forEach = function (callback, thisArg) {
Object.getOwnPropertyNames(this.map).forEach(function (name) {
this.map[name].forEach(function (value) {
callback.call(thisArg, value, name, this)
}, this)
}, this)
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this);
}
}
};
Headers.prototype.keys = function () {
var items = [];
this.forEach(function (value, name) {
items.push(name);
});
return iteratorFor(items);
};
Headers.prototype.values = function () {
var items = [];
this.forEach(function (value) {
items.push(value);
});
return iteratorFor(items);
};
Headers.prototype.entries = function () {
var items = [];
this.forEach(function (value, name) {
items.push([name, value]);
});
return iteratorFor(items);
};
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
}
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
return Promise.reject(new TypeError("Already read"));
}
body.bodyUsed = true
body.bodyUsed = true;
}
function fileReaderReady(reader) {
return new Promise(function (resolve, reject) {
reader.onload = function () {
resolve(reader.result)
}
resolve(reader.result);
};
reader.onerror = function () {
reject(reader.error)
}
})
reject(reader.error);
};
});
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader()
reader.readAsArrayBuffer(blob)
return fileReaderReady(reader)
var reader = new FileReader();
var promise = fileReaderReady(reader);
reader.readAsArrayBuffer(blob);
return promise;
}
function readBlobAsText(blob) {
var reader = new FileReader()
reader.readAsText(blob)
return fileReaderReady(reader)
var reader = new FileReader();
var promise = fileReaderReady(reader);
reader.readAsText(blob);
return promise;
}
var support = {
blob: 'FileReader' in exports && 'Blob' in exports && (function () {
try {
new Blob();
return true
} catch (e) {
return false
}
})(),
formData: 'FormData' in exports
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf);
var chars = new Array(view.length);
for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i]);
}
return chars.join("");
}
function bufferClone(buf) {
if (buf.slice) {
return buf.slice(0);
} else {
var view = new Uint8Array(buf.byteLength);
view.set(new Uint8Array(buf));
return view.buffer;
}
}
function Body() {
this.bodyUsed = false
this.bodyUsed = false;
this._initBody = function (body) {
this._bodyInit = body
if (typeof body === 'string') {
this._bodyText = body
this._bodyInit = body;
if (!body) {
this._bodyText = "";
} else if (typeof body === "string") {
this._bodyText = body;
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body
} else if (support.formData && exports.FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body
} else if (!body) {
this._bodyText = ''
this._bodyBlob = body;
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body;
} else if (
support.searchParams &&
URLSearchParams.prototype.isPrototypeOf(body)
) {
this._bodyText = body.toString();
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer);
// IE 10-11 can't handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer]);
} else if (
support.arrayBuffer &&
(ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))
) {
this._bodyArrayBuffer = bufferClone(body);
} else {
throw new Error('unsupported BodyInit type')
this._bodyText = body = Object.prototype.toString.call(body);
}
}
if (!this.headers.get("content-type")) {
if (typeof body === "string") {
this.headers.set("content-type", "text/plain;charset=UTF-8");
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set("content-type", this._bodyBlob.type);
} else if (
support.searchParams &&
URLSearchParams.prototype.isPrototypeOf(body)
) {
this.headers.set(
"content-type",
"application/x-www-form-urlencoded;charset=UTF-8"
);
}
}
};
if (support.blob) {
this.blob = function () {
var rejected = consumed(this)
var rejected = consumed(this);
if (rejected) {
return rejected
return rejected;
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
return Promise.resolve(this._bodyBlob);
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]));
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
throw new Error("could not read FormData body as blob");
} else {
return Promise.resolve(new Blob([this._bodyText]))
return Promise.resolve(new Blob([this._bodyText]));
}
}
};
this.arrayBuffer = function () {
return this.blob().then(readBlobAsArrayBuffer)
}
this.text = function () {
var rejected = consumed(this)
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
if (this._bodyArrayBuffer) {
return consumed(this) || Promise.resolve(this._bodyArrayBuffer);
} else {
return Promise.resolve(this._bodyText)
return this.blob().then(readBlobAsArrayBuffer);
}
}
} else {
this.text = function () {
var rejected = consumed(this)
return rejected ? rejected : Promise.resolve(this._bodyText)
}
};
}
this.text = function () {
var rejected = consumed(this);
if (rejected) {
return rejected;
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob);
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer));
} else if (this._bodyFormData) {
throw new Error("could not read FormData body as text");
} else {
return Promise.resolve(this._bodyText);
}
};
if (support.formData) {
this.formData = function () {
return this.text().then(decode)
}
return this.text().then(decode);
};
}
this.json = function () {
return this.text().then(JSON.parse)
}
return this.text().then(JSON.parse);
};
return this
return this;
}
// HTTP methods whose capitalization should be normalized
var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
var methods = ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"];
function normalizeMethod(method) {
var upcased = method.toUpperCase()
return (methods.indexOf(upcased) > -1) ? upcased : method
var upcased = method.toUpperCase();
return methods.indexOf(upcased) > -1 ? upcased : method;
}
function Request(url, options) {
options = options || {}
this.url = url
function Request(input, options) {
options = options || {};
var body = options.body;
this.credentials = options.credentials || 'omit'
this.headers = new Headers(options.headers)
this.method = normalizeMethod(options.method || 'GET')
this.mode = options.mode || null
this.referrer = null
if ((this.method === 'GET' || this.method === 'HEAD') && options.body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError("Already read");
}
this.url = input.url;
this.credentials = input.credentials;
if (!options.headers) {
this.headers = new Headers(input.headers);
}
this.method = input.method;
this.mode = input.mode;
this.signal = input.signal;
if (!body && input._bodyInit != null) {
body = input._bodyInit;
input.bodyUsed = true;
}
} else {
this.url = String(input);
}
this._initBody(options.body)
this.credentials = options.credentials || this.credentials || "same-origin";
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers);
}
this.method = normalizeMethod(options.method || this.method || "GET");
this.mode = options.mode || this.mode || null;
this.signal = options.signal || this.signal;
this.referrer = null;
if ((this.method === "GET" || this.method === "HEAD") && body) {
throw new TypeError("Body not allowed for GET or HEAD requests");
}
this._initBody(body);
}
Request.prototype.clone = function () {
return new Request(this, { body: this._bodyInit });
};
function decode(body) {
var form = new exports.FormData()
body.trim().split('&').forEach(function (bytes) {
if (bytes) {
var split = bytes.split('=')
var name = split.shift().replace(/\+/g, ' ')
var value = split.join('=').replace(/\+/g, ' ')
form.append(decodeURIComponent(name), decodeURIComponent(value))
var form = new FormData();
body
.trim()
.split("&")
.forEach(function (bytes) {
if (bytes) {
var split = bytes.split("=");
var name = split.shift().replace(/\+/g, " ");
var value = split.join("=").replace(/\+/g, " ");
form.append(decodeURIComponent(name), decodeURIComponent(value));
}
});
return form;
}
function parseHeaders(rawHeaders) {
var headers = new Headers();
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, " ");
preProcessedHeaders.split(/\r?\n/).forEach(function (line) {
var parts = line.split(":");
var key = parts.shift().trim();
if (key) {
var value = parts.join(":").trim();
headers.append(key, value);
}
})
return form
});
return headers;
}
function headers(xhr) {
var head = new Headers()
var pairs = xhr.getAllResponseHeaders().trim().split('\n')
pairs.forEach(function (header) {
var split = header.trim().split(':')
var key = split.shift().trim()
var value = split.join(':').trim()
head.append(key, value)
})
return head
}
Body.call(Request.prototype)
Body.call(Request.prototype);
function Response(bodyInit, options) {
if (!options) {
options = {}
options = {};
}
this._initBody(bodyInit)
this.type = 'default'
this.url = null
this.status = options.status
this.ok = this.status >= 200 && this.status < 300
this.statusText = options.statusText
this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers)
this.url = options.url || ''
this.type = "default";
this.status = options.status === undefined ? 200 : options.status;
this.ok = this.status >= 200 && this.status < 300;
this.statusText = "statusText" in options ? options.statusText : "OK";
this.headers = new Headers(options.headers);
this.url = options.url || "";
this._initBody(bodyInit);
}
Body.call(Response.prototype)
Body.call(Response.prototype);
Response.prototype.clone = function () {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
});
};
Response.error = function () {
var response = new Response(null, { status: 0, statusText: "" });
response.type = "error";
return response;
};
var redirectStatuses = [301, 302, 303, 307, 308];
Response.redirect = function (url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError("Invalid status code");
}
return new Response(null, { status: status, headers: { location: url } });
};
exports.DOMException = global.DOMException;
try {
new exports.DOMException();
} catch (err) {
exports.DOMException = function (message, name) {
this.message = message;
this.name = name;
var error = Error(message);
this.stack = error.stack;
};
exports.DOMException.prototype = Object.create(Error.prototype);
exports.DOMException.prototype.constructor = exports.DOMException;
}
function fetch(input, init) {
return new Promise(function (resolve, reject) {
var request = new Request(input, init);
if (request.signal && request.signal.aborted) {
return reject(new exports.DOMException("Aborted", "AbortError"));
}
var xhr = new XMLHttpRequest();
function abortXhr() {
xhr.abort();
}
xhr.onload = function () {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || "")
};
options.url =
"responseURL" in xhr
? xhr.responseURL
: options.headers.get("X-Request-URL");
var body = "response" in xhr ? xhr.response : xhr.responseText;
resolve(new Response(body, options));
};
xhr.onerror = function () {
reject(new TypeError("Network request failed"));
};
xhr.ontimeout = function () {
reject(new TypeError("Network request failed"));
};
xhr.onabort = function () {
reject(new exports.DOMException("Aborted", "AbortError"));
};
xhr.open(request.method, request.url, true);
if (request.credentials === "include") {
xhr.withCredentials = true;
} else if (request.credentials === "omit") {
xhr.withCredentials = false;
}
if ("responseType" in xhr && support.blob) {
xhr.responseType = "blob";
}
request.headers.forEach(function (value, name) {
xhr.setRequestHeader(name, value);
});
if (request.signal) {
request.signal.addEventListener("abort", abortXhr);
xhr.onreadystatechange = function () {
// DONE (success or failure)
if (xhr.readyState === 4) {
request.signal.removeEventListener("abort", abortXhr);
}
};
}
xhr.send(
typeof request._bodyInit === "undefined" ? null : request._bodyInit
);
});
}
fetch.polyfill = true;
exports.Headers = Headers;
exports.Request = Request;
exports.Response = Response;
exports.fetch = fetch;
exports.fetch = function (input, init) {
// TODO: Request constructor should accept input, init
var request
if (Request.prototype.isPrototypeOf(input) && !init) {
request = input
} else {
request = new Request(input, init)
}
return new Promise(function (resolve, reject) {
var xhr = new exports.XMLHttpRequest()
function responseURL() {
if ('responseURL' in xhr) {
return xhr.responseURL
}
// Avoid security warnings on getResponseHeader when not allowed by CORS
if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
return xhr.getResponseHeader('X-Request-URL')
}
return;
}
xhr.onload = function () {
var status = (xhr.status === 1223) ? 204 : xhr.status
if (status < 100 || status > 599) {
reject(new TypeError('Network request failed'))
return
}
var options = {
status: status,
statusText: xhr.statusText,
headers: headers(xhr),
url: responseURL()
}
//var body = 'response' in xhr ? xhr.response : xhr.responseText;
resolve(new Response(xhr.responseText, options))
}
xhr.onerror = function (error) {
reject(new TypeError(['Network request failed:', error.message].join(' ')))
}
xhr.open(request.method, request.url, true)
if (request.credentials === 'include') {
xhr.withCredentials = true
}
if ('responseType' in xhr && support.blob) {
xhr.responseType = 'blob'
}
request.headers.forEach(function (value, name) {
xhr.setRequestHeader(name, value)
})
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
})
}
exports.fetch.polyfill = true
})();
Object.defineProperty(exports, "__esModule", { value: true });
})();

View File

@@ -3,6 +3,7 @@ import "./core";
import "./polyfills/timers";
import "./polyfills/animation";
import "./polyfills/dialogs";
import "./polyfills/text";
import "./polyfills/xhr";
import "./polyfills/fetch";

View File

@@ -0,0 +1,5 @@
{
"name" : "text",
"main" : "text",
"nativescript": {}
}

View File

@@ -0,0 +1,8 @@
import "../../core";
import "../../polyfills/xhr";
import { installPolyfills } from "../polyfill-helpers";
global.registerModule("text", () => require("../../../text"));
installPolyfills("text", ["TextDecoder", "TextEncoder"]);

View File

@@ -3,4 +3,4 @@ import { installPolyfills } from "../polyfill-helpers";
global.registerModule("xhr", () => require("../../../xhr"));
installPolyfills("xhr", ["XMLHttpRequest", "FormData"]);
installPolyfills("xhr", ["XMLHttpRequest", "FormData", "Blob", "File", "FileReader"]);

View File

@@ -92,6 +92,7 @@ function onRequestComplete(requestId: number, result: org.nativescript.widgets.A
callbacks.resolveCallback({
content: {
raw: result.raw,
toArrayBuffer: () => Uint8Array.from(result.raw.toByteArray()).buffer,
toString: (encoding?: HttpResponseEncoding) => {
let str: string;
if (encoding) {
@@ -180,7 +181,14 @@ function buildJavaOptions(options: httpModule.HttpRequestOptions) {
javaOptions.method = options.method;
}
if (typeof options.content === "string" || options.content instanceof FormData) {
javaOptions.content = options.content.toString();
const nativeString = new java.lang.String(options.content.toString());
const nativeBytes = nativeString.getBytes("UTF-8");
const nativeBuffer = java.nio.ByteBuffer.wrap(nativeBytes);
javaOptions.content = nativeBuffer;
} else if (options.content instanceof ArrayBuffer) {
const typedArray = new Uint8Array(options.content as ArrayBuffer);
const nativeBuffer = java.nio.ByteBuffer.wrap(Array.from(typedArray));
javaOptions.content = nativeBuffer;
}
if (typeof options.timeout === "number") {
javaOptions.timeout = options.timeout;

View File

@@ -95,6 +95,9 @@ export function request(options: httpModule.HttpRequestOptions): Promise<httpMod
if (types.isString(options.content) || options.content instanceof FormData) {
urlRequest.HTTPBody = NSString.stringWithString(options.content.toString()).dataUsingEncoding(4);
} else if (options.content instanceof ArrayBuffer) {
const buffer = options.content as ArrayBuffer;
urlRequest.HTTPBody = NSData.dataWithData(buffer as any);
}
if (types.isNumber(options.timeout)) {
@@ -142,7 +145,15 @@ export function request(options: httpModule.HttpRequestOptions): Promise<httpMod
resolve({
content: {
raw: data,
toString: (encoding?: any) => NSDataToString(data, encoding),
toArrayBuffer: () => interop.bufferFromData(data),
toString: (encoding?: any) => {
const str = NSDataToString(data, encoding);
if (typeof str === "string") {
return str;
} else {
throw new Error("Response content may not be converted to string");
}
},
toJSON: (encoding?: any) => parseJSON(NSDataToString(data, encoding)),
toImage: () => {
ensureImageSource();

View File

@@ -56,6 +56,18 @@ export function getFile(url: string, destinationFilePath?: string): Promise<File
*/
export function getFile(options: HttpRequestOptions, destinationFilePath?: string): Promise<File>;
/**
* Downloads the content from the specified URL as binary and returns an ArrayBuffer.
* @param url The URL to request from.
*/
export function getBinary(url: string): Promise<ArrayBuffer>;
/**
* Downloads the content from the specified URL as binary and returns an ArrayBuffer.
* @param options An object that specifies various request options.
*/
export function getBinary(options: HttpRequestOptions): Promise<ArrayBuffer>;
/**
* Makes a generic http request using the provided options and returns a HttpResponse Object.
* @param options An object that specifies various request options.
@@ -84,7 +96,7 @@ export interface HttpRequestOptions {
/**
* Gets or sets the request body.
*/
content?: string | FormData;
content?: string | FormData | ArrayBuffer;
/**
* Gets or sets the request timeout in milliseconds.
@@ -132,6 +144,11 @@ export interface HttpContent {
*/
raw: any;
/**
* Gets the response body as ArrayBuffer
*/
toArrayBuffer: () => ArrayBuffer;
/**
* Gets the response body as string.
*/

View File

@@ -58,3 +58,17 @@ export function getFile(arg: any, destinationFilePath?: string): Promise<any> {
}, e => reject(e));
});
}
export function getBinary(arg: any): Promise<ArrayBuffer> {
return new Promise<ArrayBuffer>((resolve, reject) => {
httpRequest.request(typeof arg === "string" ? { url: arg, method: "GET" } : arg)
.then(r => {
try {
const arrayBuffer = r.content.toArrayBuffer();
resolve(arrayBuffer);
} catch (e) {
reject(e);
}
}, e => reject(e));
});
}

View File

@@ -0,0 +1,112 @@
const Object_prototype_toString = ({}).toString;
const ArrayBufferString = Object_prototype_toString.call(ArrayBuffer.prototype);
function decoderReplacer(encoded) {
var codePoint = encoded.charCodeAt(0) << 24;
var leadingOnes = Math.clz32(~codePoint) | 0;
var endPos = 0, stringLen = encoded.length | 0;
var result = "";
if (leadingOnes < 5 && stringLen >= leadingOnes) {
codePoint = (codePoint << leadingOnes) >>> (24 + leadingOnes);
for (endPos = 1; endPos < leadingOnes; endPos = endPos + 1 | 0) {
codePoint = (codePoint << 6) | (encoded.charCodeAt(endPos) & 0x3f/*0b00111111*/);
}
if (codePoint <= 0xFFFF) { // BMP code point
result += String.fromCharCode(codePoint);
} else if (codePoint <= 0x10FFFF) {
// https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
codePoint = codePoint - 0x10000 | 0;
result += String.fromCharCode(
(codePoint >> 10) + 0xD800 | 0, // highSurrogate
(codePoint & 0x3ff) + 0xDC00 | 0 // lowSurrogate
);
} else { endPos = 0; } // to fill it in with INVALIDs
}
for (; endPos < stringLen; endPos = endPos + 1 | 0) { result += "\ufffd"; }
return result;
}
function encoderReplacer(nonAsciiChars) {
// make the UTF string into a binary UTF-8 encoded string
var point = nonAsciiChars.charCodeAt(0) | 0;
if (point >= 0xD800 && point <= 0xDBFF) {
var nextcode = nonAsciiChars.charCodeAt(1) | 0;
if (nextcode !== nextcode) { // NaN because string is 1 code point long
return String.fromCharCode(0xef/*11101111*/, 0xbf/*10111111*/, 0xbd/*10111101*/);
}
// https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
if (nextcode >= 0xDC00 && nextcode <= 0xDFFF) {
point = ((point - 0xD800) << 10) + nextcode - 0xDC00 + 0x10000 | 0;
if (point > 0xffff) {
return String.fromCharCode(
(0x1e /*0b11110*/ << 3) | (point >>> 18),
(0x2 /*0b10*/ << 6) | ((point >>> 12) & 0x3f/*0b00111111*/),
(0x2 /*0b10*/ << 6) | ((point >>> 6) & 0x3f/*0b00111111*/),
(0x2 /*0b10*/ << 6) | (point & 0x3f/*0b00111111*/)
);
}
} else { return String.fromCharCode(0xef, 0xbf, 0xbd); }
}
if (point <= 0x007f) { return nonAsciiChars; }
else if (point <= 0x07ff) {
return String.fromCharCode((0x6 << 5) | (point >>> 6), (0x2 << 6) | (point & 0x3f));
} else {
return String.fromCharCode(
(0xe /*0b1110*/ << 4) | (point >>> 12),
(0x2 /*0b10*/ << 6) | ((point >>> 6) & 0x3f/*0b00111111*/),
(0x2 /*0b10*/ << 6) | (point & 0x3f/*0b00111111*/)
);
}
}
export class TextDecoder {
public get encoding() {
return "utf-8";
}
public decode(input: BufferSource): string {
const buffer = ArrayBuffer.isView(input) ? input.buffer : input;
if (Object_prototype_toString.call(buffer) !== ArrayBufferString) {
throw Error("Failed to execute 'decode' on 'TextDecoder': The provided value is not of type '(ArrayBuffer or ArrayBufferView)'");
}
let inputAs8 = new Uint8Array(buffer);
let resultingString = "";
for (let index = 0, len = inputAs8.length | 0; index < len; index = index + 32768 | 0) {
resultingString += String.fromCharCode.apply(0, inputAs8.slice(index, index + 32768 | 0));
}
return resultingString.replace(/[\xc0-\xff][\x80-\xbf]*/g, decoderReplacer);
}
public toString() {
return "[object TextDecoder]";
}
[Symbol.toStringTag] = "TextDecoder";
}
export class TextEncoder {
public get encoding() {
return "utf-8";
}
public encode(input: string = ""): Uint8Array {
// 0xc0 => 0b11000000; 0xff => 0b11111111; 0xc0-0xff => 0b11xxxxxx
// 0x80 => 0b10000000; 0xbf => 0b10111111; 0x80-0xbf => 0b10xxxxxx
const encodedString = input === undefined ? "" : ("" + input).replace(/[\x80-\uD7ff\uDC00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]?/g, encoderReplacer);
const len = encodedString.length | 0, result = new Uint8Array(len);
for (let i = 0; i < len; i = i + 1 | 0) {
result[i] = encodedString.charCodeAt(i);
}
return result;
}
public toString() {
return "[object TextEncoder]";
}
[Symbol.toStringTag] = "TextEncoder";
}

View File

@@ -1,4 +1,6 @@
export module encoding {
export * from "./text-common";
export module encoding {
export const ISO_8859_1 = "ISO-8859-1";
export const US_ASCII = "US-ASCII";
export const UTF_16 = "UTF-16";

View File

@@ -3,6 +3,8 @@
* @module "text"
*/ /** */
export * from "./text-common";
/**
* Defines the supported character encodings.
*/

View File

@@ -1,4 +1,6 @@
export module encoding {
export * from "./text-common";
export module encoding {
export const ISO_8859_1 = 5; //NSISOLatin1StringEncoding
export const US_ASCII = 1; //NSASCIIStringEncoding
export const UTF_16 = 10; //NSUnicodeStringEncoding

View File

@@ -5,6 +5,8 @@ module XMLHttpRequestResponseType {
export const empty = "";
export const text = "text";
export const json = "json";
export const blob = "blob";
export const arraybuffer = "arraybuffer";
}
export class XMLHttpRequest {
@@ -14,8 +16,12 @@ export class XMLHttpRequest {
public LOADING = 3;
public DONE = 4;
public onload: () => void;
public onerror: (any) => void;
public onabort: (...args: any[]) => void;
public onerror: (...args: any[]) => void;
public onload: (...args: any[]) => void;
public onloadend: (...args: any[]) => void;
public onloadstart: (...args: any[]) => void;
public onprogress: (...args: any[]) => void;
private _options: http.HttpRequestOptions;
private _readyState: number;
@@ -24,14 +30,168 @@ export class XMLHttpRequest {
private _responseTextReader: Function;
private _headers: any;
private _errorFlag: boolean;
private _sendFlag: boolean;
private _responseType: string = "";
private _overrideMimeType: string;
private _listeners: Map<string, Array<Function>> = new Map<string, Array<Function>>();
public onreadystatechange: Function;
public get upload() {
return this;
}
public get readyState(): number {
return this._readyState;
}
public get responseType(): string {
return this._responseType;
}
public set responseType(value: string) {
if (value === XMLHttpRequestResponseType.empty
|| value in XMLHttpRequestResponseType) {
this._responseType = value;
} else {
throw new Error(`Response type of '${value}' not supported.`);
}
}
public get responseText(): string {
if (this._responseType !== XMLHttpRequestResponseType.empty
&& this._responseType !== XMLHttpRequestResponseType.text) {
throw new Error(
"Failed to read the 'responseText' property from 'XMLHttpRequest': " +
"The value is only accessible if the object's 'responseType' is '' or 'text' " +
`(was '${this._responseType}').`
);
}
if (types.isFunction(this._responseTextReader)) {
return this._responseTextReader();
}
return "";
}
public get response(): any {
if (this._responseType === XMLHttpRequestResponseType.empty
|| this._responseType === XMLHttpRequestResponseType.text) {
if (this._readyState !== this.LOADING && this._readyState !== this.DONE) {
return "";
} else {
return this._response;
}
} else {
if (this._readyState !== this.DONE) {
return null;
} else {
return this._response;
}
}
}
public get status(): number {
return this._status;
}
public get statusText(): string {
if (this._readyState === this.UNSENT
|| this._readyState === this.OPENED
|| this._errorFlag) {
return "";
}
return statuses[this._status];
}
constructor() {
this._readyState = this.UNSENT;
}
private _loadResponse(r: http.HttpResponse) {
this._status = r.statusCode;
this._headers = r.headers;
this._setReadyState(this.HEADERS_RECEIVED);
this._setReadyState(this.LOADING);
this._responseTextReader = () => r.content.toString();
const contentType = this.getResponseHeader("Content-Type");
const mimeType = (contentType && contentType.toLowerCase()) || "text/xml";
const finalMimeType = this._overrideMimeType || mimeType;
if (this._responseType === XMLHttpRequestResponseType.json) {
this._response = r.content.toJSON();
} else if (this._responseType === XMLHttpRequestResponseType.text
|| this._responseType === XMLHttpRequestResponseType.empty) {
this._response = this.responseText;
} else if (this._responseType === XMLHttpRequestResponseType.arraybuffer) {
this._response = r.content.toArrayBuffer();
} else if (this._responseType === XMLHttpRequestResponseType.blob) {
this._response = new Blob([r.content.toArrayBuffer()], { type: finalMimeType });
}
this.emitEvent("progress");
this._sendFlag = false;
this._setReadyState(this.DONE);
}
private emitEvent(eventName: string, ...args: Array<any>) {
if (types.isFunction(this["on" + eventName])) {
this["on" + eventName](...args);
}
let handlers = this._listeners.get(eventName) || [];
handlers.forEach((handler) => {
handler(...args);
});
}
private _setReadyState(value: number) {
if (this._readyState !== value) {
this._readyState = value;
this.emitEvent("readystatechange");
}
if (this._readyState === this.DONE) {
this.emitEvent("load");
this.emitEvent("loadend");
}
}
private _setRequestError(eventName: string, error?: any) {
this._readyState = this.DONE;
this._response = error;
this.emitEvent("readystatechange");
this.emitEvent(eventName, error);
this.emitEvent("loadend");
}
public addEventListener(eventName: string, handler: Function) {
if (["abort", "error", "load", "loadend", "loadstart", "progress"].indexOf(eventName) === -1) {
throw new Error("Event not supported: " + eventName);
}
let handlers = this._listeners.get(eventName) || [];
handlers.push(handler);
this._listeners.set(eventName, handlers);
}
public removeEventListener(eventName: string, toDetach: Function) {
let handlers = this._listeners.get(eventName) || [];
handlers = handlers.filter((handler) => handler !== toDetach);
this._listeners.set(eventName, handlers);
}
public open(method: string, url: string, async?: boolean, user?: string, password?: string) {
if (types.isString(method) && types.isString(url)) {
this._options = { url: url, method: method };
@@ -50,17 +210,21 @@ export class XMLHttpRequest {
}
public abort() {
this._errorFlag = true;
this._response = null;
this._responseTextReader = null;
this._headers = null;
this._status = null;
if (this._readyState === this.UNSENT || this._readyState === this.OPENED || this._readyState === this.DONE) {
if ((this._readyState === this.OPENED && this._sendFlag)
|| this._readyState === this.HEADERS_RECEIVED
|| this._readyState === this.LOADING) {
this._errorFlag = true;
this._sendFlag = false;
this._setRequestError("abort");
}
if (this._readyState === this.DONE) {
this._readyState = this.UNSENT;
} else {
this._setReadyState(this.DONE);
}
}
@@ -71,125 +235,51 @@ export class XMLHttpRequest {
this._headers = null;
this._status = null;
if (types.isDefined(this._options)) {
if (types.isString(data) && this._options.method !== "GET") {
//The Android Java HTTP lib throws an exception if we provide a
//a request body for GET requests, so we avoid doing that.
//Browser implementations silently ignore it as well.
this._options.content = data;
} else if (data instanceof FormData) {
this._options.content = (<FormData>data).toString();
if (this._readyState !== this.OPENED || this._sendFlag) {
throw new Error(
"Failed to execute 'send' on 'XMLHttpRequest': " +
"The object's state must be OPENED."
);
}
if (types.isString(data) && this._options.method !== "GET") {
//The Android Java HTTP lib throws an exception if we provide a
//a request body for GET requests, so we avoid doing that.
//Browser implementations silently ignore it as well.
this._options.content = data;
} else if (data instanceof FormData) {
this._options.content = (<FormData>data).toString();
} else if (data instanceof Blob) {
this.setRequestHeader("Content-Type", data.type);
this._options.content = Blob.InternalAccessor.getBuffer(data);
} else if (data instanceof ArrayBuffer) {
this._options.content = data;
}
this._sendFlag = true;
this.emitEvent("loadstart");
http.request(this._options).then(r => {
if (!this._errorFlag && this._sendFlag) {
this._loadResponse(r);
}
http.request(this._options).then(r => {
if (!this._errorFlag) {
this._loadResponse(r);
}
}).catch(e => {
this._errorFlag = true;
this._setReadyState(this.DONE, e);
});
}
}
private _loadResponse(r) {
this._status = r.statusCode;
this._response = r.content.raw + "";
this._headers = r.headers;
this._setReadyState(this.HEADERS_RECEIVED);
this._setReadyState(this.LOADING);
this._setResponseType();
this._responseTextReader = () => r.content.toString();
this._addToStringOnResponse();
if (this.responseType === XMLHttpRequestResponseType.json) {
this._response = JSON.parse(this.responseText);
} else if (this.responseType === XMLHttpRequestResponseType.text) {
this._response = this.responseText;
}
this._setReadyState(this.DONE);
}
private _addToStringOnResponse() {
// Add toString() method to ease debugging and
// make Angular2 response.text() method work properly.
if (types.isObject(this.response)) {
Object.defineProperty(this._response, "toString", {
configurable: true,
enumerable: false,
writable: true,
value: () => this.responseText
});
}
}
private textTypes: string[] = [
"text/plain",
"application/xml",
"application/rss+xml",
"text/html",
"text/xml"
];
private isTextContentType(contentType: string): boolean {
let result = false;
for (let i = 0; i < this.textTypes.length; i++) {
if (contentType.toLowerCase().indexOf(this.textTypes[i]) >= 0) {
result = true;
break;
}
}
return result;
}
private _setResponseType() {
const header = this.getResponseHeader("Content-Type");
const contentType = header && header.toLowerCase();
if (contentType) {
if (contentType.indexOf("application/json") >= 0 || contentType.indexOf("+json") >= 0) {
this.responseType = XMLHttpRequestResponseType.json;
} else if (this.isTextContentType(contentType)) {
this.responseType = XMLHttpRequestResponseType.text;
}
} else {
this.responseType = XMLHttpRequestResponseType.text;
}
}
private _listeners: Map<string, Array<Function>> = new Map<string, Array<Function>>();
public addEventListener(eventName: string, handler: Function) {
if (eventName !== "load" && eventName !== "error" && eventName !== "progress") {
throw new Error("Event not supported: " + eventName);
}
let handlers = this._listeners.get(eventName) || [];
handlers.push(handler);
this._listeners.set(eventName, handlers);
}
public removeEventListener(eventName: string, toDetach: Function) {
let handlers = this._listeners.get(eventName) || [];
handlers = handlers.filter((handler) => handler !== toDetach);
this._listeners.set(eventName, handlers);
}
private emitEvent(eventName: string, ...args: Array<any>) {
let handlers = this._listeners.get(eventName) || [];
handlers.forEach((handler) => {
handler(...args);
}).catch(e => {
this._errorFlag = true;
this._sendFlag = false;
this._setRequestError("error", e);
});
}
public setRequestHeader(header: string, value: string) {
if (types.isDefined(this._options) && types.isString(header) && types.isString(value)) {
if (this._readyState !== this.OPENED || this._sendFlag) {
throw new Error(
"Failed to execute 'setRequestHeader' on 'XMLHttpRequest': " +
"The object's state must be OPENED."
);
}
if (types.isString(header) && types.isString(value)) {
this._options.headers[header] = value;
}
}
@@ -225,71 +315,14 @@ export class XMLHttpRequest {
}
public overrideMimeType(mime: string) {
//
}
get readyState(): number {
return this._readyState;
}
public get responseType(): string {
return this._responseType;
}
public set responseType(value: string) {
if (value === XMLHttpRequestResponseType.empty || value in XMLHttpRequestResponseType) {
this._responseType = value;
} else {
throw new Error(`Response type of '${value}' not supported.`);
}
}
private _setReadyState(value: number, error?: any) {
if (this._readyState !== value) {
this._readyState = value;
if (types.isFunction(this.onreadystatechange)) {
this.onreadystatechange();
}
if (this._readyState === this.LOADING || this._readyState === this.DONE) {
throw new Error(
"Failed to execute 'overrideMimeType' on 'XMLHttpRequest': " +
"MimeType cannot be overridden when the state is LOADING or DONE."
);
}
if (this._readyState === this.DONE) {
if (this._errorFlag) {
if (types.isFunction(this.onerror)) {
this.onerror(error);
}
this.emitEvent("error", error);
} else {
if (types.isFunction(this.onload)) {
this.onload();
}
this.emitEvent("load");
}
}
}
get responseText(): string {
if (types.isFunction(this._responseTextReader)) {
return this._responseTextReader();
}
return "";
}
get response(): any {
return this._response;
}
get status(): number {
return this._status;
}
get statusText(): string {
if (this._readyState === this.UNSENT || this._readyState === this.OPENED || this._errorFlag) {
return "";
}
return statuses[this._status];
this._overrideMimeType = mime;
}
}
@@ -357,3 +390,257 @@ export class FormData {
return arr.join("&");
}
}
export class Blob {
// Note: only for use by XHR
public static InternalAccessor = class {
public static getBuffer(blob: Blob) {
return blob._buffer;
}
};
private _buffer: Uint8Array;
private _size: number;
private _type: string;
public get size() {
return this._size;
}
public get type() {
return this._type;
}
constructor(
chunks: Array<BufferSource | DataView | Blob | string> = [],
opts: { type?: string } = {}
) {
const dataChunks: Uint8Array[] = [];
for (const chunk of chunks) {
if (chunk instanceof Blob) {
dataChunks.push(chunk._buffer);
} else if (typeof chunk === "string") {
const textEncoder = new TextEncoder();
dataChunks.push(textEncoder.encode(chunk));
} else if (chunk instanceof DataView) {
dataChunks.push(new Uint8Array(chunk.buffer.slice(0)));
} else if (chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
dataChunks.push(new Uint8Array(
ArrayBuffer.isView(chunk)
? chunk.buffer.slice(0)
: chunk.slice(0)
));
} else {
const textEncoder = new TextEncoder();
dataChunks.push(textEncoder.encode(String(chunk)));
}
}
const size = dataChunks.reduce((size, chunk) => size + chunk.byteLength, 0);
const buffer = new Uint8Array(size);
let offset = 0;
for (let i = 0; i < dataChunks.length; i++) {
const chunk = dataChunks[i];
buffer.set(chunk, offset);
offset += chunk.byteLength;
}
this._buffer = buffer;
this._size = this._buffer.byteLength;
this._type = opts.type || "";
if (/[^\u0020-\u007E]/.test(this._type)) {
this._type = "";
} else {
this._type = this._type.toLowerCase();
}
}
public arrayBuffer(): Promise<ArrayBuffer> {
return Promise.resolve(this._buffer);
}
public text(): Promise<string> {
const textDecoder = new TextDecoder();
return Promise.resolve(textDecoder.decode(this._buffer));
}
public slice(start?: number, end?: number, type?: string): Blob {
const slice = this._buffer.slice(start || 0, end || this._buffer.length);
return new Blob([slice], { type: type });
}
public stream() {
throw new Error("stream is currently not supported");
}
public toString() {
return "[object Blob]";
}
[Symbol.toStringTag] = "Blob";
}
export class File extends Blob {
private _name: string;
private _lastModified: number;
public get name() {
return this._name;
}
public get lastModified() {
return this._lastModified;
}
constructor(
chunks: Array<BufferSource | DataView | Blob | string>,
name: string,
opts: { type?: string, lastModified?: number } = {}
) {
super(chunks, opts);
this._name = name.replace(/\//g, ":");
this._lastModified =
opts.lastModified
? new Date(opts.lastModified).valueOf()
: Date.now();
}
public toString() {
return "[object File]";
}
[Symbol.toStringTag] = "File";
}
export class FileReader {
public EMPTY = 0;
public LOADING = 1;
public DONE = 2;
public onabort: (...args: any[]) => void;
public onerror: (...args: any[]) => void;
public onload: (...args: any[]) => void;
public onloadend: (...args: any[]) => void;
public onloadstart: (...args: any[]) => void;
public onprogress: (...args: any[]) => void;
private _readyState: number;
private _result: string | ArrayBuffer | null;
private _listeners: Map<string, Array<Function>> = new Map<string, Array<Function>>();
public get readyState(): number {
return this._readyState;
}
public get result(): string | ArrayBuffer | null {
return this._result;
}
constructor() {
//
}
private _array2base64(input: Uint8Array): string {
var byteToCharMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = [];
for (var i = 0; i < input.length; i += 3) {
var byte1 = input[i];
var haveByte2 = i + 1 < input.length;
var byte2 = haveByte2 ? input[i + 1] : 0;
var haveByte3 = i + 2 < input.length;
var byte3 = haveByte3 ? input[i + 2] : 0;
var outByte1 = byte1 >> 2;
var outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4);
var outByte3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6);
var outByte4 = byte3 & 0x3F;
if (!haveByte3) {
outByte4 = 64;
if (!haveByte2) {
outByte3 = 64;
}
}
output.push(
byteToCharMap[outByte1], byteToCharMap[outByte2],
byteToCharMap[outByte3], byteToCharMap[outByte4]
);
}
return output.join("");
}
private _read(blob, kind) {
if (!(blob instanceof Blob)) {
throw new TypeError(`Failed to execute '${kind}' on 'FileReader': parameter 1 is not of type 'Blob'.`);
}
this._result = "";
setTimeout(() => {
this._readyState = this.LOADING;
this.emitEvent("load");
this.emitEvent("loadend");
});
}
private emitEvent(eventName: string, ...args: Array<any>) {
if (types.isFunction(this["on" + eventName])) {
this["on" + eventName](...args);
}
let handlers = this._listeners.get(eventName) || [];
handlers.forEach((handler) => {
handler(...args);
});
}
public addEventListener(eventName: string, handler: Function) {
if (["abort", "error", "load", "loadend", "loadstart", "progress"].indexOf(eventName) === -1) {
throw new Error("Event not supported: " + eventName);
}
let handlers = this._listeners.get(eventName) || [];
handlers.push(handler);
this._listeners.set(eventName, handlers);
}
public removeEventListener(eventName: string, toDetach: Function) {
let handlers = this._listeners.get(eventName) || [];
handlers = handlers.filter((handler) => handler !== toDetach);
this._listeners.set(eventName, handlers);
}
public readAsDataURL(blob: Blob) {
this._read(blob, "readAsDataURL");
this._result = `data:${blob.type};base64,${this._array2base64(Blob.InternalAccessor.getBuffer(blob))}`;
}
public readAsText(blob: Blob) {
this._read(blob, "readAsText");
const textDecoder = new TextDecoder();
this._result = textDecoder.decode(Blob.InternalAccessor.getBuffer(blob));
}
public readAsArrayBuffer(blob: Blob) {
this._read(blob, "readAsArrayBuffer");
this._result = Blob.InternalAccessor.getBuffer(blob).buffer.slice(0);
}
public abort() {
//
}
public toString() {
return "[object FileReader]";
}
[Symbol.toStringTag] = "FileReader";
}