// Copyright 2018-2026 the Deno authors. MIT license.

/// <reference path="../../core/internal.d.ts" />

import { core, internals, primordials } from "ext:core/mod.js";
const {
  isAnyArrayBuffer,
  isArrayBuffer,
  internalRidSymbol,
} = core;
import {
  op_ws_check_permission_and_cancel_handle,
  op_ws_close,
  op_ws_create,
  op_ws_get_buffer,
  op_ws_get_buffer_as_string,
  op_ws_get_buffered_amount,
  op_ws_get_error,
  op_ws_next_event,
  op_ws_send_binary,
  op_ws_send_binary_ab,
  op_ws_send_ping,
  op_ws_send_text,
} from "ext:core/ops";
const {
  ArrayBufferIsView,
  ArrayIsArray,
  ArrayPrototypeJoin,
  ArrayPrototypeMap,
  ArrayPrototypePush,
  ArrayPrototypeShift,
  ArrayPrototypeSome,
  DateNow,
  decodeURIComponent,
  Error,
  ErrorPrototypeToString,
  ObjectDefineProperties,
  ObjectPrototypeIsPrototypeOf,
  PromisePrototypeCatch,
  PromisePrototypeThen,
  RegExpPrototypeExec,
  SafeSet,
  SetPrototypeGetSize,
  String,
  StringPrototypeEndsWith,
  StringPrototypeToLowerCase,
  Symbol,
  SymbolFor,
  SymbolIterator,
  TypedArrayPrototypeGetByteLength,
  TypeError,
} = primordials;

const { URL } = core.loadExtScript("ext:deno_web/00_url.js");
const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
const { createFilteredInspectProxy } = core.loadExtScript(
  "ext:deno_web/01_console.js",
);
const { HTTP_TOKEN_CODE_POINT_RE, byteLowerCase, forgivingBase64Encode } = core
  .loadExtScript(
    "ext:deno_web/00_infra.js",
  );
const { DOMException } = core.loadExtScript("ext:deno_web/01_dom_exception.js");
const {
  CloseEvent,
  defineEventHandler,
  dispatch,
  ErrorEvent,
  Event,
  EventTarget,
  MessageEvent,
  setIsTrusted,
} = core.loadExtScript("ext:deno_web/02_event.js");
const { Blob, BlobPrototype } = core.loadExtScript("ext:deno_web/09_file.js");
const { getLocationHref } = core.loadExtScript("ext:deno_web/12_location.js");
const {
  fillHeaders,
  headerListFromHeaders,
  headersFromHeaderList,
} = core.loadExtScript("ext:deno_fetch/20_headers.js");
const { HttpClientPrototype } = core.loadExtScript(
  "ext:deno_fetch/22_http_client.js",
);

const kNodeUndiciDispatcherOptions = SymbolFor(
  "Deno.internal.node.undici.dispatcherOptions",
);
const kNodeUndiciGlobalDispatcher = SymbolFor(
  "Deno.internal.node.undici.globalDispatcher",
);

webidl.converters["WebSocketInit"] = webidl.createDictionaryConverter(
  "WebSocketInit",
  [
    {
      key: "headers",
      converter: webidl.converters["HeadersInit"],
    },
    {
      key: "protocols",
      converter: webidl.converters["sequence<DOMString>"],
    },
    { key: "client", converter: webidl.converters.any },
    { key: "dispatcher", converter: webidl.converters.any },
  ],
);

webidl.converters["WebSocketInit or sequence<DOMString> or DOMString"] = (
  V,
  prefix,
  context,
  opts,
) => {
  // Union for (WebSocketInit or sequence<DOMString> or DOMString)
  if (V === null || V === undefined) {
    return webidl.converters["WebSocketInit"](V, prefix, context, opts);
  }
  if (webidl.type(V) === "Object" && V !== null) {
    if (V[SymbolIterator] !== undefined) {
      return webidl.converters["sequence<DOMString>"](V, prefix, context, opts);
    }
    return webidl.converters["WebSocketInit"](V, prefix, context, opts);
  }
  return webidl.converters.DOMString(V, prefix, context, opts);
};

