- reimplementation of BrowserWebSocket (#58199)
authorPavel Savara <pavelsavara@microsoft.com>
Sat, 18 Sep 2021 15:40:21 +0000 (17:40 +0200)
committerGitHub <noreply@github.com>
Sat, 18 Sep 2021 15:40:21 +0000 (17:40 +0200)
- CC0 notice for JavaScript queue
- feedback

18 files changed:
THIRD-PARTY-NOTICES.TXT
src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs
src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs
src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs
src/libraries/System.Net.WebSockets.Client/src/System.Net.WebSockets.Client.csproj
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ReceivePayload.cs [deleted file]
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Browser.cs
src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs
src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs
src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs
src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs
src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs
src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.JS.Owned.cs
src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs
src/mono/wasm/runtime/binding_support.js
src/mono/wasm/runtime/corebindings.c
src/mono/wasm/runtime/library_mono.js

index 0ead806..e38f6ef 100644 (file)
@@ -971,3 +971,29 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 SUCH DAMAGE.
+
+License notice for JavaScript queues
+-------------------------------------
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.
+
+Statement of Purpose
+The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
+Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
+For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
+the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
+moral rights retained by the original author(s) and/or performer(s);
+publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
+rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
+rights protecting the extraction, dissemination, use and reuse of data in a Work;
+database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
+other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
+2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
+3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
+4. Limitations and Disclaimers.
+a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
+b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
+c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
+d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
index 5a61a86..d6ac21c 100644 (file)
@@ -46,6 +46,18 @@ internal static partial class Interop
         internal static extern string? AddEventListener(int jsHandle, string name, int gcHandle, int optionsJsHandle);
         [MethodImplAttribute(MethodImplOptions.InternalCall)]
         internal static extern string? RemoveEventListener(int jsHandle, string name, int gcHandle, bool capture);
+        [MethodImplAttribute(MethodImplOptions.InternalCall)]
+        internal static extern object WebSocketSend(int webSocketJSHandle, IntPtr messagePtr, int offset, int length, int messageType, bool endOfMessage, out int promiseJSHandle, out int exceptionalResult);
+        [MethodImplAttribute(MethodImplOptions.InternalCall)]
+        internal static extern object WebSocketReceive(int webSocketJSHandle, IntPtr bufferPtr, int offset, int length, IntPtr responsePtr, out int promiseJSHandle, out int exceptionalResult);
+        [MethodImplAttribute(MethodImplOptions.InternalCall)]
+        internal static extern object WebSocketOpen(string uri, object[]? subProtocols, Delegate onClosed, out int webSocketJSHandle, out int promiseJSHandle, out int exceptionalResult);
+        [MethodImplAttribute(MethodImplOptions.InternalCall)]
+        internal static extern string WebSocketAbort(int webSocketJSHandle, out int exceptionalResult);
+        [MethodImplAttribute(MethodImplOptions.InternalCall)]
+        internal static extern object WebSocketClose(int webSocketJSHandle, int code, string? reason, bool waitForCloseReceived, out int promiseJSHandle, out int exceptionalResult);
+        [MethodImplAttribute(MethodImplOptions.InternalCall)]
+        internal static extern string CancelPromise(int promiseJSHandle, out int exceptionalResult);
 
         // / <summary>
         // / Execute the provided string in the JavaScript context
index ecff49e..67166cf 100644 (file)
@@ -94,18 +94,20 @@ namespace System.Net.Test.Common
 
         private void CloseWebSocket()
         {
-            if (_websocket != null && (_websocket.State == WebSocketState.Open || _websocket.State == WebSocketState.Connecting || _websocket.State == WebSocketState.None))
+            if (_websocket == null) return;
+
+            var state = _websocket.State;
+            if (state != WebSocketState.Open && state != WebSocketState.Connecting && state != WebSocketState.CloseSent) return;
+
+            try
             {
-                try
-                {
-                    var task = _websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "closing remoteLoop", CancellationToken.None);
-                    // Block and wait for the task to complete synchronously
-                    Task.WaitAll(task);
-                }
-                catch (Exception)
-                {
-                }
-            }            
+                var task = _websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "closing remoteLoop", CancellationToken.None);
+                // Block and wait for the task to complete synchronously
+                Task.WaitAll(task);
+            }
+            catch (Exception)
+            {
+            }
         }
     }
 
index 8e89511..f4e5562 100644 (file)
@@ -26,6 +26,10 @@ namespace NetCoreServer
             {
                 Thread.Sleep(10000);
             }
+            else if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay20sec"))
+            {
+                Thread.Sleep(20000);
+            }
 
             try
             {
index e84ea02..f8859ad 100644 (file)
@@ -19,7 +19,6 @@
     <Compile Include="System\Net\WebSockets\WebSocketHandle.Browser.cs" />
     <Compile Include="System\Net\WebSockets\BrowserWebSockets\BrowserWebSocket.cs" />
     <Compile Include="System\Net\WebSockets\BrowserWebSockets\ClientWebSocketOptions.cs" />
-    <Compile Include="System\Net\WebSockets\BrowserWebSockets\ReceivePayload.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="Microsoft.Win32.Primitives" />
index e46a616..b7b1b01 100644 (file)
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.Generic;
-using System.IO;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Threading.Channels;
 using System.Runtime.InteropServices.JavaScript;
 
 using JavaScript = System.Runtime.InteropServices.JavaScript;
 
 namespace System.Net.WebSockets
 {
-    // **Note** on `Task.ConfigureAwait(continueOnCapturedContext: true)` for the WebAssembly Browser.
-    // The current implementation of WebAssembly for the Browser does not have a SynchronizationContext nor a Scheduler
-    // thus forcing the callbacks to run on the main browser thread.  When threading is eventually implemented using
-    // emscripten's threading model of remote worker threads, via SharedArrayBuffer, any API calls will have to be
-    // remoted back to the main thread.  Most APIs only work on the main browser thread.
-    // During discussions the concensus has been that it will not matter right now which value is used for ConfigureAwait
-    // we should put this in place now.
-
     /// <summary>
     /// Provides a client for connecting to WebSocket services.
     /// </summary>
     internal sealed class BrowserWebSocket : WebSocket
     {
-        private readonly Channel<ReceivePayload> _receiveMessageQueue = Channel.CreateUnbounded<ReceivePayload>(new UnboundedChannelOptions()
-        {
-            SingleReader = true,
-            SingleWriter = true,
-        });
-
-        private TaskCompletionSource? _tcsClose;
-        private TaskCompletionSource? _tcsConnect;
-        private WebSocketCloseStatus? _innerWebSocketCloseStatus;
-        private string? _innerWebSocketCloseStatusDescription;
-
+        private WebSocketCloseStatus? _closeStatus;
+        private string? _closeStatusDescription;
         private JSObject? _innerWebSocket;
-
-        private Action<JSObject>? _onOpen;
-        private Action<JSObject>? _onError;
-        private Action<JSObject?>? _onClose;
-        private Action<JSObject>? _onMessage;
-
-        private MemoryStream? _writeBuffer;
-        private ReceivePayload? _bufferedPayload;
-        private readonly CancellationTokenSource _cts;
-        private int _closeStatus;  // variable to track the close status after a close is sent.
-
-        // Stages of this class.
-        private int _state;
-
-        private enum InternalState
-        {
-            Created = 0,
-            Connecting = 1,
-            Connected = 2,
-            CloseSent = 3,
-            Disposed = 4,
-            Aborted = 5,
-        }
-
+        private WebSocketState _state;
         private bool _disposed;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="System.Net.WebSockets.BrowserWebSocket"/> class.
-        /// </summary>
-        public BrowserWebSocket()
-        {
-            _cts = new CancellationTokenSource();
-        }
+        private bool _aborted;
 
         #region Properties
 
-        /// <summary>
-        /// Gets the WebSocket state of the <see cref="System.Net.WebSockets.BrowserWebSocket"/> instance.
-        /// </summary>
-        /// <value>The state.</value>
         public override WebSocketState State
         {
             get
             {
-                if (_innerWebSocket != null && !_innerWebSocket.IsDisposed && _state != (int)InternalState.Aborted)
+                if (_innerWebSocket != null && !_disposed && (_state == WebSocketState.Connecting || _state == WebSocketState.Open || _state == WebSocketState.CloseSent))
                 {
-                    return ReadyStateToDotNetState((int)_innerWebSocket.GetObjectProperty("readyState"));
+                    _state = GetReadyState();
                 }
-                return (InternalState)_state switch
-                {
-                    InternalState.Created => WebSocketState.None,
-                    InternalState.Connecting => WebSocketState.Connecting,
-                    InternalState.Aborted => WebSocketState.Aborted,
-                    InternalState.Disposed => WebSocketState.Closed,
-                    InternalState.CloseSent => WebSocketState.CloseSent,
-                    _ => WebSocketState.Closed
-                };
+                return _state;
             }
         }
 
-        private static WebSocketState ReadyStateToDotNetState(int readyState) =>
-            // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
-            readyState switch
-            {
-                0 => WebSocketState.Connecting, // 0 (CONNECTING)
-                1 => WebSocketState.Open, // 1 (OPEN)
-                2 => WebSocketState.CloseSent, // 2 (CLOSING)
-                3 => WebSocketState.Closed, // 3 (CLOSED)
-                _ => WebSocketState.None
-            };
-
-        public override WebSocketCloseStatus? CloseStatus => _innerWebSocket == null ? null : _innerWebSocketCloseStatus;
-
-        public override string? CloseStatusDescription => _innerWebSocket == null ? null : _innerWebSocketCloseStatusDescription;
-
+        public override WebSocketCloseStatus? CloseStatus => _closeStatus;
+        public override string? CloseStatusDescription => _closeStatusDescription;
         public override string? SubProtocol => _innerWebSocket != null && !_innerWebSocket.IsDisposed ? _innerWebSocket!.GetObjectProperty("protocol")?.ToString() : null;
 
         #endregion Properties
 
-        internal async Task ConnectAsyncJavaScript(Uri uri, CancellationToken cancellationToken, List<string>? requestedSubProtocols)
+        internal Task ConnectAsync(Uri uri, List<string>? requestedSubProtocols, CancellationToken cancellationToken)
         {
-            // Check that we have not started already.
-            int prevState = _state;
-            if (prevState == (int)InternalState.Created)
+            ThrowIfDisposed();
+            if (_state != WebSocketState.None)
             {
-                _state = (int)InternalState.Connecting;
+                throw new InvalidOperationException(SR.net_WebSockets_AlreadyStarted);
             }
+            _state = WebSocketState.Connecting;
+            return ConnectAsyncCore(uri, requestedSubProtocols, cancellationToken);
+        }
 
-            switch ((InternalState)prevState)
-            {
-                case InternalState.Disposed:
-                    throw new ObjectDisposedException(GetType().FullName);
+        public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
 
-                case InternalState.Created:
-                    break;
+            ThrowIfDisposed();
 
-                default:
-                    throw new InvalidOperationException(SR.net_WebSockets_AlreadyStarted);
+            // fast check of previous _state instead of GetReadyState(), the readyState would be validated on JS side
+            if (_state != WebSocketState.Open)
+            {
+                throw new InvalidOperationException(SR.net_WebSockets_NotConnected);
             }
 
-            CancellationTokenRegistration connectRegistration = cancellationToken.Register(cts => ((CancellationTokenSource)cts!).Cancel(), _cts);
-            _tcsConnect = new TaskCompletionSource();
-
-            // For Abort/Dispose.  Calling Abort on the request at any point will close the connection.
-            _cts.Token.Register(s => ((BrowserWebSocket)s!).AbortRequest(), this);
-
-            try
+            if (messageType != WebSocketMessageType.Binary && messageType != WebSocketMessageType.Text)
             {
-                if (requestedSubProtocols?.Count > 0)
-                {
-                    using (JavaScript.Array subProtocols = new JavaScript.Array())
-                    {
-                        foreach (string item in requestedSubProtocols)
-                        {
-                            subProtocols.Push(item);
-                        }
-                        _innerWebSocket = new HostObject("WebSocket", uri.ToString(), subProtocols);
-                    }
-                }
-                else
-                {
-                    _innerWebSocket = new HostObject("WebSocket", uri.ToString());
-                }
-                _innerWebSocket.SetObjectProperty("binaryType", "arraybuffer");
+                throw new ArgumentException(SR.Format(SR.net_WebSockets_Argument_InvalidMessageType,
+                    messageType,
+                    nameof(SendAsync),
+                    WebSocketMessageType.Binary,
+                    WebSocketMessageType.Text,
+                    nameof(CloseOutputAsync)),
+                    nameof(messageType));
+            }
 
-                // Setup the onError callback
-                _onError = errorEvt => errorEvt.Dispose();
+            WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer));
 
-                // Attach the onError callback
-                _innerWebSocket.AddEventListener("error", _onError);
+            return SendAsyncCore(buffer, messageType, endOfMessage, cancellationToken);
+        }
 
-                // Setup the onClose callback
-                _onClose = (closeEvent) => OnCloseCallback(closeEvent, cancellationToken);
+        public override Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
+        {
+            if (cancellationToken.IsCancellationRequested)
+            {
+                return Task.FromException<WebSocketReceiveResult>(new OperationCanceledException(cancellationToken));
+            }
+            ThrowIfDisposed();
+            // fast check of previous _state instead of GetReadyState(), the readyState would be validated on JS side
+            if (_state != WebSocketState.Open && _state != WebSocketState.CloseSent)
+            {
+                throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, _state, "Open, CloseSent"));
+            }
 
