1import {Metadata} from "../../metadata"; 2import {Transport, TransportFactory, TransportOptions} from "../Transport"; 3import {debug} from "../../debug"; 4import detach from "../../detach"; 5import {detectMozXHRSupport, detectXHROverrideMimeTypeSupport} from "./xhrUtil"; 6 7export interface XhrTransportInit { 8 withCredentials?: boolean 9} 10 11export function XhrTransport(init: XhrTransportInit): TransportFactory { 12 return (opts: TransportOptions) => { 13 if (detectMozXHRSupport()) { 14 return new MozChunkedArrayBufferXHR(opts, init); 15 } else if (detectXHROverrideMimeTypeSupport()) { 16 return new XHR(opts, init); 17 } else { 18 throw new Error("This environment's XHR implementation cannot support binary transfer."); 19 } 20 } 21} 22 23export class XHR implements Transport { 24 options: TransportOptions; 25 init: XhrTransportInit; 26 xhr: XMLHttpRequest; 27 metadata: Metadata; 28 index: 0; 29 30 constructor(transportOptions: TransportOptions, init: XhrTransportInit) { 31 this.options = transportOptions; 32 this.init = init; 33 } 34 35 onProgressEvent() { 36 this.options.debug && debug("XHR.onProgressEvent.length: ", this.xhr.response.length); 37 const rawText = this.xhr.response.substr(this.index); 38 this.index = this.xhr.response.length; 39 const asArrayBuffer = stringToArrayBuffer(rawText); 40 detach(() => { 41 this.options.onChunk(asArrayBuffer); 42 }); 43 } 44 45 onLoadEvent() { 46 this.options.debug && debug("XHR.onLoadEvent"); 47 detach(() => { 48 this.options.onEnd(); 49 }); 50 } 51 52 onStateChange() { 53 this.options.debug && debug("XHR.onStateChange", this.xhr.readyState); 54 if (this.xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { 55 detach(() => { 56 this.options.onHeaders(new Metadata(this.xhr.getAllResponseHeaders()), this.xhr.status); 57 }); 58 } 59 } 60 61 sendMessage(msgBytes: Uint8Array) { 62 this.xhr.send(msgBytes); 63 } 64 65 finishSend() { 66 } 67 68 start(metadata: Metadata) { 69 this.metadata = metadata; 70 71 const xhr = new XMLHttpRequest(); 72 this.xhr = xhr; 73 xhr.open("POST", this.options.url); 74 75 this.configureXhr(); 76 77 this.metadata.forEach((key, values) => { 78 xhr.setRequestHeader(key, values.join(", ")); 79 }); 80 81 xhr.withCredentials = Boolean(this.init.withCredentials); 82 83 xhr.addEventListener("readystatechange", this.onStateChange.bind(this)); 84 xhr.addEventListener("progress", this.onProgressEvent.bind(this)); 85 xhr.addEventListener("loadend", this.onLoadEvent.bind(this)); 86 xhr.addEventListener("error", (err: ErrorEvent) => { 87 this.options.debug && debug("XHR.error", err); 88 detach(() => { 89 this.options.onEnd(err.error); 90 }); 91 }); 92 } 93 94 protected configureXhr(): void { 95 this.xhr.responseType = "text"; 96 97 // overriding the mime type causes a response that has a code point per byte, which can be decoded using the 98 // stringToArrayBuffer function. 99 this.xhr.overrideMimeType("text/plain; charset=x-user-defined"); 100 } 101 102 cancel() { 103 this.options.debug && debug("XHR.abort"); 104 this.xhr.abort(); 105 } 106} 107 108export class MozChunkedArrayBufferXHR extends XHR { 109 protected configureXhr(): void { 110 this.options.debug && debug("MozXHR.configureXhr: setting responseType to 'moz-chunked-arraybuffer'"); 111 (this.xhr as any).responseType = "moz-chunked-arraybuffer"; 112 } 113 114 onProgressEvent() { 115 const resp = this.xhr.response; 116 this.options.debug && debug("MozXHR.onProgressEvent: ", new Uint8Array(resp)); 117 detach(() => { 118 this.options.onChunk(new Uint8Array(resp)); 119 }); 120 } 121} 122 123function codePointAtPolyfill(str: string, index: number) { 124 let code = str.charCodeAt(index); 125 if (code >= 0xd800 && code <= 0xdbff) { 126 const surr = str.charCodeAt(index + 1); 127 if (surr >= 0xdc00 && surr <= 0xdfff) { 128 code = 0x10000 + ((code - 0xd800) << 10) + (surr - 0xdc00); 129 } 130 } 131 return code; 132} 133 134export function stringToArrayBuffer(str: string): Uint8Array { 135 const asArray = new Uint8Array(str.length); 136 let arrayIndex = 0; 137 for (let i = 0; i < str.length; i++) { 138 const codePoint = (String.prototype as any).codePointAt ? (str as any).codePointAt(i) : codePointAtPolyfill(str, i); 139 asArray[arrayIndex++] = codePoint & 0xFF; 140 } 141 return asArray; 142} 143 144