webidl.converters["WebSocketSend"] = (V, prefix, context, opts) => {
  // Union for (Blob or ArrayBufferView or ArrayBuffer or USVString)
  if (ObjectPrototypeIsPrototypeOf(BlobPrototype, V)) {
    return webidl.converters["Blob"](V, prefix, context, opts);
  }
  if (typeof V === "object") {
    if (isAnyArrayBuffer(V)) {
      return webidl.converters["ArrayBuffer"](V, prefix, context, opts);
    }
    if (ArrayBufferIsView(V)) {
      return webidl.converters["ArrayBufferView"](V, prefix, context, opts);
    }
  }
  return webidl.converters["USVString"](V, prefix, context, opts);
};

/** role */
const SERVER = 0;
const CLIENT = 1;

/** state */
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;

const _readyState = Symbol("[[readyState]]");
const _url = Symbol("[[url]]");
const _rid = Symbol("[[rid]]");
const _role = Symbol("[[role]]");
const _extensions = Symbol("[[extensions]]");
const _protocol = Symbol("[[protocol]]");
const _binaryType = Symbol("[[binaryType]]");
const _eventLoop = Symbol("[[eventLoop]]");
const _sendQueue = Symbol("[[sendQueue]]");
const _queueSend = Symbol("[[queueSend]]");
const _cancelHandle = Symbol("[[cancelHandle]]");

const _idleTimeoutDuration = Symbol("[[idleTimeout]]");
const _idleTimeoutTimeout = Symbol("[[idleTimeoutTimeout]]");
const _serverHandleIdleTimeout = Symbol("[[serverHandleIdleTimeout]]");

// Inspector Network domain instrumentation. The bridge is installed by
// `ext/node/polyfills/inspector.js` on `internals.__inspectorNetwork` when
// `node:inspector` is loaded; it stays null in normal runs so the cost is
// one method call per WebSocket lifecycle.
const _inspectorRequestId = Symbol("[[inspectorRequestId]]");

function getInspectorNetwork() {
  const ins = internals.__inspectorNetwork;
  if (ins && ins.isEnabled()) return ins;
  return null;
}

function emitWebSocketClosed(ws) {
  const ins = getInspectorNetwork();
  const requestId = ws[_inspectorRequestId];
  if (ins === null || requestId === undefined) return;
  try {
    ins.webSocketClosed({
      requestId,
      timestamp: DateNow() / 1000,
    });
  } catch {
    // never let inspector instrumentation break a real WebSocket
  }
  ws[_inspectorRequestId] = undefined;
}

// Opcodes per RFC 6455 / CDP `WebSocketFrame.opcode`:
// 1 = text, 2 = binary. Per RFC 6455 client-to-server frames are masked,
// server-to-client are not - so the `mask` flag depends on direction and
// role: a CLIENT socket masks what it sends, a SERVER socket sees masked
// frames coming in.
function emitWebSocketFrame(ws, eventName, opcode, payloadData) {
  const ins = getInspectorNetwork();
  const requestId = ws[_inspectorRequestId];
  if (ins === null || requestId === undefined) return;
  const sent = eventName === "frameSent";
  const mask = ws[_role] === SERVER ? !sent : sent;
  try {
    const params = {
      requestId,
      timestamp: DateNow() / 1000,
      response: { opcode, mask, payloadData },
    };
    if (sent) ins.webSocketFrameSent(params);
    else ins.webSocketFrameReceived(params);
  } catch {
    // never let inspector instrumentation break a real WebSocket
  }
}

// Called by ext/http's `upgradeWebSocket` after a successful handshake, so
// server-accepted sockets show up in the inspector's Network panel and
// the frame emitters in `_eventLoop` / `send` start firing for them.
// Handshake events (`willSendHandshakeRequest`, `handshakeResponseReceived`)
// are intentionally skipped here - those are client-side concepts.
function installServerInspector(ws, url) {
  const ins = getInspectorNetwork();
  if (ins === null) return;
  const requestId = ins.nextRequestId();
  ws[_inspectorRequestId] = requestId;
  try {
    ins.webSocketCreated({ requestId, url });
  } catch {
    // never let inspector instrumentation break a real WebSocket
  }
}

function emitWebSocketFrameError(ws, errorMessage) {
  const ins = getInspectorNetwork();
  const requestId = ws[_inspectorRequestId];
  if (ins === null || requestId === undefined) return;
  try {
    ins.webSocketFrameError({
      requestId,
      timestamp: DateNow() / 1000,
      errorMessage,
    });
  } catch {
    // never let inspector instrumentation break a real WebSocket
  }
}