-                // Attach the onClose callback
-                _innerWebSocket.AddEventListener("close", _onClose);
+            WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer));
 
-                // Setup the onOpen callback
-                _onOpen = (evt) =>
-                {
-                    using (evt)
-                    {
-                        if (!cancellationToken.IsCancellationRequested)
-                        {
-                            // Change internal _state to 'Connected' to enable the other methods
-                            int prevState = _state;
-                            _state = _state == (int)InternalState.Connecting ? (int)InternalState.Connected : _state;
-                            if (prevState != (int)InternalState.Connecting)
-                            {
-                                // Aborted/Disposed during connect.
-                                _tcsConnect.TrySetException(new ObjectDisposedException(GetType().FullName));
-                            }
-                            else
-                            {
-                                _tcsConnect.TrySetResult();
-                            }
-                        }
-                        else
-                        {
-                            _tcsConnect.TrySetCanceled(cancellationToken);
-                        }
-                    }
-                };
+            return ReceiveAsyncCore(buffer, cancellationToken);
+        }
 
-                // Attach the onOpen callback
-                _innerWebSocket.AddEventListener("open", _onOpen);
+        public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+            ThrowIfDisposed();
 
-                // Setup the onMessage callback
-                _onMessage = (messageEvent) => OnMessageCallback(messageEvent);
+            WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription);
 
-                // Attach the onMessage callaback
-                _innerWebSocket.AddEventListener("message", _onMessage);
-                await _tcsConnect.Task.ConfigureAwait(continueOnCapturedContext: true);
-            }
-            catch (Exception wse)
+            var state = State;
+            if (state == WebSocketState.None || state == WebSocketState.Closed)
             {
-                Dispose();
-                switch (wse)
-                {
-                    case OperationCanceledException:
-                        throw;
-                    default:
-                        throw new WebSocketException(WebSocketError.Faulted, SR.net_webstatus_ConnectFailure, wse);
-                }
+                throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, state, "Connecting, Open, CloseSent, Aborted"));
             }
-            finally
-            {
-                connectRegistration.Unregister();
-            }
-        }
 
+            return state == WebSocketState.Open || state == WebSocketState.Connecting || state == WebSocketState.Aborted
+                ? CloseAsyncCore(closeStatus, statusDescription, false, cancellationToken)
+                : Task.CompletedTask;
+        }
 
-        private void OnCloseCallback(JSObject? closeEvt, CancellationToken cancellationToken)
+        public override Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
         {
-            if (closeEvt != null)
-            {
-                using (closeEvt)
-                {
-                    _innerWebSocketCloseStatus = (WebSocketCloseStatus)closeEvt.GetObjectProperty("code");
-                    _innerWebSocketCloseStatusDescription = closeEvt.GetObjectProperty("reason")?.ToString();
-                }
-            }
-            _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(Array.Empty<byte>(), WebSocketMessageType.Close));
-            NativeCleanup();
-            if ((InternalState)_state == InternalState.Connecting || (InternalState)_state == InternalState.Aborted)
-            {
-                _state = (int)InternalState.Disposed;
-                if (cancellationToken.IsCancellationRequested)
-                {
-                    _tcsConnect?.TrySetCanceled(cancellationToken);
-                }
-                else
-                {
-                    _tcsConnect?.TrySetException(new WebSocketException(WebSocketError.NativeError));
-                }
-            }
-            else
+            cancellationToken.ThrowIfCancellationRequested();
+            ThrowIfDisposed();
+
+            WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription);
+
+            var state = State;
+            if (state == WebSocketState.None || state == WebSocketState.Closed)
             {
-                _tcsClose?.TrySetResult();
+                throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, state, "Connecting, Open, CloseSent, Aborted"));
             }
+
+            return state == WebSocketState.Open || state == WebSocketState.Connecting || state == WebSocketState.Aborted || state == WebSocketState.CloseSent
+                ? CloseAsyncCore(closeStatus, statusDescription, state != WebSocketState.Aborted, cancellationToken)
+                : Task.CompletedTask;
         }
 
-        private void OnMessageCallback(JSObject messageEvent)
+        public override void Abort()
         {
-            // get the events "data"
-            using (messageEvent)
+            if (!_disposed && State != WebSocketState.Closed)
             {
-                ThrowIfNotConnected();
-                // If the messageEvent's data property is marshalled as a JSObject then we are dealing with
-                // binary data
-                object eventData = messageEvent.GetObjectProperty("data");
-                switch (eventData)
+                _state = WebSocketState.Aborted;
+                _aborted = true;
+                if (_innerWebSocket != null)
                 {
-                    case ArrayBuffer buffer:
-                        using (buffer)
-                        {
-                            _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(buffer, WebSocketMessageType.Binary));
-                            break;
-                        }
-                    case JSObject blobData:
-                        using (blobData)
-                        {
-                            // Create a new "FileReader" object
-                            using (HostObject reader = new HostObject("FileReader"))
-                            {
-                                Action<JSObject> loadend = (loadEvent) =>
-                                {
-                                    using (loadEvent)
-                                    using (JSObject target = (JSObject)loadEvent.GetObjectProperty("target"))
-                                    {
-                                        // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readyState
-                                        if ((int)target.GetObjectProperty("readyState") == 2) // DONE - The operation is complete.
-                                        {
-                                            using (ArrayBuffer binResult = (ArrayBuffer)target.GetObjectProperty("result"))
-                                            {
-                                                _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(binResult, WebSocketMessageType.Binary));
-                                            }
-                                        }
-                                    }
-                                };
-                                reader.AddEventListener("loadend", loadend);
-                                reader.Invoke("readAsArrayBuffer", blobData);
-                            }
-                            break;
-                        }
-                    case string message:
-                        {
-                            _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text));
-                            break;
-                        }
-                    default:
-                        throw new NotImplementedException(SR.Format(SR.net_WebSockets_Invalid_Binary_Type, _innerWebSocket?.GetObjectProperty("binaryType").ToString()));
+                    JavaScript.Runtime.WebSocketAbort(_innerWebSocket!);
                 }
             }
         }
 
-        private void NativeCleanup()
-        {
-            // We need to clear the events on websocket as well or stray events
-            // are possible leading to crashes.
-            _innerWebSocket?.RemoveEventListener("close", _onClose);
-            _innerWebSocket?.RemoveEventListener("error", _onError);
-            _innerWebSocket?.RemoveEventListener("open", _onOpen);
-            _innerWebSocket?.RemoveEventListener("message", _onMessage);
-        }
-
         public override void Dispose()
         {
             if (!_disposed)
             {
-                if (_state < (int)InternalState.Aborted) {
-                    _state = (int)InternalState.Disposed;
-                }
+                var state = State;
                 _disposed = true;
-
-                if (!_cts.IsCancellationRequested)
+                if (state < WebSocketState.Aborted && state != WebSocketState.None)
                 {
-                    // registered by the CancellationTokenSource cts in the connect method
-                    _cts.Cancel();
-                    _cts.Dispose();
+                    Abort();
+                }
+                if (state != WebSocketState.Aborted)
+                {
+                    _state = WebSocketState.Closed;
                 }
-
-                _writeBuffer?.Dispose();
-                _receiveMessageQueue.Writer.TryComplete();
-
-                NativeCleanup();
-
                 _innerWebSocket?.Dispose();
+                _innerWebSocket = null;
             }
         }
 
-        // This method is registered by the CancellationTokenSource cts in the connect method
-        // and called by Dispose or Abort so that any open websocket connection can be closed.
-        private async void AbortRequest()
+        private async Task ConnectAsyncCore(Uri uri, List<string>? requestedSubProtocols, CancellationToken cancellationToken)
         {
-            switch (State)
+            try
             {
-                case WebSocketState.Open:
-                case WebSocketState.Connecting:
+                object[]? subProtocols = requestedSubProtocols?.ToArray();
+                var onClose = (int code, string reason) =>
+                {
+                    _closeStatus = (WebSocketCloseStatus)code;
+                    _closeStatusDescription = reason;
+                    WebSocketState state = State;
+                    if (state == WebSocketState.Connecting || state == WebSocketState.Open || state == WebSocketState.CloseSent)
                     {
-                        await CloseAsyncCore(WebSocketCloseStatus.NormalClosure, SR.net_WebSockets_Connection_Aborted, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: true);
-                        // The following code is for those browsers that do not set Close and send an onClose event in certain instances i.e. firefox and safari.
-                        // chrome will send an onClose event and we tear down the websocket there.
-                        if (ReadyStateToDotNetState(_closeStatus) == WebSocketState.CloseSent)
-                        {
-                            _writeBuffer?.Dispose();
-                            _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(Array.Empty<byte>(), WebSocketMessageType.Close));
-                            _receiveMessageQueue.Writer.TryComplete();
-                            NativeCleanup();
-                            _tcsConnect?.TrySetCanceled();
-                        }
+                        _state = WebSocketState.Closed;
                     }
-                    break;
-            }
-        }
-
-        /// <summary>
-        /// Send data on <see cref="System.Net.WebSockets.ClientWebSocket"/> as an asynchronous operation.
-        /// </summary>
-        /// <returns>The async.</returns>
-        /// <param name="buffer">Buffer.</param>
-        /// <param name="messageType">Message type.</param>
-        /// <param name="endOfMessage">If set to <c>true</c> end of message.</param>
-        /// <param name="cancellationToken">Cancellation token.</param>
-        public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
-        {
-            ThrowIfNotConnected();
-
-            if (messageType != WebSocketMessageType.Binary &&
-                    messageType != WebSocketMessageType.Text)
-            {
-                throw new ArgumentException(SR.Format(SR.net_WebSockets_Argument_InvalidMessageType,
-                    messageType,
-                    nameof(SendAsync),
-                    WebSocketMessageType.Binary,
-                    WebSocketMessageType.Text,
-                    nameof(CloseOutputAsync)),
-                    nameof(messageType));
-            }
-
-            WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer));
-
-            if (!endOfMessage)
-            {
-                _writeBuffer ??= new MemoryStream();
-                _writeBuffer.Write(buffer.Array!, buffer.Offset, buffer.Count);
-                return Task.CompletedTask;
-            }
+                };
 
-            MemoryStream? writtenBuffer = _writeBuffer;
-            _writeBuffer = null;
+                var openTask = JavaScript.Runtime.WebSocketOpen(uri.ToString(), subProtocols, onClose, out _innerWebSocket, out int promiseJSHandle);
+                var wrappedTask = CancelationHelper(openTask, promiseJSHandle, cancellationToken, _state);
 
-            if (writtenBuffer is not null)
-            {
-                writtenBuffer.Write(buffer.Array!, buffer.Offset, buffer.Count);
-                if (writtenBuffer.TryGetBuffer(out var tmpBuffer))
-                {
-                    buffer = tmpBuffer;
-                }
-                else
+                await wrappedTask.ConfigureAwait(true);
+                if (State == WebSocketState.Connecting)
                 {
-                    buffer = writtenBuffer.ToArray();
+                    _state = WebSocketState.Open;
                 }
             }
-
-            try
+            catch (OperationCanceledException ex)
             {
-                switch (messageType)
+                _state = WebSocketState.Closed;
+                if (_aborted)
                 {
-                    case WebSocketMessageType.Binary:
-                        using (Uint8Array uint8Buffer = Uint8Array.From(buffer))
-                        {
-                            _innerWebSocket!.Invoke("send", uint8Buffer);
-                        }
-                        break;
-                    default:
-                        string strBuffer = buffer.Array == null ? string.Empty : Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
-                        _innerWebSocket!.Invoke("send", strBuffer);
-                        break;
+                    throw new WebSocketException(WebSocketError.Faulted, SR.net_webstatus_ConnectFailure, ex);
                 }
+                throw;
             }
-            catch (Exception excb)
-            {
-                return Task.FromException(new WebSocketException(WebSocketError.NativeError, excb));
-            }
-            finally
+            catch (Exception)
             {
-                writtenBuffer?.Dispose();
+                Dispose();
+                throw;
             }
