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