class WebSocket extends EventTarget {
  constructor(url, initOrProtocols) {
    super();
    this[webidl.brand] = webidl.brand;
    this[_rid] = undefined;
    this[_role] = undefined;
    this[_readyState] = CONNECTING;
    this[_extensions] = "";
    this[_protocol] = "";
    this[_url] = "";
    this[_binaryType] = "blob";
    this[_idleTimeoutDuration] = 0;
    this[_idleTimeoutTimeout] = undefined;
    this[_sendQueue] = [];
    this[_cancelHandle] = undefined;

    const prefix = "Failed to construct 'WebSocket'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    url = webidl.converters.USVString(url, prefix, "Argument 1");
    initOrProtocols = webidl.converters
      ["WebSocketInit or sequence<DOMString> or DOMString"](
        initOrProtocols,
        prefix,
        "Argument 2",
      );

    let wsURL;

    try {
      wsURL = new URL(url, getLocationHref());
    } catch (e) {
      throw new DOMException(e.message, "SyntaxError");
    }

    if (wsURL.protocol === "http:") {
      wsURL.protocol = "ws:";
    } else if (wsURL.protocol === "https:") {
      wsURL.protocol = "wss:";
    }

    if (wsURL.protocol !== "ws:" && wsURL.protocol !== "wss:") {
      throw new DOMException(
        `Only ws & wss schemes are allowed in a WebSocket URL: received ${wsURL.protocol}`,
        "SyntaxError",
      );
    }

    if (wsURL.hash !== "" || StringPrototypeEndsWith(wsURL.href, "#")) {
      throw new DOMException(
        "Fragments are not allowed in a WebSocket URL",
        "SyntaxError",
      );
    }

    // WHATWG WebSocket: credentials in the URL userinfo are sent as an
    // `Authorization: Basic` header during the handshake and then stripped
    // from the URL exposed via `url`. This matches browser behavior.
    let urlCredentials = null;
    if (wsURL.username !== "" || wsURL.password !== "") {
      urlCredentials = forgivingBase64Encode(
        core.encode(
          `${decodeURIComponent(wsURL.username)}:${
            decodeURIComponent(wsURL.password)
          }`,
        ),
      );
      wsURL.username = "";
      wsURL.password = "";
    }

    this[_url] = wsURL.href;
    this[_role] = CLIENT;

    let protocols;
    let headers = null;
    let clientRid = null;
    let caCerts = null;
    let unsafelyIgnoreCertificateErrors = false;

    if (typeof initOrProtocols === "string") {
      protocols = [initOrProtocols];
    } else if (ArrayIsArray(initOrProtocols)) {
      protocols = initOrProtocols;
    } else {
      protocols = initOrProtocols.protocols || [];

      if (initOrProtocols.headers !== undefined) {
        headers = headersFromHeaderList([], "request");
        fillHeaders(headers, initOrProtocols.headers);
      }

      // NOTE: non standard extension. This handles Deno.HttpClient parameter
      if (initOrProtocols.client !== undefined) {
        if (
          initOrProtocols.client !== null &&
          !ObjectPrototypeIsPrototypeOf(
            HttpClientPrototype,
            initOrProtocols.client,
          )
        ) {
          throw webidl.makeException(
            TypeError,
            "`client` must be a Deno.HttpClient",
            prefix,
            "Argument 2",
          );
        }
        clientRid = initOrProtocols.client?.[internalRidSymbol] ?? null;
      }

      let dispatcher = initOrProtocols.dispatcher;
      if (
        dispatcher === undefined &&
        internals[kNodeUndiciGlobalDispatcher] !== undefined
      ) {
        dispatcher = internals[kNodeUndiciGlobalDispatcher];
      }

      if (clientRid === null && dispatcher !== undefined) {
        clientRid = dispatcher?.client?.[internalRidSymbol] ?? null;
        const dispatcherOptions = dispatcher?.[kNodeUndiciDispatcherOptions];
        if (dispatcherOptions !== undefined) {
          caCerts = dispatcherOptions.caCerts ?? null;
          unsafelyIgnoreCertificateErrors =
            dispatcherOptions.unsafelyIgnoreCertificateErrors === true;
        }
      }
    }

    // Apply URL-derived Basic credentials. An explicit `Authorization` header
    // in `init.headers` takes precedence over the URL userinfo.
    if (urlCredentials !== null) {
      if (headers === null) {
        headers = headersFromHeaderList([], "request");
      }
      if (headers.get("Authorization") === null) {
        headers.set("Authorization", `Basic ${urlCredentials}`);
      }
    }

    if (
      protocols.length !==
        SetPrototypeGetSize(
          new SafeSet(
            ArrayPrototypeMap(protocols, (p) => StringPrototypeToLowerCase(p)),
          ),
        )
    ) {
      throw new DOMException(
        "Cannot supply the same protocol multiple times",
        "SyntaxError",
      );
    }

    if (
      ArrayPrototypeSome(
        protocols,
        (protocol) =>
          RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, protocol) === null,
      )
    ) {
      throw new DOMException(
        "Invalid protocol value",
        "SyntaxError",
      );
    }