-            return Task.CompletedTask;
         }
 
-        // This method is registered by the CancellationTokenSource in the receive async method
-        private async void CancelRequest()
+        private async Task SendAsyncCore(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
         {
-            int prevState = _state;
-            _state = (int)InternalState.Aborted;
-            _receiveMessageQueue.Writer.TryComplete();
-            if (prevState == (int)InternalState.Connected || prevState == (int)InternalState.Connecting)
+            try
             {
-                if (prevState == (int)InternalState.Connecting)
-                    _state = (int)InternalState.CloseSent;
-                await CloseAsyncCore(WebSocketCloseStatus.NormalClosure, SR.net_WebSockets_Connection_Aborted, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: true);
-            }
-        }
-
-        /// <summary>
-        /// Receives data on <see cref="System.Net.WebSockets.ClientWebSocket"/> as an asynchronous operation.
-        /// </summary>
-        /// <returns>The async.</returns>
-        /// <param name="buffer">Buffer.</param>
-        /// <param name="cancellationToken">Cancellation token.</param>
-        public override async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
-        {
+                var sendTask = JavaScript.Runtime.WebSocketSend(_innerWebSocket!, buffer, (int)messageType, endOfMessage, out int promiseJSHandle);
+                if (sendTask == null)
+                {
+                    // return synchronously
+                    return;
+                }
+                var wrappedTask = CancelationHelper(sendTask, promiseJSHandle, cancellationToken, _state);
 
-            if (cancellationToken.IsCancellationRequested)
-            {
-                return await Task.FromException<WebSocketReceiveResult>(new OperationCanceledException()).ConfigureAwait(continueOnCapturedContext: true);
+                await wrappedTask.ConfigureAwait(true);
             }
-
-            CancellationTokenSource _receiveCTS = new CancellationTokenSource();
-            CancellationTokenRegistration receiveRegistration = cancellationToken.Register(cts => ((CancellationTokenSource)cts!).Cancel(), _receiveCTS);
-            _receiveCTS.Token.Register(s => ((BrowserWebSocket)s!).CancelRequest(), this);
-
-            try
+            catch (OperationCanceledException)
             {
-                WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer));
-
-                ThrowIfDisposed();
-                ThrowOnInvalidState(State, WebSocketState.Open, WebSocketState.CloseSent);
-                _bufferedPayload ??= await _receiveMessageQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: true);
-                bool endOfMessage = _bufferedPayload!.BufferPayload(buffer, out WebSocketReceiveResult receiveResult);
-                if (endOfMessage)
-                    _bufferedPayload = null;
-                return receiveResult;
+                throw;
             }
-            catch (Exception exc)
+            catch (JSException ex)
             {
-                switch (exc)
+                if (ex.Message.StartsWith("InvalidState:"))
                 {
-                    case OperationCanceledException:
-                        return await Task.FromException<WebSocketReceiveResult>(exc).ConfigureAwait(continueOnCapturedContext: true);
-                    case ChannelClosedException:
-                        return await Task.FromException<WebSocketReceiveResult>(new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, State, "Open, CloseSent"))).ConfigureAwait(continueOnCapturedContext: true);
-                    default:
-                        return await Task.FromException<WebSocketReceiveResult>(new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, State, "Open, CloseSent"))).ConfigureAwait(continueOnCapturedContext: true);
+                    throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, State, "Open"), ex);
                 }
-            }
-            finally
-            {
-                receiveRegistration.Unregister();
+                throw new WebSocketException(WebSocketError.NativeError, ex);
             }
         }
 
-        /// <summary>
-        /// Aborts the connection and cancels any pending IO operations.
-        /// </summary>
-        public override void Abort()
+        private async Task<WebSocketReceiveResult> ReceiveAsyncCore(ArraySegment<byte> buffer, CancellationToken cancellationToken)
         {
-            if (_state != (int)InternalState.Disposed)
+            try
             {
-                int prevState = _state;
-                if (prevState != (int)InternalState.Connecting)
-                {
-                    _state = (int)InternalState.Aborted;
-                }
-
-                if (prevState < (int)InternalState.Aborted)
+                ArraySegment<int> response = new ArraySegment<int>(new int[3]);
+                var receiveTask = JavaScript.Runtime.WebSocketReceive(_innerWebSocket!, buffer, response, out int promiseJSHandle);
+                if (receiveTask == null)
                 {
-                    _cts.Cancel(true);
-                    _tcsClose?.TrySetResult();
+                    // return synchronously
+                    return ConvertResponse(response);
                 }
-            }
-        }
 
-        public override Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
-        {
-            _writeBuffer = null;
+                var wrappedTask = CancelationHelper(receiveTask, promiseJSHandle, cancellationToken, _state);
+                await wrappedTask.ConfigureAwait(true);
 
-            WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription);
-
-            try
+                return ConvertResponse(response);
+            }
+            catch (OperationCanceledException)
             {
-                ThrowOnInvalidState(State, WebSocketState.Connecting, WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent);
+                throw;
             }
-            catch (Exception exc)
+            catch (JSException ex)
             {
-                return Task.FromException(exc);
+                if (ex.Message.StartsWith("InvalidState:"))
+                {
+                    throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, State, "Open, CloseSent"), ex);
+                }
+                throw new WebSocketException(WebSocketError.NativeError, ex);
             }
-            return State == WebSocketState.CloseSent ? Task.CompletedTask : CloseAsyncCore(closeStatus, statusDescription, cancellationToken);
         }
 
-        private Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
+        private WebSocketReceiveResult ConvertResponse(ArraySegment<int> response)
         {
-            try
-            {
-                _tcsClose = new TaskCompletionSource();
-                _innerWebSocketCloseStatus = closeStatus;
-                _innerWebSocketCloseStatusDescription = statusDescription;
-                _innerWebSocket!.Invoke("close", (int)closeStatus, statusDescription);
-                if (_innerWebSocket != null && !_innerWebSocket.IsDisposed && _state != (int)InternalState.Aborted)
-                {
-                    _closeStatus = (int)_innerWebSocket.GetObjectProperty("readyState");
-                }
-                else
-                {
-                    _closeStatus = 3; // (CLOSED)
-                }
+            const int countIndex = 0;
+            const int typeIndex = 1;
+            const int endIndex = 2;
 
-                return _tcsClose.Task;
-            }
-            catch (Exception exc)
+            WebSocketMessageType messageType = (WebSocketMessageType)response[typeIndex];
+            if (messageType == WebSocketMessageType.Close)
             {
-                return Task.FromException(exc);
+                return new WebSocketReceiveResult(response[countIndex], messageType, response[endIndex] != 0, CloseStatus, CloseStatusDescription);
             }
+            return new WebSocketReceiveResult(response[countIndex], messageType, response[endIndex] != 0);
         }
 
-        public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
+        private async Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? statusDescription, bool waitForCloseReceived, CancellationToken cancellationToken)
         {
-            _writeBuffer = null;
+            _closeStatus = closeStatus;
+            _closeStatusDescription = statusDescription;
 
-            WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription);
-
-            try
+            var closeTask = JavaScript.Runtime.WebSocketClose(_innerWebSocket!, (int)closeStatus, statusDescription, waitForCloseReceived, out int promiseJSHandle);
+            if (closeTask != null)
             {
-                ThrowOnInvalidState(State, WebSocketState.Connecting, WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent);
+                var wrappedTask = CancelationHelper(closeTask, promiseJSHandle, cancellationToken, _state);
+                await wrappedTask.ConfigureAwait(true);
             }
-            catch (Exception exc)
+
+            var state = State;
+            if (state == WebSocketState.Open || state == WebSocketState.Connecting || state == WebSocketState.CloseSent)
             {
-                return Task.FromException(exc);
+                _state = waitForCloseReceived ? WebSocketState.Closed : WebSocketState.CloseSent;
             }
-            return CloseOutputAsyncCore(closeStatus, statusDescription, cancellationToken);
         }
 
-        private Task CloseOutputAsyncCore(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
+        private async ValueTask<object> CancelationHelper(Task<object> jsTask, int promiseJSHandle, CancellationToken cancellationToken, WebSocketState previousState)
         {
+            if (jsTask.IsCompletedSuccessfully)
+            {
+                return jsTask.Result;
+            }
             try
             {
-                // as per comments
-                // - We clear all events on the websocket (including onClose),
-                // - call websocket.close()
-                // - then call the user provided onClose method manually.
-                NativeCleanup();
-                _innerWebSocketCloseStatus = closeStatus;
-                _innerWebSocketCloseStatusDescription = statusDescription;
-                _innerWebSocket!.Invoke("close", (int)closeStatus, statusDescription);
-                if (_innerWebSocket != null && !_innerWebSocket.IsDisposed && _state != (int)InternalState.Aborted)
+                using (var receiveRegistration = cancellationToken.Register(() =>
                 {
-                    _closeStatus = (int)_innerWebSocket.GetObjectProperty("readyState");
-                }
-                else
+                    // this check makes sure that promiseJSHandle is still valid handle
+                    if (!jsTask.IsCompleted)
+                    {
+                        JavaScript.Runtime.CancelPromise(promiseJSHandle);
+                    }
+                }))
                 {
-                    _closeStatus = 3; // (CLOSED)
+                    return await jsTask.ConfigureAwait(true);
                 }
-                OnCloseCallback(null, cancellationToken);
-                return Task.CompletedTask;
             }
-            catch (Exception exc)
+            catch (JSException ex)
             {
-                return Task.FromException(exc);
+                if (State == WebSocketState.Aborted)
+                {
+                    throw new OperationCanceledException(nameof(WebSocketState.Aborted), ex);
+                }
+
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    _state = WebSocketState.Aborted;
+                    throw new OperationCanceledException(cancellationToken);
+                }
+                if (ex.Message == "OperationCanceledException")
+                {
+                    _state = WebSocketState.Aborted;
+                    throw new OperationCanceledException("The operation was cancelled.", ex, cancellationToken);
+                }
+                if (previousState == WebSocketState.Connecting)
+                {
+                    throw new WebSocketException(WebSocketError.Faulted, SR.net_webstatus_ConnectFailure, ex);
+                }
+                throw new WebSocketException(WebSocketError.NativeError, ex);
             }
         }
 
-        private void ThrowIfNotConnected()
+        private void ThrowIfDisposed()
         {
-            if (_state == (int)InternalState.Disposed)
+            if (_disposed)
             {
                 throw new ObjectDisposedException(GetType().FullName);
             }
-            else if (State != WebSocketState.Open)
-            {
-                throw new InvalidOperationException(SR.net_WebSockets_NotConnected);
-            }
         }
 
-        private void ThrowIfDisposed()
+        private WebSocketState GetReadyState()
         {
-            if (_state == (int)InternalState.Disposed)
+            int readyState = (int)_innerWebSocket!.GetObjectProperty("readyState");
+
+            // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
+            return readyState switch
             {
-                throw new ObjectDisposedException(GetType().FullName);
-            }
+                0 => WebSocketState.Connecting, // 0 (CONNECTING)
+                1 => WebSocketState.Open, // 1 (OPEN)
+                2 => WebSocketState.CloseSent, // 2 (CLOSING)
+                3 => WebSocketState.Closed, // 3 (CLOSED)
+                _ => WebSocketState.None
+            };
         }
     }
 }
diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ReceivePayload.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ReceivePayload.cs
deleted file mode 100644 (file)
index 9de3e9c..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Runtime.InteropServices.JavaScript;
-
-namespace System.Net.WebSockets
-{
-    internal sealed class ReceivePayload
-    {
-        private readonly byte[] _dataMessageReceived;
-        private readonly WebSocketMessageType _messageType;
-        private int _unconsumedDataOffset;
-
-        public ReceivePayload(ArrayBuffer arrayBuffer, WebSocketMessageType messageType)
-        {
-            using (var bin = new Uint8Array(arrayBuffer))
-            {
-                _dataMessageReceived = bin.ToArray();
-                _messageType = messageType;
-            }
-        }
-
-        public ReceivePayload(ArraySegment<byte> payload, WebSocketMessageType messageType)
-        {
-            _dataMessageReceived = payload.Array ?? Array.Empty<byte>();
-            _messageType = messageType;
-        }
-
-        public bool BufferPayload(ArraySegment<byte> arraySegment, out WebSocketReceiveResult receiveResult)
-        {
-            int bytesTransferred = Math.Min(_dataMessageReceived.Length - _unconsumedDataOffset, arraySegment.Count);
-            bool endOfMessage = (_dataMessageReceived.Length - _unconsumedDataOffset) <= arraySegment.Count;
-            Buffer.BlockCopy(_dataMessageReceived, _unconsumedDataOffset, arraySegment.Array!, arraySegment.Offset, bytesTransferred);
-            _unconsumedDataOffset += arraySegment.Count;
-            receiveResult = new WebSocketReceiveResult(bytesTransferred, _messageType, endOfMessage);
-            return endOfMessage;
-        }
-    }
-
-}
index 1dde389..3a948cc 100644 (file)
@@ -8,7 +8,6 @@ namespace System.Net.WebSockets
 {
     internal sealed class WebSocketHandle
     {
-        private readonly CancellationTokenSource _abortSource = new CancellationTokenSource();
         private WebSocketState _state = WebSocketState.Connecting;
 
         public WebSocket? WebSocket { get; private set; }
@@ -24,53 +23,17 @@ namespace System.Net.WebSockets
 
         public void Abort()
         {
-            _abortSource.Cancel();
+            _state = WebSocketState.Aborted;
             WebSocket?.Abort();
         }
 
-        public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
+        public Task ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         {
-            try
-            {
-                cancellationToken.ThrowIfCancellationRequested();  // avoid allocating a WebSocket object if cancellation was requested before connect
-                CancellationTokenSource? linkedCancellation;
-                CancellationTokenSource externalAndAbortCancellation;
-                if (cancellationToken.CanBeCanceled) // avoid allocating linked source if external token is not cancelable
-                {
-                    linkedCancellation =
-                        externalAndAbortCancellation =
-                        CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _abortSource.Token);
-                }
-                else
-                {
-                    linkedCancellation = null;
-                    externalAndAbortCancellation = _abortSource;
-                }
+            cancellationToken.ThrowIfCancellationRequested();
 
-                using (linkedCancellation)
-                {
-                    WebSocket = new BrowserWebSocket();
-                    await ((BrowserWebSocket)WebSocket).ConnectAsyncJavaScript(uri, externalAndAbortCancellation.Token, options.RequestedSubProtocols).ConfigureAwait(continueOnCapturedContext: true);
-                    externalAndAbortCancellation.Token.ThrowIfCancellationRequested();
-                }
-            }
-            catch (Exception exc)
-            {
-                if (_state < WebSocketState.Closed)
-                {
-                    _state = WebSocketState.Closed;
-                }
-
-                Abort();
-
-                switch (exc) {
-                    case WebSocketException:
-                    case OperationCanceledException _ when cancellationToken.IsCancellationRequested:
-                        throw;
-                    default:
-                        throw new WebSocketException(WebSocketError.Faulted, SR.net_webstatus_ConnectFailure, exc);
-                }
-            }
+            var ws = new BrowserWebSocket();
+            WebSocket = ws;
+            return ws.ConnectAsync(uri, options.RequestedSubProtocols, cancellationToken);
         }
     }
 }
index da80a16..9e400c3 100644 (file)
@@ -19,13 +19,13 @@ namespace System.Net.WebSockets.Client.Tests
         {
             using (var cws = new ClientWebSocket())
             {
-                var cts = new CancellationTokenSource(500);
+                var cts = new CancellationTokenSource(100);
 
                 var ub = new UriBuilder(server);
-                ub.Query = "delay10sec";
+                ub.Query = "delay20sec";
 
-                await Assert.ThrowsAnyAsync<OperationCanceledException>(() => cws.ConnectAsync(ub.Uri, cts.Token));
-                Assert.Equal(WebSocketState.Closed, cws.State);
+                var ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => cws.ConnectAsync(ub.Uri, cts.Token));
+                Assert.True(WebSocketState.Closed == cws.State, $"Actual {cws.State} when {ex}");
             }
         }
 
index ab8ebe2..91e5f8c 100644 (file)
@@ -69,20 +69,20 @@ namespace System.Net.WebSockets.Client.Tests
                     await action(cws);
                     // Operation finished before CTS expired.
                 }
-                catch (OperationCanceledException)
+                catch (OperationCanceledException exception)
                 {
                     // Expected exception
-                    Assert.Equal(WebSocketState.Aborted, cws.State);
+                    Assert.True(WebSocketState.Aborted == cws.State, $"Actual {cws.State} when {exception}");
                 }
-                catch (ObjectDisposedException)
+                catch (ObjectDisposedException exception)
                 {
                     // Expected exception
-                    Assert.Equal(WebSocketState.Aborted, cws.State);
+                    Assert.True(WebSocketState.Aborted == cws.State, $"Actual {cws.State} when {exception}");
                 }
                 catch (WebSocketException exception)
                 {
-                    Assert.Equal(WebSocketError.InvalidState, exception.WebSocketErrorCode);
-                    Assert.Equal(WebSocketState.Aborted, cws.State);
+                    Assert.True(WebSocketError.InvalidState == exception.WebSocketErrorCode, $"Actual WebSocketErrorCode {exception.WebSocketErrorCode} when {exception}");
+                    Assert.True(WebSocketState.Aborted == cws.State, $"Actual {cws.State} when {exception}");
                 }
             }
         }
index a10e9e4..38a53d5 100644 (file)
@@ -44,6 +44,7 @@ namespace System.Net.WebSockets.Client.Tests
                 // Verify received server-initiated close message.
                 Assert.Equal(WebSocketCloseStatus.NormalClosure, recvResult.CloseStatus);
                 Assert.Equal(closeWebSocketMetaCommand, recvResult.CloseStatusDescription);
+                Assert.Equal(WebSocketMessageType.Close, recvResult.MessageType);
 
                 // Verify current websocket state as CloseReceived which indicates only partial close.
                 Assert.Equal(WebSocketState.CloseReceived, cws.State);
index 43b119e..31c375a 100644 (file)
@@ -187,7 +187,7 @@ namespace System.Net.WebSockets.Client.Tests
                 if (PlatformDetection.IsNetCore) // bug fix in netcoreapp: https://github.com/dotnet/corefx/pull/35960
                 {
                     Assert.True(ex.WebSocketErrorCode == WebSocketError.Faulted ||
-                        ex.WebSocketErrorCode == WebSocketError.NotAWebSocket);
+                        ex.WebSocketErrorCode == WebSocketError.NotAWebSocket, $"Actual WebSocketErrorCode {ex.WebSocketErrorCode} {ex.InnerException?.Message} \n {ex}");
                 }
                 Assert.Equal(WebSocketState.Closed, cws.State);
             }
index b8b4159..1c94c04 100644 (file)
@@ -158,6 +158,7 @@ namespace System.Net.WebSockets.Client.Tests
 
         [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))]
         [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))]
+        // This will also pass when no exception is thrown. Current implementation doesn't throw.
         public async Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server)
         {
             using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output))
@@ -217,6 +218,7 @@ namespace System.Net.WebSockets.Client.Tests
 
         [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))]
         [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))]
+        // This will also pass when no exception is thrown. Current implementation doesn't throw.
         public async Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server)
         {
             using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output))
@@ -320,42 +322,55 @@ namespace System.Net.WebSockets.Client.Tests
 
         [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))]
         [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/53957", TestPlatforms.Browser)]
         public async Task SendReceive_VaryingLengthBuffers_Success(Uri server)
         {
-            using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output))
+            CancellationTokenSource ctsDefault = null;
+            try
             {
-                var rand = new Random();
-                var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds);
-
-                // Values chosen close to boundaries in websockets message length handling as well
-                // as in vectors used in mask application.
-                foreach (int bufferSize in new int[] { 1, 3, 4, 5, 31, 32, 33, 125, 126, 127, 128, ushort.MaxValue - 1, ushort.MaxValue, ushort.MaxValue + 1, ushort.MaxValue * 2 })
+                using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output))
                 {
-                    byte[] sendBuffer = new byte[bufferSize];
-                    rand.NextBytes(sendBuffer);
-                    await SendAsync(cws, new ArraySegment<byte>(sendBuffer), WebSocketMessageType.Binary, true, ctsDefault.Token);
+                    var rand = new Random();
+                    ctsDefault = new CancellationTokenSource(TimeOutMilliseconds);
 
-                    byte[] receiveBuffer = new byte[bufferSize];
-                    int totalReceived = 0;
-                    while (true)
+                    // Values chosen close to boundaries in websockets message length handling as well
+                    // as in vectors used in mask application.
+                    foreach (int bufferSize in new int[] { 1, 3, 4, 5, 31, 32, 33, 125, 126, 127, 128, ushort.MaxValue - 1, ushort.MaxValue, ushort.MaxValue + 1, ushort.MaxValue * 2 })
                     {
-                        WebSocketReceiveResult recvResult = await ReceiveAsync(
-                            cws,
-                            new ArraySegment<byte>(receiveBuffer, totalReceived, receiveBuffer.Length - totalReceived),
-                            ctsDefault.Token);
+                        byte[] sendBuffer = new byte[bufferSize];
+                        rand.NextBytes(sendBuffer);
+                        await SendAsync(cws, new ArraySegment<byte>(sendBuffer), WebSocketMessageType.Binary, true, ctsDefault.Token);
 
-                        Assert.InRange(recvResult.Count, 0, receiveBuffer.Length - totalReceived);
-                        totalReceived += recvResult.Count;
+                        byte[] receiveBuffer = new byte[bufferSize];
+                        int totalReceived = 0;
+                        while (true)
+                        {
+                            WebSocketReceiveResult recvResult = await ReceiveAsync(
+                                cws,
+                                new ArraySegment<byte>(receiveBuffer, totalReceived, receiveBuffer.Length - totalReceived),
+                                ctsDefault.Token);
+
+                            Assert.InRange(recvResult.Count, 0, receiveBuffer.Length - totalReceived);
+                            totalReceived += recvResult.Count;
+
+                            if (recvResult.EndOfMessage) break;
+                        }
 
-                        if (recvResult.EndOfMessage) break;
+                        Assert.Equal(receiveBuffer.Length, totalReceived);
+                        Assert.Equal<byte>(sendBuffer, receiveBuffer);
                     }
 
-                    Assert.Equal(receiveBuffer.Length, totalReceived);
-                    Assert.Equal<byte>(sendBuffer, receiveBuffer);
+                    await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_VaryingLengthBuffers_Success", ctsDefault.Token);
                 }
-
-                await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_VaryingLengthBuffers_Success", ctsDefault.Token);
+            }
+            catch (OperationCanceledException ex)
+            {
+                if (PlatformDetection.IsBrowser && ctsDefault != null && ex.CancellationToken == ctsDefault.Token)
+                {
+                    _output.WriteLine($"ActiveIssue https://github.com/dotnet/runtime/issues/53957");
+                    _output.WriteLine($"The test {nameof(SendReceive_VaryingLengthBuffers_Success)} took more than {TimeOutMilliseconds} to finish, it was canceled.");
+                    return;
+                }
+                throw;
             }
         }
 
@@ -363,28 +378,42 @@ namespace System.Net.WebSockets.Client.Tests
         [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))]
         public async Task SendReceive_Concurrent_Success(Uri server)
         {
-            using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output))
+            CancellationTokenSource ctsDefault = null;
+            try
             {
-                var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds);
-
-                byte[] receiveBuffer = new byte[10];
-                byte[] sendBuffer = new byte[10];
-                for (int i = 0; i < sendBuffer.Length; i++)
+                using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output))
                 {
-                    sendBuffer[i] = (byte)i;
-                }
+                    ctsDefault = new CancellationTokenSource(TimeOutMilliseconds);
+
+                    byte[] receiveBuffer = new byte[10];
+                    byte[] sendBuffer = new byte[10];
+                    for (int i = 0; i < sendBuffer.Length; i++)
+                    {
+                        sendBuffer[i] = (byte)i;
+                    }
 
-                for (int i = 0; i < sendBuffer.Length; i++)
+                    for (int i = 0; i < sendBuffer.Length; i++)
+                    {
+                        Task<WebSocketReceiveResult> receive = ReceiveAsync(cws, new ArraySegment<byte>(receiveBuffer, receiveBuffer.Length - i - 1, 1), ctsDefault.Token);
+                        Task send = SendAsync(cws, new ArraySegment<byte>(sendBuffer, i, 1), WebSocketMessageType.Binary, true, ctsDefault.Token);
+                        await Task.WhenAll(receive, send);
+                        Assert.Equal(1, receive.Result.Count);
+                    }
+                    await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_Concurrent_Success", ctsDefault.Token);
+
+                    Array.Reverse(receiveBuffer);
+                    Assert.Equal<byte>(sendBuffer, receiveBuffer);
+                }
+            }
+            catch (OperationCanceledException ex)
+            {
+                if (PlatformDetection.IsBrowser && ctsDefault != null && ex.CancellationToken == ctsDefault.Token)
                 {
-                    Task<WebSocketReceiveResult> receive = ReceiveAsync(cws, new ArraySegment<byte>(receiveBuffer, receiveBuffer.Length - i - 1, 1), ctsDefault.Token);
-                    Task send = SendAsync(cws, new ArraySegment<byte>(sendBuffer, i, 1), WebSocketMessageType.Binary, true, ctsDefault.Token);
-                    await Task.WhenAll(receive, send);
-                    Assert.Equal(1, receive.Result.Count);
+                    _output.WriteLine($"ActiveIssue https://github.com/dotnet/runtime/issues/57519");
+                    _output.WriteLine($"The test {nameof(SendReceive_Concurrent_Success)} took more than {TimeOutMilliseconds} to finish, it was canceled.");
+                    return;
                 }
-                await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_VaryingLengthBuffers_Success", ctsDefault.Token);
-
-                Array.Reverse(receiveBuffer);
-                Assert.Equal<byte>(sendBuffer, receiveBuffer);
+                throw;
             }
         }
 
