diff options
author | Andy Hayden <andyhayden1@gmail.com> | 2021-04-30 12:51:48 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-30 15:51:48 -0400 |
commit | 684c357136fd44f9d5a1b8bb4402400ed1354677 (patch) | |
tree | ebc14b1d01b6643dd4d588516692dffc0f8fcb52 /extensions/websocket/01_websocket.js | |
parent | abaec7a88e991188d885bede652f35d76ab4f340 (diff) |
Rename crate_ops to extensions (#10431)
Diffstat (limited to 'extensions/websocket/01_websocket.js')
-rw-r--r-- | extensions/websocket/01_websocket.js | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/extensions/websocket/01_websocket.js b/extensions/websocket/01_websocket.js new file mode 100644 index 000000000..c77af4566 --- /dev/null +++ b/extensions/websocket/01_websocket.js @@ -0,0 +1,401 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +"use strict"; + +((window) => { + const core = window.Deno.core; + + // provided by "deno_web" + const { URL } = window.__bootstrap.url; + + const CONNECTING = 0; + const OPEN = 1; + const CLOSING = 2; + const CLOSED = 3; + + function requiredArguments( + name, + length, + required, + ) { + if (length < required) { + const errMsg = `${name} requires at least ${required} argument${ + required === 1 ? "" : "s" + }, but only ${length} present`; + throw new TypeError(errMsg); + } + } + + /** + * Tries to close the resource (and ignores BadResource errors). + * @param {number} rid + */ + function tryClose(rid) { + try { + core.close(rid); + } catch (err) { + // Ignore error if the socket has already been closed. + if (!(err instanceof Deno.errors.BadResource)) throw err; + } + } + + const handlerSymbol = Symbol("eventHandlers"); + function makeWrappedHandler(handler) { + function wrappedHandler(...args) { + if (typeof wrappedHandler.handler !== "function") { + return; + } + return wrappedHandler.handler.call(this, ...args); + } + wrappedHandler.handler = handler; + return wrappedHandler; + } + // TODO(lucacasonato) reuse when we can reuse code between web crates + function defineEventHandler(emitter, name) { + // HTML specification section 8.1.5.1 + Object.defineProperty(emitter, `on${name}`, { + get() { + return this[handlerSymbol]?.get(name)?.handler; + }, + set(value) { + if (!this[handlerSymbol]) { + this[handlerSymbol] = new Map(); + } + let handlerWrapper = this[handlerSymbol]?.get(name); + if (handlerWrapper) { + handlerWrapper.handler = value; + } else { + handlerWrapper = makeWrappedHandler(value); + this.addEventListener(name, handlerWrapper); + } + this[handlerSymbol].set(name, handlerWrapper); + }, + configurable: true, + enumerable: true, + }); + } + + class WebSocket extends EventTarget { + #readyState = CONNECTING; + + constructor(url, protocols = []) { + super(); + requiredArguments("WebSocket", arguments.length, 1); + + const wsURL = new URL(url); + + if (wsURL.protocol !== "ws:" && wsURL.protocol !== "wss:") { + throw new DOMException( + "Only ws & wss schemes are allowed in a WebSocket URL.", + "SyntaxError", + ); + } + + if (wsURL.hash !== "" || wsURL.href.endsWith("#")) { + throw new DOMException( + "Fragments are not allowed in a WebSocket URL.", + "SyntaxError", + ); + } + + this.#url = wsURL.href; + + core.opSync("op_ws_check_permission", this.#url); + + if (protocols && typeof protocols === "string") { + protocols = [protocols]; + } + + if ( + protocols.some((x) => protocols.indexOf(x) !== protocols.lastIndexOf(x)) + ) { + throw new DOMException( + "Can't supply multiple times the same protocol.", + "SyntaxError", + ); + } + + core.opAsync("op_ws_create", { + url: wsURL.href, + protocols: protocols.join(", "), + }).then((create) => { + if (create.success) { + this.#rid = create.rid; + this.#extensions = create.extensions; + this.#protocol = create.protocol; + + if (this.#readyState === CLOSING) { + core.opAsync("op_ws_close", { + rid: this.#rid, + }).then(() => { + this.#readyState = CLOSED; + + const errEvent = new ErrorEvent("error"); + errEvent.target = this; + this.dispatchEvent(errEvent); + + const event = new CloseEvent("close"); + event.target = this; + this.dispatchEvent(event); + tryClose(this.#rid); + }); + } else { + this.#readyState = OPEN; + const event = new Event("open"); + event.target = this; + this.dispatchEvent(event); + + this.#eventLoop(); + } + } else { + this.#readyState = CLOSED; + + const errEvent = new ErrorEvent("error"); + errEvent.target = this; + this.dispatchEvent(errEvent); + + const closeEvent = new CloseEvent("close"); + closeEvent.target = this; + this.dispatchEvent(closeEvent); + } + }).catch((err) => { + this.#readyState = CLOSED; + + const errorEv = new ErrorEvent( + "error", + { error: err, message: err.toString() }, + ); + errorEv.target = this; + this.dispatchEvent(errorEv); + + const closeEv = new CloseEvent("close"); + closeEv.target = this; + this.dispatchEvent(closeEv); + }); + } + + get CONNECTING() { + return CONNECTING; + } + get OPEN() { + return OPEN; + } + get CLOSING() { + return CLOSING; + } + get CLOSED() { + return CLOSED; + } + + get readyState() { + return this.#readyState; + } + + #extensions = ""; + #protocol = ""; + #url = ""; + #rid; + + get extensions() { + return this.#extensions; + } + get protocol() { + return this.#protocol; + } + + #binaryType = "blob"; + get binaryType() { + return this.#binaryType; + } + set binaryType(value) { + if (value === "blob" || value === "arraybuffer") { + this.#binaryType = value; + } + } + #bufferedAmount = 0; + get bufferedAmount() { + return this.#bufferedAmount; + } + + get url() { + return this.#url; + } + + send(data) { + requiredArguments("WebSocket.send", arguments.length, 1); + + if (this.#readyState != OPEN) { + throw Error("readyState not OPEN"); + } + + const sendTypedArray = (ta) => { + this.#bufferedAmount += ta.size; + core.opAsync("op_ws_send", { + rid: this.#rid, + kind: "binary", + }, ta).then(() => { + this.#bufferedAmount -= ta.size; + }); + }; + + if (data instanceof Blob) { + data.slice().arrayBuffer().then((ab) => + sendTypedArray(new DataView(ab)) + ); + } else if ( + data instanceof Int8Array || data instanceof Int16Array || + data instanceof Int32Array || data instanceof Uint8Array || + data instanceof Uint16Array || data instanceof Uint32Array || + data instanceof Uint8ClampedArray || data instanceof Float32Array || + data instanceof Float64Array || data instanceof DataView + ) { + sendTypedArray(data); + } else if (data instanceof ArrayBuffer) { + sendTypedArray(new DataView(data)); + } else { + const string = String(data); + const encoder = new TextEncoder(); + const d = encoder.encode(string); + this.#bufferedAmount += d.size; + core.opAsync("op_ws_send", { + rid: this.#rid, + kind: "text", + text: string, + }).then(() => { + this.#bufferedAmount -= d.size; + }); + } + } + + close(code, reason) { + if (code && !(code === 1000 || (3000 <= code && code < 5000))) { + throw new DOMException( + "The close code must be either 1000 or in the range of 3000 to 4999.", + "NotSupportedError", + ); + } + + const encoder = new TextEncoder(); + if (reason && encoder.encode(reason).byteLength > 123) { + throw new DOMException( + "The close reason may not be longer than 123 bytes.", + "SyntaxError", + ); + } + + if (this.#readyState === CONNECTING) { + this.#readyState = CLOSING; + } else if (this.#readyState === OPEN) { + this.#readyState = CLOSING; + + core.opAsync("op_ws_close", { + rid: this.#rid, + code, + reason, + }).then(() => { + this.#readyState = CLOSED; + const event = new CloseEvent("close", { + wasClean: true, + code, + reason, + }); + event.target = this; + this.dispatchEvent(event); + tryClose(this.#rid); + }); + } + } + + async #eventLoop() { + while (this.#readyState === OPEN) { + const { kind, value } = await core.opAsync( + "op_ws_next_event", + this.#rid, + ); + + switch (kind) { + case "string": { + const event = new MessageEvent("message", { + data: value, + origin: this.#url, + }); + event.target = this; + this.dispatchEvent(event); + break; + } + case "binary": { + let data; + + if (this.binaryType === "blob") { + data = new Blob([value]); + } else { + data = value.buffer; + } + + const event = new MessageEvent("message", { + data, + origin: this.#url, + }); + event.target = this; + this.dispatchEvent(event); + break; + } + case "ping": { + core.opAsync("op_ws_send", { + rid: this.#rid, + kind: "pong", + }); + break; + } + case "close": { + this.#readyState = CLOSED; + + const event = new CloseEvent("close", { + wasClean: true, + code: value.code, + reason: value.reason, + }); + event.target = this; + this.dispatchEvent(event); + tryClose(this.#rid); + break; + } + case "error": { + this.#readyState = CLOSED; + + const errorEv = new ErrorEvent("error"); + errorEv.target = this; + this.dispatchEvent(errorEv); + + const closeEv = new CloseEvent("close"); + closeEv.target = this; + this.dispatchEvent(closeEv); + tryClose(this.#rid); + break; + } + } + } + } + } + + Object.defineProperties(WebSocket, { + CONNECTING: { + value: 0, + }, + OPEN: { + value: 1, + }, + CLOSING: { + value: 2, + }, + CLOSED: { + value: 3, + }, + }); + + defineEventHandler(WebSocket.prototype, "message"); + defineEventHandler(WebSocket.prototype, "error"); + defineEventHandler(WebSocket.prototype, "close"); + defineEventHandler(WebSocket.prototype, "open"); + + window.__bootstrap.webSocket = { WebSocket }; +})(this); |