    const cancelRid = op_ws_check_permission_and_cancel_handle(
      "WebSocket.abort()",
      this[_url],
      true,
    );

    this[_cancelHandle] = cancelRid;

    // Inspector: emit `Network.webSocketCreated` before initiating the
    // handshake. The op call itself reaches deep into Rust async work, so
    // we want the "we're attempting this URL" notification to fire even if
    // the handshake later fails.
    const inspectorNetwork = getInspectorNetwork();
    if (inspectorNetwork !== null) {
      const requestId = inspectorNetwork.nextRequestId();
      this[_inspectorRequestId] = requestId;
      try {
        inspectorNetwork.webSocketCreated({
          requestId,
          url: this[_url],
          // initiator is filled in by the inspector op using V8's stack.
        });
      } catch {
        // ignore
      }
      // `Network.webSocketWillSendHandshakeRequest` populates DevTools'
      // "Headers" panel for the request side. Tungstenite-generated headers
      // (Sec-WebSocket-Key, Sec-WebSocket-Version, Upgrade, Connection) are
      // produced in Rust and not visible here; we surface what JS knows -
      // user-supplied headers via `init.headers` plus the negotiated
      // Sec-WebSocket-Protocol. That's enough for the panel to show the
      // intent of the request; the response panel still gets the real
      // server-sent headers via `webSocketHandshakeResponseReceived`.
      const requestHeaders = { __proto__: null };
      if (headers !== null) {
        const list = headerListFromHeaders(headers);
        for (let i = 0; i < list.length; i++) {
          const name = byteLowerCase(list[i][0]);
          const value = list[i][1];
          if (requestHeaders[name] === undefined) {
            requestHeaders[name] = value;
          } else {
            requestHeaders[name] = requestHeaders[name] + ", " + value;
          }
        }
      }
      if (protocols.length > 0) {
        requestHeaders["sec-websocket-protocol"] = ArrayPrototypeJoin(
          protocols,
          ", ",
        );
      }
      try {
        inspectorNetwork.webSocketWillSendHandshakeRequest({
          requestId,
          timestamp: DateNow() / 1000,
          wallTime: DateNow() / 1000,
          request: {
            headers: requestHeaders,
          },
        });
      } catch {
        // ignore
      }
    }