index 0f7d988..d91fd24 100644 (file)
@@ -85,6 +85,11 @@ namespace System.Runtime.InteropServices.JavaScript
             return tcs.Task;
         }
 
+        public static object TaskFromResult(object? obj)
+        {
+            return Task.FromResult(obj);
+        }
+
         public static void SetupJSContinuation(Task task, JSObject continuationObj)
         {
             if (task.IsCompleted)
index 65fb97f..60e6e4d 100644 (file)
@@ -180,5 +180,75 @@ namespace System.Runtime.InteropServices.JavaScript
         {
             return new Uri(uri);
         }
+
+        public static void CancelPromise(int promiseJSHandle)
+        {
+            var res = Interop.Runtime.CancelPromise(promiseJSHandle, out int exception);
+            if (exception != 0)
+                throw new JSException(res);
+        }
+
+        public static Task<object> WebSocketOpen(string uri, object[]? subProtocols, Delegate onClosed, out JSObject webSocket, out int promiseJSHandle)
+        {
+            var res = Interop.Runtime.WebSocketOpen(uri, subProtocols, onClosed, out int webSocketJSHandle, out promiseJSHandle, out int exception);
+            if (exception != 0)
+                throw new JSException((string)res);
+            webSocket = new JSObject((IntPtr)webSocketJSHandle);
+
+            return (Task<object>)res;
+        }
+
+        public static unsafe Task<object>? WebSocketSend(JSObject webSocket, ArraySegment<byte> buffer, int messageType, bool endOfMessage, out int promiseJSHandle)
+        {
+            fixed (byte* messagePtr = buffer.Array)
+            {
+                var res = Interop.Runtime.WebSocketSend(webSocket.JSHandle, (IntPtr)messagePtr, buffer.Offset, buffer.Count, messageType, endOfMessage, out promiseJSHandle, out int exception);
+                if (exception != 0)
+                    throw new JSException((string)res);
+
+                if (res == null)
+                {
+                    return null;
+                }
+
+                return (Task<object>)res;
+            }
+        }
+
+        public static unsafe Task<object>? WebSocketReceive(JSObject webSocket, ArraySegment<byte> buffer, ReadOnlySpan<int> response, out int promiseJSHandle)
+        {
+            fixed (int* responsePtr = response)
+            fixed (byte* bufferPtr = buffer.Array)
+            {
+                var res = Interop.Runtime.WebSocketReceive(webSocket.JSHandle, (IntPtr)bufferPtr, buffer.Offset, buffer.Count, (IntPtr)responsePtr, out promiseJSHandle, out int exception);
+                if (exception != 0)
+                    throw new JSException((string)res);
+                if (res == null)
+                {
+                    return null;
+                }
+                return (Task<object>)res;
+            }
+        }
+
+        public static Task<object>? WebSocketClose(JSObject webSocket, int code, string? reason, bool waitForCloseReceived, out int promiseJSHandle)
+        {
+            var res = Interop.Runtime.WebSocketClose(webSocket.JSHandle, code, reason, waitForCloseReceived, out promiseJSHandle, out int exception);
+            if (exception != 0)
+                throw new JSException((string)res);
+
+            if (res == null)
+            {
+                return null;
+            }
+            return (Task<object>)res;
+        }
+
+        public static void WebSocketAbort(JSObject webSocket)
+        {
+            var res = Interop.Runtime.WebSocketAbort(webSocket.JSHandle, out int exception);
+            if (exception != 0)
+                throw new JSException(res);
+        }
     }
 }
