diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2021-08-11 12:27:05 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-11 12:27:05 +0200 |
commit | a0285e2eb88f6254f6494b0ecd1878db3a3b2a58 (patch) | |
tree | 90671b004537e20f9493fd3277ffd21d30b39a0e /ext/websocket/01_websocket.js | |
parent | 3a6994115176781b3a93d70794b1b81bc95e42b4 (diff) |
Rename extensions/ directory to ext/ (#11643)
Diffstat (limited to 'ext/websocket/01_websocket.js')
-rw-r--r-- | ext/websocket/01_websocket.js | 538 |
1 files changed, 538 insertions, 0 deletions
diff --git a/ext/websocket/01_websocket.js b/ext/websocket/01_websocket.js new file mode 100644 index 000000000..df8063d21 --- /dev/null +++ b/ext/websocket/01_websocket.js @@ -0,0 +1,538 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +"use strict"; + +/// <reference path="../../core/internal.d.ts" /> + +((window) => { + const core = window.Deno.core; + const { URL } = window.__bootstrap.url; + const webidl = window.__bootstrap.webidl; + const { HTTP_TOKEN_CODE_POINT_RE } = window.__bootstrap.infra; + const { DOMException } = window.__bootstrap.domException; + const { Blob } = globalThis.__bootstrap.file; + const { + ArrayBuffer, + ArrayBufferIsView, + ArrayPrototypeJoin, + DataView, + ErrorPrototypeToString, + ObjectDefineProperty, + Map, + MapPrototypeGet, + MapPrototypeSet, + Set, + Symbol, + String, + StringPrototypeToLowerCase, + StringPrototypeEndsWith, + FunctionPrototypeCall, + RegExpPrototypeTest, + ObjectDefineProperties, + ArrayPrototypeMap, + ArrayPrototypeSome, + PromisePrototypeThen, + } = window.__bootstrap.primordials; + + webidl.converters["sequence<DOMString> or DOMString"] = (V, opts) => { + // Union for (sequence<DOMString> or DOMString) + if (webidl.type(V) === "Object" && V !== null) { + if (V[Symbol.iterator] !== undefined) { + return webidl.converters["sequence<DOMString>"](V, opts); + } + } + return webidl.converters.DOMString(V, opts); + }; + + webidl.converters["WebSocketSend"] = (V, opts) => { + // Union for (Blob or ArrayBufferView or ArrayBuffer or USVString) + if (V instanceof Blob) { + return webidl.converters["Blob"](V, opts); + } + if (typeof V === "object") { + // TODO(littledivy): use primordial for SharedArrayBuffer + if (V instanceof ArrayBuffer || V instanceof SharedArrayBuffer) { + return webidl.converters["ArrayBuffer"](V, opts); + } + if (ArrayBufferIsView(V)) { + return webidl.converters["ArrayBufferView"](V, opts); + } + } + return webidl.converters["USVString"](V, opts); + }; + + const CONNECTING = 0; + const OPEN = 1; + const CLOSING = 2; + const CLOSED = 3; + + /** + * 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 FunctionPrototypeCall(wrappedHandler.handler, 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 + ObjectDefineProperty(emitter, `on${name}`, { + get() { + if (!this[handlerSymbol]) { + return null; + } + return MapPrototypeGet(this[handlerSymbol], name)?.handler; + }, + set(value) { + if (!this[handlerSymbol]) { + this[handlerSymbol] = new Map(); + } + let handlerWrapper = MapPrototypeGet(this[handlerSymbol], name); + if (handlerWrapper) { + handlerWrapper.handler = value; + } else { + handlerWrapper = makeWrappedHandler(value); + this.addEventListener(name, handlerWrapper); + } + MapPrototypeSet(this[handlerSymbol], name, handlerWrapper); + }, + configurable: true, + enumerable: true, + }); + } + + const _readyState = Symbol("[[readyState]]"); + const _url = Symbol("[[url]]"); + const _rid = Symbol("[[rid]]"); + const _extensions = Symbol("[[extensions]]"); + const _protocol = Symbol("[[protocol]]"); + const _binaryType = Symbol("[[binaryType]]"); + const _bufferedAmount = Symbol("[[bufferedAmount]]"); + const _eventLoop = Symbol("[[eventLoop]]"); + const _server = Symbol("[[server]]"); + class WebSocket extends EventTarget { + [_rid]; + + [_readyState] = CONNECTING; + get readyState() { + webidl.assertBranded(this, WebSocket); + return this[_readyState]; + } + + get CONNECTING() { + webidl.assertBranded(this, WebSocket); + return CONNECTING; + } + get OPEN() { + webidl.assertBranded(this, WebSocket); + return OPEN; + } + get CLOSING() { + webidl.assertBranded(this, WebSocket); + return CLOSING; + } + get CLOSED() { + webidl.assertBranded(this, WebSocket); + return CLOSED; + } + + [_extensions] = ""; + get extensions() { + webidl.assertBranded(this, WebSocket); + return this[_extensions]; + } + + [_protocol] = ""; + get protocol() { + webidl.assertBranded(this, WebSocket); + return this[_protocol]; + } + + [_url] = ""; + get url() { + webidl.assertBranded(this, WebSocket); + return this[_url]; + } + + [_binaryType] = "blob"; + get binaryType() { + webidl.assertBranded(this, WebSocket); + return this[_binaryType]; + } + set binaryType(value) { + webidl.assertBranded(this, WebSocket); + value = webidl.converters.DOMString(value, { + prefix: "Failed to set 'binaryType' on 'WebSocket'", + }); + if (value === "blob" || value === "arraybuffer") { + this[_binaryType] = value; + } + } + + [_bufferedAmount] = 0; + get bufferedAmount() { + webidl.assertBranded(this, WebSocket); + return this[_bufferedAmount]; + } + + constructor(url, protocols = []) { + super(); + this[webidl.brand] = webidl.brand; + const prefix = "Failed to construct 'WebSocket'"; + webidl.requiredArguments(arguments.length, 1, { + prefix, + }); + url = webidl.converters.USVString(url, { + prefix, + context: "Argument 1", + }); + protocols = webidl.converters["sequence<DOMString> or DOMString"]( + protocols, + { + prefix, + context: "Argument 2", + }, + ); + + let wsURL; + + try { + wsURL = new URL(url); + } catch (e) { + throw new DOMException(e.message, "SyntaxError"); + } + + if (wsURL.protocol !== "ws:" && wsURL.protocol !== "wss:") { + throw new DOMException( + "Only ws & wss schemes are allowed in a WebSocket URL.", + "SyntaxError", + ); + } + + if (wsURL.hash !== "" || StringPrototypeEndsWith(wsURL.href, "#")) { + throw new DOMException( + "Fragments are not allowed in a WebSocket URL.", + "SyntaxError", + ); + } + + this[_url] = wsURL.href; + + core.opSync( + "op_ws_check_permission_and_cancel_handle", + this[_url], + false, + ); + + if (typeof protocols === "string") { + protocols = [protocols]; + } + + if ( + protocols.length !== + new Set( + ArrayPrototypeMap(protocols, (p) => StringPrototypeToLowerCase(p)), + ).size + ) { + throw new DOMException( + "Can't supply multiple times the same protocol.", + "SyntaxError", + ); + } + + if ( + ArrayPrototypeSome( + protocols, + (protocol) => + !RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, protocol), + ) + ) { + throw new DOMException( + "Invalid protocol value.", + "SyntaxError", + ); + } + + PromisePrototypeThen( + core.opAsync("op_ws_create", { + url: wsURL.href, + protocols: ArrayPrototypeJoin(protocols, ", "), + }), + (create) => { + this[_rid] = create.rid; + this[_extensions] = create.extensions; + this[_protocol] = create.protocol; + + if (this[_readyState] === CLOSING) { + PromisePrototypeThen( + core.opAsync("op_ws_close", { + rid: this[_rid], + }), + () => { + this[_readyState] = CLOSED; + + const errEvent = new ErrorEvent("error"); + this.dispatchEvent(errEvent); + + const event = new CloseEvent("close"); + this.dispatchEvent(event); + tryClose(this[_rid]); + }, + ); + } else { + this[_readyState] = OPEN; + const event = new Event("open"); + this.dispatchEvent(event); + + this[_eventLoop](); + } + }, + (err) => { + this[_readyState] = CLOSED; + + const errorEv = new ErrorEvent( + "error", + { error: err, message: ErrorPrototypeToString(err) }, + ); + this.dispatchEvent(errorEv); + + const closeEv = new CloseEvent("close"); + this.dispatchEvent(closeEv); + }, + ); + } + + send(data) { + webidl.assertBranded(this, WebSocket); + const prefix = "Failed to execute 'send' on 'WebSocket'"; + + webidl.requiredArguments(arguments.length, 1, { + prefix, + }); + data = webidl.converters.WebSocketSend(data, { + prefix, + context: "Argument 1", + }); + + if (this[_readyState] !== OPEN) { + throw new DOMException("readyState not OPEN", "InvalidStateError"); + } + + const sendTypedArray = (ta) => { + this[_bufferedAmount] += ta.byteLength; + PromisePrototypeThen( + core.opAsync("op_ws_send", { + rid: this[_rid], + kind: "binary", + }, ta), + () => { + this[_bufferedAmount] -= ta.byteLength; + }, + ); + }; + + if (data instanceof Blob) { + PromisePrototypeThen( + data.slice().arrayBuffer(), + (ab) => sendTypedArray(new DataView(ab)), + ); + } else if (ArrayBufferIsView(data)) { + sendTypedArray(data); + } else if (data instanceof ArrayBuffer) { + sendTypedArray(new DataView(data)); + } else { + const string = String(data); + const d = core.encode(string); + this[_bufferedAmount] += d.byteLength; + PromisePrototypeThen( + core.opAsync("op_ws_send", { + rid: this[_rid], + kind: "text", + text: string, + }), + () => { + this[_bufferedAmount] -= d.byteLength; + }, + ); + } + } + + close(code = undefined, reason = undefined) { + webidl.assertBranded(this, WebSocket); + const prefix = "Failed to execute 'close' on 'WebSocket'"; + + if (code !== undefined) { + code = webidl.converters["unsigned short"](code, { + prefix, + clamp: true, + context: "Argument 1", + }); + } + + if (reason !== undefined) { + reason = webidl.converters.USVString(reason, { + prefix, + context: "Argument 2", + }); + } + + if (!this[_server]) { + if ( + code !== undefined && + !(code === 1000 || (3000 <= code && code < 5000)) + ) { + throw new DOMException( + "The close code must be either 1000 or in the range of 3000 to 4999.", + "InvalidAccessError", + ); + } + } + + if (reason !== undefined && core.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; + + PromisePrototypeThen( + core.opAsync("op_ws_close", { + rid: this[_rid], + code, + reason, + }), + () => { + this[_readyState] = CLOSED; + const event = new CloseEvent("close", { + wasClean: true, + code: code ?? 1005, + reason, + }); + 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], + }); + 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], + }); + 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, + }); + this.dispatchEvent(event); + tryClose(this[_rid]); + break; + } + case "error": { + this[_readyState] = CLOSED; + + const errorEv = new ErrorEvent("error", { + message: value, + }); + this.dispatchEvent(errorEv); + + const closeEv = new CloseEvent("close"); + this.dispatchEvent(closeEv); + tryClose(this[_rid]); + break; + } + } + } + } + } + + ObjectDefineProperties(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"); + + webidl.configurePrototype(WebSocket); + + window.__bootstrap.webSocket = { + WebSocket, + _rid, + _readyState, + _eventLoop, + _protocol, + _server, + }; +})(this); |