    PromisePrototypeThen(
      op_ws_create(
        "new WebSocket()",
        wsURL.href,
        ArrayPrototypeJoin(protocols, ", "),
        cancelRid,
        headers ? headerListFromHeaders(headers) : null,
        caCerts,
        unsafelyIgnoreCertificateErrors,
        clientRid,
      ),
      (create) => {
        this[_rid] = create.rid;
        this[_extensions] = create.extensions;
        this[_protocol] = create.protocol;

        // Inspector: emit `Network.webSocketHandshakeResponseReceived` with
        // the real upgrade response (status, statusText, headers) returned
        // by the Rust op.
        const reqId = this[_inspectorRequestId];
        if (reqId !== undefined) {
          const ins = getInspectorNetwork();
          if (ins !== null) {
            const responseHeaders = { __proto__: null };
            for (let i = 0; i < create.headers.length; i++) {
              const name = byteLowerCase(create.headers[i][0]);
              const value = create.headers[i][1];
              if (responseHeaders[name] === undefined) {
                responseHeaders[name] = value;
              } else {
                // Connection, Upgrade etc. shouldn't repeat, but be defensive.
                responseHeaders[name] = responseHeaders[name] + ", " + value;
              }
            }
            try {
              ins.webSocketHandshakeResponseReceived({
                requestId: reqId,
                timestamp: DateNow() / 1000,
                response: {
                  status: create.status,
                  statusText: create.statusText,
                  headers: responseHeaders,
                },
              });
            } catch {
              // ignore
            }
          }
        }

        if (this[_readyState] === CLOSING) {
          PromisePrototypeThen(
            op_ws_close(this[_rid]),
            () => {
              this[_readyState] = CLOSED;

              const errEvent = new ErrorEvent("error");
              this.dispatchEvent(errEvent);

              emitWebSocketClosed(this);
              const event = new CloseEvent("close");
              this.dispatchEvent(event);
              core.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);

        if (this[_cancelHandle]) {
          core.tryClose(this[_cancelHandle]);

          this[_cancelHandle] = undefined;
        }

        emitWebSocketClosed(this);
        const closeEv = new CloseEvent("close");
        this.dispatchEvent(closeEv);
      },
    );
  }

  get readyState() {
    webidl.assertBranded(this, WebSocketPrototype);
    return this[_readyState];
  }

  get CONNECTING() {
    webidl.assertBranded(this, WebSocketPrototype);
    return CONNECTING;
  }
  get OPEN() {
    webidl.assertBranded(this, WebSocketPrototype);
    return OPEN;
  }
  get CLOSING() {
    webidl.assertBranded(this, WebSocketPrototype);
    return CLOSING;
  }
  get CLOSED() {
    webidl.assertBranded(this, WebSocketPrototype);
    return CLOSED;
  }

  get extensions() {
    webidl.assertBranded(this, WebSocketPrototype);
    return this[_extensions];
  }

  get protocol() {
    webidl.assertBranded(this, WebSocketPrototype);
    return this[_protocol];
  }

  get url() {
    webidl.assertBranded(this, WebSocketPrototype);
    return this[_url];
  }

  get binaryType() {
    webidl.assertBranded(this, WebSocketPrototype);
    return this[_binaryType];
  }
  set binaryType(value) {
    webidl.assertBranded(this, WebSocketPrototype);
    value = webidl.converters.DOMString(
      value,
      "Failed to set 'binaryType' on 'WebSocket'",
    );
    if (value === "blob" || value === "arraybuffer") {
      this[_binaryType] = value;
    }
  }

  get bufferedAmount() {
    webidl.assertBranded(this, WebSocketPrototype);
    if (this[_readyState] === OPEN) {
      return op_ws_get_buffered_amount(this[_rid]);
    } else {
      return 0;
    }
  }

  send(data) {
    webidl.assertBranded(this, WebSocketPrototype);
    const prefix = "Failed to execute 'send' on 'WebSocket'";

    webidl.requiredArguments(arguments.length, 1, prefix);
    data = webidl.converters.WebSocketSend(data, prefix, "Argument 1");

    if (this[_readyState] === CONNECTING) {
      throw new DOMException("'readyState' not OPEN", "InvalidStateError");
    }

    if (this[_readyState] !== OPEN) {
      return;
    }

    if (this[_sendQueue].length === 0) {
      // Fast path if the send queue is empty, for example when only synchronous
      // data is being sent.
      if (ArrayBufferIsView(data)) {
        op_ws_send_binary(this[_rid], data);
        emitWebSocketFrame(this, "frameSent", 2, data);
      } else if (isArrayBuffer(data)) {
        op_ws_send_binary_ab(this[_rid], data);
        emitWebSocketFrame(this, "frameSent", 2, data);
      } else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
        this[_queueSend](data);
      } else {
        const string = String(data);
        op_ws_send_text(
          this[_rid],
          string,
        );
        emitWebSocketFrame(this, "frameSent", 1, string);
      }
    } else {
      // Slower path if the send queue is not empty, for example when sending
      // asynchronous data like a Blob.
      this[_queueSend](data);
    }
  }