index 8d51dc8..0eae959 100644 (file)
@@ -13,6 +13,8 @@ var BindingSupportLib = {
                _js_handle_free_list: [],
                _next_js_handle: 1,
 
+               ws_send_buffer_blocking_threshold: 65536,
+
                mono_wasm_marshal_enum_as_int: true,
                mono_bindings_init: function (binding_asm) {
                        this.BINDING_ASM = binding_asm;
@@ -42,7 +44,17 @@ var BindingSupportLib = {
                        this.cs_owned_js_handle_symbol = Symbol.for("wasm cs_owned_js_handle");
                        this.delegate_invoke_symbol = Symbol.for("wasm delegate_invoke");
                        this.delegate_invoke_signature_symbol = Symbol.for("wasm delegate_invoke_signature");
+                       this.promise_control_symbol = Symbol.for("wasm promise_control");
                        this.listener_registration_count_symbol = Symbol.for("wasm listener_registration_count");
+                       this.wasm_ws_pending_send_buffer = Symbol.for("wasm ws_pending_send_buffer");
+                       this.wasm_ws_pending_send_buffer_offset = Symbol.for("wasm ws_pending_send_buffer_offset");
+                       this.wasm_ws_pending_send_buffer_type = Symbol.for("wasm ws_pending_send_buffer_type");
+                       this.wasm_ws_pending_receive_event_queue = Symbol.for("wasm ws_pending_receive_event_queue");
+                       this.wasm_ws_pending_receive_promise_queue = Symbol.for("wasm ws_pending_receive_promise_queue");
+                       this.wasm_ws_pending_open_promise = Symbol.for("wasm ws_pending_open_promise");
+                       this.wasm_ws_pending_close_promises = Symbol.for("wasm ws_pending_close_promises");
+                       this.wasm_ws_pending_send_promises = Symbol.for("wasm ws_pending_send_promises");
+                       this.wasm_ws_is_aborted = Symbol.for("wasm ws_is_aborted");
 
                        // please keep System.Runtime.InteropServices.JavaScript.Runtime.MappedType in sync
                        Object.prototype[this.wasm_type_symbol] = 0;
@@ -147,6 +159,7 @@ var BindingSupportLib = {
                        this._set_tcs_result = bind_runtime_method ("SetTaskSourceResult","io");
                        this._set_tcs_failure = bind_runtime_method ("SetTaskSourceFailure","is");
                        this._get_tcs_task = bind_runtime_method ("GetTaskSourceTask","i!");
+                       this._task_from_result = bind_runtime_method ("TaskFromResult","o!");
                        this._setup_js_cont = bind_runtime_method ("SetupJSContinuation", "mo");
                        
                        this._object_to_string = bind_runtime_method ("ObjectToString", "m");
@@ -162,16 +175,6 @@ var BindingSupportLib = {
                                return Promise.resolve(js_obj) === js_obj ||
                                                ((typeof js_obj === "object" || typeof js_obj === "function") && typeof js_obj.then === "function")
                        };
-                       this.isChromium = false;
-                       if (globalThis.navigator) {
-                               var nav = globalThis.navigator;
-                               if (nav.userAgentData && nav.userAgentData.brands) {
-                                       this.isChromium = nav.userAgentData.brands.some((i) => i.brand == 'Chromium');
-                               }
-                               else if (globalThis.navigator.userAgent) {
-                                       this.isChromium = nav.userAgent.includes("Chrome");
-                               }
-                       }
 
                        this._empty_string = "";
                        this._empty_string_ptr = 0;
@@ -195,8 +198,10 @@ var BindingSupportLib = {
                        // As such, it's not possible for this gc_handle to be invoked by JS anymore, so
                        //  we can release the tracking weakref (it's null now, by definition),
                        //  and tell the C# side to stop holding a reference to the managed object.
-                       this._js_owned_object_table.delete(gc_handle);
-                       this._release_js_owned_object_by_gc_handle(gc_handle);
+                       // "The FinalizationRegistry callback is called potentially multiple times"
+                       if (this._js_owned_object_table.delete(gc_handle)) {
+                               this._release_js_owned_object_by_gc_handle(gc_handle);
+                       }
                },
 
                _lookup_js_owned_object: function (gc_handle) {
@@ -246,7 +251,7 @@ var BindingSupportLib = {
                                // let go of the thenable reference
                                this._mono_wasm_release_js_handle(thenable_js_handle);
 
-                               // when FinalizationRegistry is not supported by this browser, we will do immediate cleanup after use
+                               // when FinalizationRegistry is not supported by this browser, we will do immediate cleanup after promise resolve/reject
                                if (!this._use_finalization_registry) {
                                        this._release_js_owned_object_by_gc_handle(tcs_gc_handle);
                                }
@@ -255,7 +260,7 @@ var BindingSupportLib = {
                                // let go of the thenable reference
                                this._mono_wasm_release_js_handle(thenable_js_handle);
 
-                               // when FinalizationRegistry is not supported by this browser, we will do immediate cleanup after use
+                               // when FinalizationRegistry is not supported by this browser, we will do immediate cleanup after promise resolve/reject
                                if (!this._use_finalization_registry) {
                                        this._release_js_owned_object_by_gc_handle(tcs_gc_handle);
                                }
@@ -267,9 +272,39 @@ var BindingSupportLib = {
                        }
 
                        // returns raw pointer to tcs.Task
-                       return this._get_tcs_task(tcs_gc_handle);
+                       return {
+                               task_ptr: this._get_tcs_task(tcs_gc_handle),
+                               then_js_handle: thenable_js_handle,
+                       };
+               },
+               _create_cancelable_promise: function (afterResolve, afterReject) {
+                       var promise_control = null;
+                       const promise = new Promise(function (resolve, reject) {
+                               promise_control = {
+                                       isDone: false,
+                                       resolve: (data) => {
+                                               if (!promise_control.isDone) {
+                                                       promise_control.isDone = true;
+                                                       resolve(data);
+                                                       if (afterResolve) {
+                                                               afterResolve();
+                                                       }
+                                               }
+                                       },
+                                       reject: (reason) => {
+                                               if (!promise_control.isDone) {
+                                                       promise_control.isDone = true;
+                                                       reject(reason);
+                                                       if (afterReject) {
+                                                               afterReject();
+                                                       }
+                                               }
+                                       }
+                               };
+                       });
+                       promise[this.promise_control_symbol] = promise_control;
+                       return { promise, promise_control }
                },
-
                _unbox_task_root_as_promise: function (root) {
                        this.bindings_lazy_init ();
                        const self = this;
@@ -287,38 +322,19 @@ var BindingSupportLib = {
 
                        // If the promise for this gc_handle was already collected (or was never created)
                        if (!result) {
+                               const explicitFinalization = self._use_finalization_registry
+                                       ? null
+                                       : () => self._js_owned_object_finalized(gc_handle);
+
+                               const { promise, promise_control } = this._create_cancelable_promise(explicitFinalization, explicitFinalization);
 
-                               var cont_obj = null;
                                // note that we do not implement promise/task roundtrip
                                // With more complexity we could recover original instance when this promise is marshaled back to C#.
-                               var result = new Promise(function (resolve, reject) {
-                                       if (self._use_finalization_registry) {
-                                               cont_obj = {
-                                                       resolve: resolve,
-                                                       reject: reject
-                                               };
-                                       } else {
-                                               // when FinalizationRegistry is not supported by this browser, we will do immediate cleanup after use
-                                               cont_obj = {
-                                                       resolve: function () {
-                                                               const res = resolve.apply(null, arguments);
-                                                               self._js_owned_object_table.delete(gc_handle);
-                                                               self._release_js_owned_object_by_gc_handle(gc_handle);
-                                                               return res;
-                                                       },
-                                                       reject: function () {
-                                                               const res = reject.apply(null, arguments);
-                                                               self._js_owned_object_table.delete(gc_handle);
-                                                               self._release_js_owned_object_by_gc_handle(gc_handle);
-                                                               return res;
-                                                       }
-                                               };
-                                       }
-                               });
+                               result = promise;
 
                                // register C# side of the continuation
-                               this._setup_js_cont (root.value, cont_obj );
-                               
+                               this._setup_js_cont(root.value, promise_control);
+
                                // register for GC of the Task after the JS side is done with the promise
                                if (this._use_finalization_registry) {
                                        this._js_owned_object_registry.register(result, gc_handle);
@@ -437,6 +453,17 @@ var BindingSupportLib = {
                        return result;
                },
 
+               wrap_error: function (is_exception, ex) {
+                       var res = "unknown exception";
+                       if (ex) {
+                               res = ex.toString()
+                       }
+                       if (is_exception) {
+                               setValue(is_exception, 1, "i32");
+                       }
+                       return BINDING.js_string_to_mono_string (res);
+               },
+
                // Ensures the string is already interned on both the managed and JavaScript sides,
                //  then returns the interned string value (to provide fast reference comparisons like C#)
                mono_intern_string: function (string) {
@@ -797,7 +824,9 @@ var BindingSupportLib = {
                                case typeof js_obj === "boolean":
                                        return this._box_js_bool (js_obj);
                                case this.isThenable(js_obj) === true:
-                                       return this._wrap_js_thenable_as_task (js_obj);
+                                       var { task_ptr } = this._wrap_js_thenable_as_task(js_obj);
+                                       // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC
+                                       return task_ptr;
                                case js_obj.constructor.name === "Date":
                                        // getTime() is always UTC
                                        return this._create_date_time(js_obj.getTime());
@@ -1755,6 +1784,246 @@ var BindingSupportLib = {
                        }
                        return obj;
                },
+               Queue: function Queue() {
+                       // amortized time, By Kate Morley http://code.iamkate.com/ under CC0 1.0
+
+                       // initialise the queue and offset
+                       var queue = [];
+                       var offset = 0;
+
+                       // Returns the length of the queue.
+                       this.getLength = function () {
+                               return (queue.length - offset);
+                       }
+
+                       // Returns true if the queue is empty, and false otherwise.
+                       this.isEmpty = function () {
+                               return (queue.length == 0);
+                       }
+
+                       /* Enqueues the specified item. The parameter is:
+                       *
+                       * item - the item to enqueue
+                       */
+                       this.enqueue = function (item) {
+                               queue.push(item);
+                       }
+
+                       /* Dequeues an item and returns it. If the queue is empty, the value
+                       * 'undefined' is returned.
+                       */
+                       this.dequeue = function () {
+
+                               // if the queue is empty, return immediately
+                               if (queue.length == 0) return undefined;
+
+                               // store the item at the front of the queue
+                               var item = queue[offset];
+                               
+                               // for GC's sake
+                               queue[offset] = null;
+
+                               // increment the offset and remove the free space if necessary
+                               if (++offset * 2 >= queue.length) {
+                                       queue = queue.slice(offset);
+                                       offset = 0;
+                               }
+
+                               // return the dequeued item
+                               return item;
+                       }
+
+                       /* Returns the item at the front of the queue (without dequeuing it). If the
+                        * queue is empty then undefined is returned.
+                        */
+                       this.peek = function () {
+                               return (queue.length > 0 ? queue[offset] : undefined);
+                       }
+
+                       this.drain = function (onEach) {
+                               while (this.getLength()) {
+                                       var item = this.dequeue();
+                                       onEach(item);
+                               }
+                       }
+               },
+               _text_encoder_utf8: undefined,
+               _mono_wasm_web_socket_on_message: function (ws, event) {
+                       const event_queue = ws[this.wasm_ws_pending_receive_event_queue];
+                       const promise_queue = ws[this.wasm_ws_pending_receive_promise_queue];
+
+                       if (typeof event.data === 'string') {
+                               if (this._text_encoder_utf8 === undefined) {
+                                       this._text_encoder_utf8 = new TextEncoder();
+                               }
+                               event_queue.enqueue({
+                                       type: 0,// WebSocketMessageType.Text
+                                       // according to the spec https://encoding.spec.whatwg.org/
+                                       // - Unpaired surrogates will get replaced with 0xFFFD
+                                       // - utf8 encode specifically is defined to never throw
+                                       data: this._text_encoder_utf8.encode(event.data),
+                                       offset: 0
+                               })
+                       }
+                       else {
+                               if (event.data.constructor.name !== "ArrayBuffer") {
+                                       throw new Error('ERR19: WebSocket receive expected ArrayBuffer');
+                               }
+                               event_queue.enqueue({
+                                       type: 1,// WebSocketMessageType.Binary
+                                       data: new Uint8Array(event.data),
+                                       offset: 0
+                               })
+                       }
+                       if (promise_queue.getLength() && event_queue.getLength() > 1) {
+                               throw new Error("ERR20: Invalid WS state");// assert
+                       }
+                       while (promise_queue.getLength() && event_queue.getLength()) {
+                               const promise_control = promise_queue.dequeue();
+                               this._mono_wasm_web_socket_receive_buffering(event_queue,
+                                       promise_control.buffer_root, promise_control.buffer_offset, promise_control.buffer_length,
+                                       promise_control.response_root);
+                               promise_control.resolve(null)
+                       }
+                       MONO.prevent_timer_throttling();
+               },
+               _mono_wasm_web_socket_receive_buffering: function (event_queue, buffer_root, buffer_offset, buffer_length, response_root) {
+                       const event = event_queue.peek();
+
+                       const count = Math.min(buffer_length, event.data.length - event.offset);
+                       if (count > 0) {
+                               const targetView = Module.HEAPU8.subarray(buffer_root.value + buffer_offset, buffer_root.value + buffer_offset + buffer_length);
+                               const sourceView = event.data.subarray(event.offset, event.offset + count);
+                               targetView.set(sourceView, 0);
+                               event.offset += count;
+                       }
+                       const end_of_message = event.data.length === event.offset ? 1 : 0;
+                       if (end_of_message) {
+                               event_queue.dequeue();
+                       }
+                       setValue(response_root.value + 0, count, "i32");
+                       setValue(response_root.value + 4, event.type, "i32");
+                       setValue(response_root.value + 8, end_of_message, "i32");
+               },
+
+               _text_decoder_utf8: undefined,
+               _mono_wasm_web_socket_send_buffering: function (ws, buffer_root, buffer_offset, length, message_type, end_of_message) {
+                       var buffer = ws[this.wasm_ws_pending_send_buffer];
+                       var offset = 0;
+                       var message_ptr = buffer_root.value + buffer_offset;
+
+                       if (buffer) {
+                               offset = ws[this.wasm_ws_pending_send_buffer_offset];
+                               // match desktop WebSocket behavior by copying message_type of the first part
+                               message_type = ws[this.wasm_ws_pending_send_buffer_type];
+                               // if not empty message, append to existing buffer
+                               if (length !== 0) {
+                                       const view = Module.HEAPU8.subarray(message_ptr, message_ptr + length);
+                                       if (offset + length > buffer.length) {
+                                               const newbuffer = new Uint8Array((offset + length + 50) * 1.5); // exponential growth
+                                               newbuffer.set(buffer, 0, offset);// copy previous buffer
+                                               newbuffer.set(view, offset);// append copy at the end
+                                               ws[this.wasm_ws_pending_send_buffer] = buffer = newbuffer;
+                                       }
+                                       else {
+                                               buffer.set(view, offset);// append copy at the end
+                                       }
+                                       offset += length;
+                                       ws[this.wasm_ws_pending_send_buffer_offset] = offset;
+                               }
+                       }
+                       else if (!end_of_message) {
+                               // create new buffer
+                               if (length !== 0) {
+                                       const view = Module.HEAPU8.subarray(message_ptr, message_ptr + length);
+                                       buffer = new Uint8Array(view); // copy
+                                       offset = length;
+                                       ws[this.wasm_ws_pending_send_buffer_offset] = offset;
+                                       ws[this.wasm_ws_pending_send_buffer] = buffer;
+                               }
+                               ws[this.wasm_ws_pending_send_buffer_type] = message_type;
+                       }
+                       else {
+                               // use the buffer only localy
+                               if (length !== 0) {
+                                       const memoryView = Module.HEAPU8.subarray(message_ptr, message_ptr + length);
+                                       buffer = memoryView; // send will make a copy
+                                       offset = length;
+                               }
+                       }
+                       // buffer was updated, do we need to trim and convert it to final format ?
+                       if (end_of_message) {
+                               if (offset == 0) {
+                                       return new Uint8Array();
+                               }
+                               if (message_type === 0) {
+                                       // text, convert from UTF-8 bytes to string, because of bad browser API
+                                       if (this._text_decoder_utf8 === undefined) {
+                                               // we do not validate outgoing data https://github.com/dotnet/runtime/issues/59214
+                                               this._text_decoder_utf8 = new TextDecoder('utf-8', { fatal: false });
+                                       }
+
+                                       // See https://github.com/whatwg/encoding/issues/172
+                                       var bytes = typeof SharedArrayBuffer !== 'undefined' && buffer instanceof SharedArrayBuffer
+                                               ? buffer.slice(0, offset)
+                                               : buffer.subarray(0, offset);
+                                       return this._text_decoder_utf8.decode(bytes);
+                               } else {
+                                       // binary, view to used part of the buffer
+                                       return buffer.subarray(0, offset);
+                               }
+                       }
+                       return null;
+               },
+               _mono_wasm_web_socket_send_and_wait: function (ws, buffer, thenable_js_handle) {
+                       // send and return promise
+                       ws.send(buffer);
+                       ws[this.wasm_ws_pending_send_buffer] = null;
+
+                       // if the remaining send buffer is small, we don't block so that the throughput doesn't suffer. 
+                       // Otherwise we block so that we apply some backpresure to the application sending large data.
+                       // this is different from Managed implementation
+                       if (ws.bufferedAmount < BINDING.ws_send_buffer_blocking_threshold) {
+                               return null; // no promise
+                       }
+
+                       // block the promise/task until the browser passed the buffer to OS
+                       const { promise, promise_control } = BINDING._create_cancelable_promise();
+                       const pending = ws[BINDING.wasm_ws_pending_send_promises];
+                       pending.push(promise_control);
+
+                       var nextDelay = 1;
+                       const polling_check = () => {
+                               // was it all sent yet ?
+                               if (ws.bufferedAmount === 0) {
+                                       promise_control.resolve(null);
+                               }
+                               else if (ws.readyState != WebSocket.OPEN) {
+                                       // only reject if the data were not sent
+                                       // bufferedAmount does not reset to zero once the connection closes
+                                       promise_control.reject("InvalidState: The WebSocket is not connected.");
+                               } 
+                               else if(!promise_control.isDone) {
+                                       globalThis.setTimeout(polling_check, nextDelay);
+                                       // exponentially longer delays, up to 1000ms
+                                       nextDelay = Math.min(nextDelay * 1.5, 1000);
+                                       return;
+                               }
+                               // remove from pending
+                               const index = pending.indexOf(promise_control);
+                               if (index > -1) {
+                                       pending.splice(index, 1);
+                               }
+                       };
+
+                       globalThis.setTimeout(polling_check, 0);
+
+                       const { task_ptr, then_js_handle } = BINDING._wrap_js_thenable_as_task(promise);
+                       // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC
+                       setValue(thenable_js_handle, then_js_handle, "i32");
+
+                       return task_ptr;
+               },
        },
        mono_wasm_invoke_js_with_args: function(js_handle, method_name, args, is_exception) {
                let argsRoot = MONO.mono_wasm_new_root (args), nameRoot = MONO.mono_wasm_new_root (method_name);
@@ -1763,14 +2032,12 @@ var BindingSupportLib = {
 
                        var js_name = BINDING.conv_string (nameRoot.value);
                        if (!js_name || (typeof(js_name) !== "string")) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("ERR12: Invalid method name object '" + nameRoot.value + "'");
+                               return BINDING.wrap_error(is_exception, "ERR12: Invalid method name object '" + nameRoot.value + "'");
                        }
 
                        var obj = BINDING.get_js_obj (js_handle);
                        if (!obj) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("ERR13: Invalid JS object handle '" + js_handle + "' while invoking '"+js_name+"'");
+                               return BINDING.wrap_error(is_exception, "ERR13: Invalid JS object handle '" + js_handle + "' while invoking '"+js_name+"'");
                        }
 
                        var js_args = BINDING._mono_array_root_to_js_array(argsRoot);
@@ -1782,12 +2049,8 @@ var BindingSupportLib = {
                                        throw new Error("Method: '" + js_name + "' not found for: '" + Object.prototype.toString.call(obj) + "'");
                                var res = m.apply (obj, js_args);
                                return BINDING._js_to_mono_obj(true, res);
-                       } catch (e) {
-                               var res = e.toString ();
-                               setValue (is_exception, 1, "i32");
-                               if (res === null || res === undefined)
-                                       res = "unknown exception";
-                               return BINDING.js_string_to_mono_string (res);
+                       } catch (ex) {
+                               return BINDING.wrap_error(is_exception, ex);
                        }
                } finally {
                        argsRoot.release();
@@ -1801,27 +2064,20 @@ var BindingSupportLib = {
                try {
                        var js_name = BINDING.conv_string (nameRoot.value);
                        if (!js_name) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("Invalid property name object '" + nameRoot.value + "'");
+                               return BINDING.wrap_error(is_exception, "Invalid property name object '" + nameRoot.value + "'");
                        }
 
                        var obj = BINDING.mono_wasm_get_jsobj_from_js_handle (js_handle);
                        if (!obj) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("ERR01: Invalid JS object handle '" + js_handle + "' while geting '"+js_name+"'");
+                               return BINDING.wrap_error(is_exception, "ERR01: Invalid JS object handle '" + js_handle + "' while geting '"+js_name+"'");
                        }
 
-                       var res;
                        try {
                                var m = obj [js_name];
 
                                return BINDING._js_to_mono_obj (true, m);
-                       } catch (e) {
-                               var res = e.toString ();
-                               setValue (is_exception, 1, "i32");
-                               if (res === null || typeof res === "undefined")
-                                       res = "unknown exception";
-                               return BINDING.js_string_to_mono_string (res);
+                       } catch (ex) {
+                               return BINDING.wrap_error(is_exception, ex);
                        }
                } finally {
                        nameRoot.release();
@@ -1833,14 +2089,12 @@ var BindingSupportLib = {
                        BINDING.bindings_lazy_init ();
                        var property = BINDING.conv_string (nameRoot.value);
                        if (!property) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("Invalid property name object '" + property_name + "'");
+                               return BINDING.wrap_error(is_exception, "Invalid property name object '" + property_name + "'");
                        }
 
                        var js_obj = BINDING.mono_wasm_get_jsobj_from_js_handle (js_handle);
                        if (!js_obj) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("ERR02: Invalid JS object handle '" + js_handle + "' while setting '"+property+"'");
+                               return BINDING.wrap_error(is_exception, "ERR02: Invalid JS object handle '" + js_handle + "' while setting '"+property+"'");
                        }
 
                        var result = false;
@@ -1880,19 +2134,14 @@ var BindingSupportLib = {
 
                var obj = BINDING.mono_wasm_get_jsobj_from_js_handle (js_handle);
                if (!obj) {
-                       setValue (is_exception, 1, "i32");
-                       return BINDING.js_string_to_mono_string ("ERR03: Invalid JS object handle '" + js_handle + "' while getting ["+property_index+"]");
+                       return BINDING.wrap_error(is_exception, "ERR03: Invalid JS object handle '" + js_handle + "' while getting ["+property_index+"]");
                }
 
                try {
                        var m = obj [property_index];
                        return BINDING._js_to_mono_obj (true, m);
-               } catch (e) {
-                       var res = e.toString ();
-                       setValue (is_exception, 1, "i32");
-                       if (res === null || typeof res === "undefined")
-                               res = "unknown exception";
-                       return BINDING.js_string_to_mono_string (res);
+               } catch (ex) {
+                       return BINDING.wrap_error(is_exception, ex);
                }
        },
        mono_wasm_set_by_index: function(js_handle, property_index, value, is_exception) {
@@ -1902,8 +2151,7 @@ var BindingSupportLib = {
 
                        var obj = BINDING.mono_wasm_get_jsobj_from_js_handle (js_handle);
                        if (!obj) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("ERR04: Invalid JS object handle '" + js_handle + "' while setting ["+property_index+"]");
+                               return BINDING.wrap_error(is_exception, "ERR04: Invalid JS object handle '" + js_handle + "' while setting ["+property_index+"]");
                        }
 
                        var js_value = BINDING._unbox_mono_obj_root(valueRoot);
@@ -1911,12 +2159,8 @@ var BindingSupportLib = {
                        try {
                                obj [property_index] = js_value;
                                return true;
-                       } catch (e) {
-                               var res = e.toString ();
-                               setValue (is_exception, 1, "i32");
-                               if (res === null || typeof res === "undefined")
-                                       res = "unknown exception";
-                               return BINDING.js_string_to_mono_string (res);
+                       } catch (ex) {
+                               return BINDING.wrap_error(is_exception, ex);
                        }
                } finally {
                        valueRoot.release();
@@ -1940,8 +2184,7 @@ var BindingSupportLib = {
 
                        // TODO returning null may be useful when probing for browser features
                        if (globalObj === null || typeof globalObj === undefined) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("Global object '" + js_name + "' not found.");
+                               return BINDING.wrap_error(is_exception, "Global object '" + js_name + "' not found.");
                        }
 
                        return BINDING._js_to_mono_obj (true, globalObj);
@@ -1961,15 +2204,13 @@ var BindingSupportLib = {
                        var js_name = BINDING.conv_string (nameRoot.value);
 
                        if (!js_name) {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("Invalid name @" + nameRoot.value);
+                               return BINDING.wrap_error(is_exception, "Invalid name @" + nameRoot.value);
                        }
 
                        var coreObj = globalThis[js_name];
 
                        if (coreObj === null || typeof coreObj === "undefined") {
-                               setValue (is_exception, 1, "i32");
-                               return BINDING.js_string_to_mono_string ("JavaScript host object '" + js_name + "' not found.");
+                               return BINDING.wrap_error(is_exception, "JavaScript host object '" + js_name + "' not found.");
                        }
 
                        var js_args = BINDING._mono_array_root_to_js_array(argsRoot);
@@ -1993,12 +2234,8 @@ var BindingSupportLib = {
                                // returns boxed js_handle int, because on exception we need to return String on same method signature
                                // here we don't have anything to in-flight reference, as the JSObject doesn't exist yet
                                return BINDING._js_to_mono_obj(false, js_handle);
-                       } catch (e) {
-                               var res = e.toString ();
-                               setValue (is_exception, 1, "i32");
-                               if (res === null || res === undefined)
-                                       res = "Error allocating object.";
-                               return BINDING.js_string_to_mono_string (res);
+                       } catch (ex) {
+                               return BINDING.wrap_error(is_exception, ex);
                        }
                } finally {
                        argsRoot.release();
@@ -2010,8 +2247,7 @@ var BindingSupportLib = {
 
                var js_obj = BINDING.mono_wasm_get_jsobj_from_js_handle (js_handle);
                if (!js_obj) {
-                       setValue (is_exception, 1, "i32");
-                       return BINDING.js_string_to_mono_string ("ERR06: Invalid JS object handle '" + js_handle + "'");
+                       return BINDING.wrap_error(is_exception, "ERR06: Invalid JS object handle '" + js_handle + "'");
                }
 
                // returns pointer to C# array
@@ -2022,8 +2258,7 @@ var BindingSupportLib = {
 
                var js_obj = BINDING.mono_wasm_get_jsobj_from_js_handle (js_handle);
                if (!js_obj) {
-                       setValue (is_exception, 1, "i32");
-                       return BINDING.js_string_to_mono_string ("ERR07: Invalid JS object handle '" + js_handle + "'");
+                       return BINDING.wrap_error(is_exception, "ERR07: Invalid JS object handle '" + js_handle + "'");
                }
 
                var res = BINDING.typedarray_copy_to(js_obj, pinned_array, begin, end, bytes_per_element);
@@ -2041,8 +2276,7 @@ var BindingSupportLib = {
 
                var js_obj = BINDING.mono_wasm_get_jsobj_from_js_handle (js_handle);
                if (!js_obj) {
-                       setValue (is_exception, 1, "i32");
-                       return BINDING.js_string_to_mono_string ("ERR08: Invalid JS object handle '" + js_handle + "'");
+                       return BINDING.wrap_error(is_exception, "ERR08: Invalid JS object handle '" + js_handle + "'");
                }
 
                var res = BINDING.typedarray_copy_from(js_obj, pinned_array, begin, end, bytes_per_element);
@@ -2059,9 +2293,9 @@ var BindingSupportLib = {
                        if (!obj)
                                throw new Error("ERR09: Invalid JS object handle for '"+sName+"'");
 
-                       const prevent_timer_throttling = !BINDING.isChromium || obj.constructor.name !== 'WebSocket'
+                       const prevent_timer_throttling = !MONO.isChromium || obj.constructor.name !== 'WebSocket'
                                ? null
-                               : () => MONO.prevent_timer_throttling(0);
+                               : MONO.prevent_timer_throttling;
 
                        var listener = BINDING._wrap_delegate_gc_handle_as_function(listener_gc_handle, prevent_timer_throttling);
                        if (!listener)
@@ -2081,8 +2315,8 @@ var BindingSupportLib = {
                        else
                                obj.addEventListener(sName, listener);
                        return 0;
-               } catch (exc) {
-                       return BINDING.js_string_to_mono_string(exc.message);
+               } catch (ex) {
+                       return BINDING.wrap_error(null, ex);
                } finally {
                        nameRoot.release();
                }
@@ -2110,19 +2344,255 @@ var BindingSupportLib = {
                        if (!BINDING._use_finalization_registry) {
                                listener[BINDING.listener_registration_count_symbol]--;
                                if (listener[BINDING.listener_registration_count_symbol] === 0) {
-                                       BINDING._js_owned_object_table.delete(listener_gc_handle);
-                                       BINDING._release_js_owned_object_by_gc_handle(listener_gc_handle);
+                                       BINDING._js_owned_object_finalized(listener_gc_handle);
                                }
                        }
 
                        return 0;
-               } catch (exc) {
-                       return BINDING.js_string_to_mono_string(exc.message);
+               } catch (ex) {
+                       return BINDING.wrap_error(null, ex);
                } finally {
                        nameRoot.release();
                }
        },
+       mono_wasm_cancel_promise: function (thenable_js_handle, is_exception) {
+               try {
+                       const promise = BINDING.mono_wasm_get_jsobj_from_js_handle(thenable_js_handle)
+                       const promise_control = promise[BINDING.promise_control_symbol];
+                       promise_control.reject("OperationCanceledException");
+               }
+               catch (ex) {
+                       return BINDING.wrap_error(is_exception, ex);
+               }
+       },
+       mono_wasm_web_socket_open: function (uri, subProtocols, on_close, web_socket_js_handle, thenable_js_handle, is_exception) {
+               const uri_root = MONO.mono_wasm_new_root(uri);
+               const sub_root = MONO.mono_wasm_new_root(subProtocols);
+               const on_close_root = MONO.mono_wasm_new_root(on_close);
+               try {
+                       const js_uri = BINDING.conv_string(uri_root.value);
+                       if (!js_uri) {
+                               return BINDING.wrap_error(is_exception, "ERR12: Invalid uri '" + uri_root.value + "'");
+                       }
+
+                       const js_subs = BINDING._mono_array_root_to_js_array(sub_root);
+
+                       const js_on_close = BINDING._wrap_delegate_root_as_function(on_close_root);
+
+                       const ws = new globalThis.WebSocket(js_uri, js_subs);
+                       var { promise, promise_control: open_promise_control } = BINDING._create_cancelable_promise();
+
+                       ws[BINDING.wasm_ws_pending_receive_event_queue] = new BINDING.Queue();
+                       ws[BINDING.wasm_ws_pending_receive_promise_queue] = new BINDING.Queue();
+                       ws[BINDING.wasm_ws_pending_open_promise] = open_promise_control;
+                       ws[BINDING.wasm_ws_pending_send_promises] = [];
+                       ws[BINDING.wasm_ws_pending_close_promises] = [];
+                       ws.binaryType = "arraybuffer";
+                       const local_on_open = (ev) => {
+                               if (ws[BINDING.wasm_ws_is_aborted]) return;
+                               open_promise_control.resolve(null);
+                               MONO.prevent_timer_throttling();
+                       }
+                       const local_on_message = (ev) => {
+                               if (ws[BINDING.wasm_ws_is_aborted]) return;
+                               BINDING._mono_wasm_web_socket_on_message(ws, ev);
+                               MONO.prevent_timer_throttling();
+                       }
+                       const local_on_close = (ev) => {
+                               ws.removeEventListener('message', local_on_message);
+                               if (ws[BINDING.wasm_ws_is_aborted]) return;
+                               js_on_close(ev.code, ev.reason);
+
+                               // this reject would not do anything if there was already "open" before it.
+                               open_promise_control.reject(ev.reason);
+
+                               for (var close_promise_control of ws[BINDING.wasm_ws_pending_close_promises]) {
+                                       close_promise_control.resolve();
+                               }
+
+                               // send close to any pending receivers, to wake them
+                               ws[BINDING.wasm_ws_pending_receive_promise_queue].drain(receive_promise_control => {
+                                       const response_root = receive_promise_control.response_root;
+                                       setValue(response_root.value + 0, 0, "i32");// count
+                                       setValue(response_root.value + 4, 2, "i32");// type:close
+                                       setValue(response_root.value + 8, 1, "i32");// end_of_message: true
+                                       receive_promise_control.resolve(null);
+                               });
+                       }
+                       ws.addEventListener('message', local_on_message);
+                       ws.addEventListener('open', local_on_open, { once: true });
+                       ws.addEventListener('close', local_on_close, { once: true });
+
+                       var ws_js_handle = BINDING.mono_wasm_get_js_handle(ws);
+                       setValue(web_socket_js_handle, ws_js_handle, "i32");
+
+                       var { task_ptr, then_js_handle } = BINDING._wrap_js_thenable_as_task(promise);
+                       // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC
+                       setValue(thenable_js_handle, then_js_handle, "i32");
+
+                       return task_ptr;
+               }
+               catch (ex) {
+                       return BINDING.wrap_error(is_exception, ex);
+               }
+               finally {
+                       uri_root.release();
+                       sub_root.release();
+                       on_close_root.release();
+               }
+       },
+       mono_wasm_web_socket_send: function (webSocket_js_handle, buffer_ptr, offset, length, message_type, end_of_message, thenable_js_handle, is_exception) {
+               const buffer_root = MONO.mono_wasm_new_root(buffer_ptr);
+               try {
+                       const ws = BINDING.mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle)
+                       if (!ws)
+                               throw new Error("ERR17: Invalid JS object handle " + webSocket_js_handle);
+
+                       if (ws.readyState != WebSocket.OPEN) {
+                               throw new Error("InvalidState: The WebSocket is not connected.");
+                       }
+
+                       const whole_buffer = BINDING._mono_wasm_web_socket_send_buffering(ws, buffer_root, offset, length, message_type, end_of_message);
+
+                       if (!end_of_message) {
+                               return null; // we are done buffering synchronously, no promise
+                       }
+                       return BINDING._mono_wasm_web_socket_send_and_wait(ws, whole_buffer, thenable_js_handle);
+               }
+               catch (ex) {
+                       return BINDING.wrap_error(is_exception, ex);
+               }
+               finally {
+                       buffer_root.release();
+               }
+       },
+       mono_wasm_web_socket_receive: function (webSocket_js_handle, buffer_ptr, offset, length, response_ptr, thenable_js_handle, is_exception) {
+               const buffer_root = MONO.mono_wasm_new_root(buffer_ptr);
+               const response_root = MONO.mono_wasm_new_root(response_ptr);
+               const release_buffer = () => {
+                       buffer_root.release();
+                       response_root.release();
+               }
+
+               try {
+                       const ws = BINDING.mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle)
+                       if (!ws)
+                               throw new Error("ERR18: Invalid JS object handle " + webSocket_js_handle);
+                       const event_queue = ws[BINDING.wasm_ws_pending_receive_event_queue];
+                       const promise_queue = ws[BINDING.wasm_ws_pending_receive_promise_queue];
+
+                       const readyState = ws.readyState;
+                       if (readyState != WebSocket.OPEN && readyState != WebSocket.CLOSING) {
+                               throw new Error("InvalidState: The WebSocket is not connected.");
+                       }
+
+                       if (event_queue.getLength()) {
+                               if (promise_queue.getLength() != 0) {
+                                       throw new Error("ERR20: Invalid WS state");// assert
+                               }
+                               // finish synchronously
+                               BINDING._mono_wasm_web_socket_receive_buffering(event_queue, buffer_root, offset, length, response_root);
+                               release_buffer();
+
+                               setValue(thenable_js_handle, 0, "i32");
+                               return null;
+                       }
+
+                       const { promise, promise_control } = BINDING._create_cancelable_promise(release_buffer, release_buffer);
+                       promise_control.buffer_root = buffer_root;
+                       promise_control.buffer_offset = offset;
+                       promise_control.buffer_length = length;
+                       promise_control.response_root = response_root;
+                       promise_queue.enqueue(promise_control);
+
+                       const { task_ptr, then_js_handle } = BINDING._wrap_js_thenable_as_task(promise);
+                       // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC
+                       setValue(thenable_js_handle, then_js_handle, "i32");
+                       return task_ptr;
+               }
+               catch (ex) {
+                       return BINDING.wrap_error(is_exception, ex);
+               }
+       },
+       mono_wasm_web_socket_close: function (webSocket_js_handle, code, reason, wait_for_close_received, thenable_js_handle, is_exception) {
+               const reason_root = MONO.mono_wasm_new_root(reason);
+               try {
+                       const ws = BINDING.mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle)
+                       if (!ws)
+                               throw new Error("ERR19: Invalid JS object handle " + webSocket_js_handle);
+
+                       if (ws.readyState == WebSocket.CLOSED) {
+                               return null;// no promise
+                       }
+
+                       const js_reason = BINDING.conv_string(reason_root.value);
+
+                       if (wait_for_close_received) {
+                               const { promise, promise_control } = BINDING._create_cancelable_promise();
+                               ws[BINDING.wasm_ws_pending_close_promises].push(promise_control);
 
+                               if (js_reason) {
+                                       ws.close(code, js_reason);
+                               } else {
+                                       ws.close(code);
+                               }
+
+                               var { task_ptr, then_js_handle } = BINDING._wrap_js_thenable_as_task(promise);
+                               // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC
+                               setValue(thenable_js_handle, then_js_handle, "i32");
+
+                               return task_ptr;
+                       }
+                       else {
+                               if (!BINDING.mono_wasm_web_socket_close_warning) {
+                                       BINDING.mono_wasm_web_socket_close_warning = true;
+                                       console.warn('WARNING: Web browsers do not support closing the output side of a WebSocket. CloseOutputAsync has closed the socket and discarded any incoming messages.');
+                               }
+                               if (js_reason) {
+                                       ws.close(code, js_reason);
+                               } else {
+                                       ws.close(code);
+                               }
+                               setValue(thenable_js_handle, 0, "i32");
+                               return null;// no promise
+                       }
+               }
+               catch (ex) {
+                       return BINDING.wrap_error(is_exception, ex);
+               }
+               finally {
+                       reason_root.release();
+               }
+       },
+       mono_wasm_web_socket_abort: function (webSocket_js_handle, is_exception) {
+               try {
+                       const ws = BINDING.mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle)
+                       if (!ws)
+                               throw new Error("ERR18: Invalid JS object handle " + webSocket_js_handle);
+                       
+                       ws[BINDING.wasm_ws_is_aborted] = true;
+                       const open_promise_control = ws[BINDING.wasm_ws_pending_open_promise];
+                       if (open_promise_control) {
+                               open_promise_control.reject("OperationCanceledException");
+                       }
+                       for(var close_promise_control of ws[BINDING.wasm_ws_pending_close_promises]){
+                               close_promise_control.reject("OperationCanceledException");
+                       }
+                       for (var send_promise_control of ws[BINDING.wasm_ws_pending_send_promises]) {
+                               send_promise_control.reject("OperationCanceledException")
+                       }
+
+                       ws[BINDING.wasm_ws_pending_receive_promise_queue].drain(receive_promise_control => {
+                               receive_promise_control.reject("OperationCanceledException");
+                       });
+
+                       // this is different from Managed implementation
+                       ws.close(1000, 'Connection was aborted.');
+               }
+               catch (ex) {
+                       return BINDING.wrap_error(is_exception, ex);
+               }
+       },
 };
 
 autoAddDeps(BindingSupportLib, '$BINDING')
index ae096d8..2a05a72 100644 (file)
@@ -24,6 +24,12 @@ extern MonoObject* mono_wasm_typed_array_from (int ptr, int begin, int end, int
 extern MonoObject* mono_wasm_typed_array_copy_from (int js_handle, int ptr, int begin, int end, int bytes_per_element, int *is_exception);
 extern MonoString* mono_wasm_add_event_listener (int jsObjHandle, MonoString *name, int weakDelegateHandle, int optionsObjHandle);
 extern MonoString* mono_wasm_remove_event_listener (int jsObjHandle, MonoString *name, int weakDelegateHandle, int capture);
+extern MonoString* mono_wasm_cancel_promise (int thenable_js_handle, int *is_exception);
+extern MonoObject* mono_wasm_web_socket_open (MonoString *uri, MonoArray *subProtocols, MonoDelegate *on_close, int *web_socket_js_handle, int *thenable_js_handle, int *is_exception);
+extern MonoObject* mono_wasm_web_socket_send (int webSocket_js_handle, void* buffer_ptr, int offset, int length, int message_type, int end_of_message, int *thenable_js_handle, int *is_exception);
+extern MonoObject* mono_wasm_web_socket_receive (int webSocket_js_handle, void* buffer_ptr, int offset, int length, void* response_ptr, int *thenable_js_handle, int *is_exception);
+extern MonoObject* mono_wasm_web_socket_close (int webSocket_js_handle, int code, MonoString * reason, int wait_for_close_received, int *thenable_js_handle, int *is_exception);
+extern MonoString* mono_wasm_web_socket_abort (int webSocket_js_handle, int *is_exception);
 
 // Compiles a JavaScript function from the function data passed.
 // Note: code snippet is not a function definition. Instead it must create and return a function instance.
@@ -83,6 +89,12 @@ void core_initialize_internals ()
        mono_add_internal_call ("Interop/Runtime::CompileFunction", mono_wasm_compile_function);
        mono_add_internal_call ("Interop/Runtime::AddEventListener", mono_wasm_add_event_listener);
        mono_add_internal_call ("Interop/Runtime::RemoveEventListener", mono_wasm_remove_event_listener);
+       mono_add_internal_call ("Interop/Runtime::WebSocketOpen", mono_wasm_web_socket_open);
+       mono_add_internal_call ("Interop/Runtime::WebSocketSend", mono_wasm_web_socket_send);
+       mono_add_internal_call ("Interop/Runtime::WebSocketReceive", mono_wasm_web_socket_receive);
+       mono_add_internal_call ("Interop/Runtime::WebSocketClose", mono_wasm_web_socket_close);
+       mono_add_internal_call ("Interop/Runtime::WebSocketAbort", mono_wasm_web_socket_abort);
+       mono_add_internal_call ("Interop/Runtime::CancelPromise", mono_wasm_cancel_promise);
 }
 
 // Int8Array           | int8_t        | byte or SByte (signed byte)
index 5364d44..668c4b5 100644 (file)
@@ -55,7 +55,8 @@ var MonoSupportLib = {
                active_frames: [],
                pump_count: 0,
                timeout_queue: [],
-               spread_timers_maximum:0,
+               spread_timers_maximum: 0,
+               isChromium: false,
                _vt_stack: [],
                mono_wasm_runtime_is_ready : false,
                mono_wasm_ignore_pdb_load_errors: true,
@@ -102,6 +103,16 @@ var MonoSupportLib = {
                        module ["mono_wasm_new_roots"] = MONO.mono_wasm_new_roots.bind(MONO);
                        module ["mono_wasm_release_roots"] = MONO.mono_wasm_release_roots.bind(MONO);
                        module ["mono_wasm_load_config"] = MONO.mono_wasm_load_config.bind(MONO);
+                       
+                       if (globalThis.navigator) {
+                               const nav = globalThis.navigator;
+                               if (nav.userAgentData && nav.userAgentData.brands) {
+                                       MONO.isChromium = nav.userAgentData.brands.some((i) => i.brand == 'Chromium');
+                               }
+                               else if (nav.userAgent) {
+                                       MONO.isChromium = nav.userAgent.includes("Chrome");
+                               }
+                       }
                },
 
                _base64Converter: {
@@ -548,14 +559,15 @@ var MonoSupportLib = {
                                return result;
                        },
                        decode: function (start, end, save) {
-                               if (!MONO.mono_text_decoder) {
-                                       MONO.mono_text_decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-16le') : undefined;
+                               if (MONO.mono_text_decoder === undefined) {
+                                       MONO.mono_text_decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-16le') : null;
                                }
 
                                var str = "";
                                if (MONO.mono_text_decoder) {
                                        // When threading is enabled, TextDecoder does not accept a view of a
                                        // SharedArrayBuffer, we must make a copy of the array first.
+                                       // See https://github.com/whatwg/encoding/issues/172
                                        var subArray = typeof SharedArrayBuffer !== 'undefined' && Module.HEAPU8.buffer instanceof SharedArrayBuffer
                                                ? Module.HEAPU8.slice(start, end)
                                                : Module.HEAPU8.subarray(start, end);
@@ -1471,6 +1483,10 @@ var MonoSupportLib = {
                        this.mono_set_timeout_exec (id);
                },
                prevent_timer_throttling: function () {
+                       if (!MONO.isChromium) {
+                               return;
+                       }
+
                        // this will schedule timers every second for next 6 minutes, it should be called from WebSocket event, to make it work
                        // on next call, it would only extend the timers to cover yet uncovered future
                        let now = new Date().valueOf();