  close(code = undefined, reason = undefined) {
    webidl.assertBranded(this, WebSocketPrototype);
    const prefix = "Failed to execute 'close' on 'WebSocket'";

    if (code !== undefined) {
      code = webidl.converters["unsigned short"](code, prefix, "Argument 1", {
        clamp: true,
      });
    }

    if (reason !== undefined) {
      reason = webidl.converters.USVString(reason, prefix, "Argument 2");
    }

    if (this[_role] === CLIENT) {
      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: received ${code}`,
          "InvalidAccessError",
        );
      }
    }

    if (
      reason !== undefined &&
      TypedArrayPrototypeGetByteLength(core.encode(reason)) > 123
    ) {
      throw new DOMException(
        "The close reason may not be longer than 123 bytes",
        "SyntaxError",
      );
    }

    if (this[_cancelHandle]) {
      // Cancel ongoing handshake.
      core.tryClose(this[_cancelHandle]);

      this[_cancelHandle] = undefined;
    }

    if (this[_readyState] === CONNECTING) {
      this[_readyState] = CLOSING;
    } else if (this[_readyState] === OPEN) {
      this[_readyState] = CLOSING;

      PromisePrototypeCatch(
        op_ws_close(
          this[_rid],
          code,
          reason,
        ),
        (err) => {
          this[_readyState] = CLOSED;

          const errorEv = new ErrorEvent("error", {
            error: err,
            message: ErrorPrototypeToString(err),
          });
          this.dispatchEvent(errorEv);

          emitWebSocketClosed(this);
          const closeEv = new CloseEvent("close");
          this.dispatchEvent(closeEv);
          core.tryClose(this[_rid]);
        },
      );
    }
  }

  async [_eventLoop]() {
    const rid = this[_rid];
    while (this[_readyState] !== CLOSED) {
      const kind = await op_ws_next_event(rid);
      /* close the connection if read was cancelled, and we didn't get a close frame */
      if (
        (this[_readyState] == CLOSING) &&
        kind <= 3 && this[_role] !== CLIENT
      ) {
        this[_readyState] = CLOSED;

        emitWebSocketClosed(this);
        const event = new CloseEvent("close");
        this.dispatchEvent(event);
        core.tryClose(rid);
        break;
      }

      switch (kind) {
        case 0: {
          /* string */
          const data = op_ws_get_buffer_as_string(rid);
          if (data === undefined) {
            break;
          }

          this[_serverHandleIdleTimeout]();
          emitWebSocketFrame(this, "frameReceived", 1, data);
          const event = new MessageEvent("message", {
            data,
            origin: this[_url],
          });
          setIsTrusted(event, true);
          dispatch(this, event);
          break;
        }
        case 1: {
          /* binary */
          const d = op_ws_get_buffer(rid);
          if (d == undefined) {
            break;
          }

          this[_serverHandleIdleTimeout]();
          // deno-lint-ignore deno-internal/prefer-primordials
          const buffer = d.buffer;
          emitWebSocketFrame(this, "frameReceived", 2, buffer);
          let data;
          if (this.binaryType === "blob") {
            data = new Blob([buffer]);
          } else {
            data = buffer;
          }

          const event = new MessageEvent("message", {
            data,
            origin: this[_url],
          });
          setIsTrusted(event, true);
          dispatch(this, event);
          break;
        }
        case 2: {
          /* pong */
          this[_serverHandleIdleTimeout]();
          break;
        }
        case 3: {
          /* error */
          this[_readyState] = CLOSED;

          const message = op_ws_get_error(rid);
          emitWebSocketFrameError(this, message);
          const error = new Error(message);
          const errorEv = new ErrorEvent("error", {
            error,
            message,
          });
          this.dispatchEvent(errorEv);

          emitWebSocketClosed(this);
          const closeEv = new CloseEvent("close");
          this.dispatchEvent(closeEv);
          core.tryClose(rid);
          break;
        }
        default: {
          /* close */
          const code = kind;
          const reason = code == 1005 ? "" : op_ws_get_error(rid);
          const prevState = this[_readyState];
          this[_readyState] = CLOSED;
          if (this[_idleTimeoutTimeout]) {
            core.cancelTimer(this[_idleTimeoutTimeout]);
          }

          if (prevState === OPEN) {
            try {
              await op_ws_close(
                rid,
                code,
                reason,
              );
            } catch {
              // ignore failures
            }
          }

          emitWebSocketClosed(this);
          const event = new CloseEvent("close", {
            wasClean: true,
            code: code,
            reason,
          });
          this.dispatchEvent(event);
          core.tryClose(rid);
          break;
        }
      }
    }
  }

  async [_queueSend](data) {
    const queue = this[_sendQueue];

    ArrayPrototypePush(queue, data);

    if (queue.length > 1) {
      // There is already a send in progress, so we just push to the queue
      // and let that task handle sending of this data.
      return;
    }

    while (queue.length > 0) {
      const data = queue[0];
      if (ArrayBufferIsView(data)) {
        op_ws_send_binary(this[_rid], data);
        emitWebSocketFrame(this, "frameSent", 2, data);
      } else if (isArrayBuffer(data)) {
        op_ws_send_binary_ab(this[_rid], data);
        emitWebSocketFrame(this, "frameSent", 2, data);
      } else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
        // deno-lint-ignore deno-internal/prefer-primordials
        const ab = await data.slice().arrayBuffer();
        op_ws_send_binary_ab(this[_rid], ab);
        emitWebSocketFrame(this, "frameSent", 2, ab);
      } else {
        const string = String(data);
        op_ws_send_text(
          this[_rid],
          string,
        );
        emitWebSocketFrame(this, "frameSent", 1, string);
      }
      ArrayPrototypeShift(queue);
    }
  }

  [_serverHandleIdleTimeout]() {
    if (this[_idleTimeoutDuration]) {
      if (this[_idleTimeoutTimeout]) {
        core.cancelTimer(this[_idleTimeoutTimeout]);
      }
      this[_idleTimeoutTimeout] = core.createSystemTimer(
        async () => {
          if (this[_readyState] === OPEN) {
            await PromisePrototypeCatch(op_ws_send_ping(this[_rid]), () => {});
            this[_idleTimeoutTimeout] = core.createSystemTimer(
              async () => {
                if (this[_readyState] === OPEN) {
                  this[_readyState] = CLOSING;
                  const reason = "No response from ping frame.";
                  await PromisePrototypeCatch(
                    op_ws_close(this[_rid], 1001, reason),
                    () => {},
                  );
                  this[_readyState] = CLOSED;

                  const errEvent = new ErrorEvent("error", {
                    message: reason,
                  });
                  this.dispatchEvent(errEvent);

                  const event = new CloseEvent("close", {
                    wasClean: false,
                    code: 1001,
                    reason,
                  });
                  this.dispatchEvent(event);
                  core.tryClose(this[_rid]);
                } else {
                  if (this[_idleTimeoutTimeout]) {
                    core.cancelTimer(this[_idleTimeoutTimeout]);
                  }
                }
              },
              (this[_idleTimeoutDuration] / 2) * 1000,
              true,
            );
          } else {
            if (this[_idleTimeoutTimeout]) {
              core.cancelTimer(this[_idleTimeoutTimeout]);
            }
          }
        },
        (this[_idleTimeoutDuration] / 2) * 1000,
        true,
      );
    }
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      createFilteredInspectProxy({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(WebSocketPrototype, this),
        keys: [
          "url",
          "readyState",
          "extensions",
          "protocol",
          "binaryType",
          "bufferedAmount",
          "onmessage",
          "onerror",
          "onclose",
          "onopen",
        ],
      }),
      inspectOptions,
    );
  }
}

ObjectDefineProperties(WebSocket, {
  CONNECTING: {
    __proto__: null,
    value: 0,
  },
  OPEN: {
    __proto__: null,
    value: 1,
  },
  CLOSING: {
    __proto__: null,
    value: 2,
  },
  CLOSED: {
    __proto__: null,
    value: 3,
  },
});

defineEventHandler(WebSocket.prototype, "message");
defineEventHandler(WebSocket.prototype, "error");
defineEventHandler(WebSocket.prototype, "close");
defineEventHandler(WebSocket.prototype, "open");

webidl.configureInterface(WebSocket);
const WebSocketPrototype = WebSocket.prototype;

function createWebSocketBranded() {
  const socket = webidl.createBranded(WebSocket);
  socket[_rid] = undefined;
  socket[_role] = undefined;
  socket[_readyState] = CONNECTING;
  socket[_extensions] = "";
  socket[_protocol] = "";
  socket[_url] = "";
  // We use ArrayBuffer for server websockets for backwards compatibility
  // and performance reasons.
  //
  // https://github.com/denoland/deno/issues/15340#issuecomment-1872353134
  socket[_binaryType] = "arraybuffer";
  socket[_idleTimeoutDuration] = 0;
  socket[_idleTimeoutTimeout] = undefined;
  socket[_sendQueue] = [];
  return socket;
}

export {
  _eventLoop,
  _idleTimeoutDuration,
  _idleTimeoutTimeout,
  _protocol,
  _readyState,
  _rid,
  _role,
  _serverHandleIdleTimeout,
  CLIENT,
  createWebSocketBranded,
  emitWebSocketClosed,
  installServerInspector,
  SERVER,
  WebSocket,
};
