Initial commit
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RudderSdk.Unity
|
||||
{
|
||||
/// <summary>
|
||||
/// WebGL WebSocket adapter using JavaScript interop via DllImport("__Internal__").
|
||||
/// Uses SendMessage callbacks from JS to route WebSocket events to Unity's main thread.
|
||||
/// Only functional in WebGL builds; throws PlatformNotSupportedException elsewhere.
|
||||
/// </summary>
|
||||
public sealed class JsWebSocketAdapter : IWebSocketAdapter, IDisposable
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
[DllImport("__Internal__")]
|
||||
private static extern void RudderWebSocketCreate(string gameObjectName, string url);
|
||||
|
||||
[DllImport("__Internal__")]
|
||||
private static extern void RudderWebSocketSend(string gameObjectName, byte[] data, int length);
|
||||
|
||||
[DllImport("__Internal__")]
|
||||
private static extern void RudderWebSocketClose(string gameObjectName);
|
||||
#endif
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private readonly ConcurrentQueue<Action> _eventQueue = new();
|
||||
private JsWebSocketBridge _bridge;
|
||||
private TaskCompletionSource<bool> _connectionTcs;
|
||||
private volatile bool _isConnected;
|
||||
private volatile bool _isConnecting;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the WebSocket connection is closed.
|
||||
/// </summary>
|
||||
public event Action Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when binary data is received from the server.
|
||||
/// </summary>
|
||||
public event Action<ArraySegment<byte>> Received;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when an error occurs on the WebSocket.
|
||||
/// </summary>
|
||||
public event Action<Exception> ReceivedError;
|
||||
|
||||
/// <summary>
|
||||
/// True when the WebSocket connection is open.
|
||||
/// </summary>
|
||||
public bool IsConnected => _isConnected;
|
||||
|
||||
/// <summary>
|
||||
/// True while the WebSocket connection is being established.
|
||||
/// </summary>
|
||||
public bool IsConnecting => _isConnecting;
|
||||
|
||||
public JsWebSocketAdapter()
|
||||
{
|
||||
var go = new GameObject("JsWebSocketBridge");
|
||||
UnityEngine.Object.DontDestroyOnLoad(go);
|
||||
_bridge = go.AddComponent<JsWebSocketBridge>();
|
||||
_bridge.Init(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the specified WebSocket URI via the JavaScript WebSocket API.
|
||||
/// </summary>
|
||||
/// <param name="uri">The WebSocket server URI (ws:// or wss://).</param>
|
||||
/// <param name="timeoutSeconds">Connection timeout in seconds.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when uri is null.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when timeoutSeconds is not positive.</exception>
|
||||
/// <exception cref="TimeoutException">Thrown when the connection attempt times out.</exception>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when not running in WebGL.</exception>
|
||||
public Task ConnectAsync(Uri uri, int timeoutSeconds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (uri == null)
|
||||
throw new ArgumentNullException(nameof(uri));
|
||||
if (timeoutSeconds <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(timeoutSeconds), "Timeout must be greater than zero.");
|
||||
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
_isConnecting = true;
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_connectionTcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// Register cancellation (including timeout)
|
||||
_cts.Token.Register(() =>
|
||||
{
|
||||
if (_connectionTcs?.TrySetException(
|
||||
new TimeoutException($"WebSocket connection timed out after {timeoutSeconds} seconds.")) == true)
|
||||
{
|
||||
_isConnecting = false;
|
||||
}
|
||||
});
|
||||
_cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||
|
||||
RudderWebSocketCreate(_bridge.gameObject.name, uri.ToString());
|
||||
|
||||
return _connectionTcs.Task;
|
||||
#else
|
||||
// Not available outside WebGL builds; editor test uses NativeWebSocketAdapter.
|
||||
return Task.FromException(new PlatformNotSupportedException(
|
||||
"JsWebSocketAdapter is only supported on WebGL. Use NativeWebSocketAdapter for other platforms."));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gracefully closes the WebSocket connection via the JavaScript WebSocket API.
|
||||
/// </summary>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
RudderWebSocketClose(_bridge.gameObject.name);
|
||||
#endif
|
||||
_cts?.Cancel();
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
EnqueueEvent(() => Closed?.Invoke());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends binary data over the WebSocket via the JavaScript WebSocket API.
|
||||
/// </summary>
|
||||
public Task SendAsync(ArraySegment<byte> data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_isConnected) return Task.CompletedTask;
|
||||
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
byte[] arr;
|
||||
int offset;
|
||||
int count;
|
||||
if (data.Offset == 0 && data.Count == data.Array.Length)
|
||||
{
|
||||
arr = data.Array;
|
||||
offset = 0;
|
||||
count = data.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Copy to a contiguous buffer since DllImport needs a pinned array from offset 0
|
||||
arr = new byte[data.Count];
|
||||
System.Buffer.BlockCopy(data.Array, data.Offset, arr, 0, data.Count);
|
||||
offset = 0;
|
||||
count = data.Count;
|
||||
}
|
||||
|
||||
RudderWebSocketSend(_bridge.gameObject.name, arr, count);
|
||||
#endif
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the adapter, cleaning up the bridge GameObject.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
RudderWebSocketClose(_bridge.gameObject.name);
|
||||
#endif
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
|
||||
if (_bridge != null && _bridge.gameObject != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(_bridge.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by JsWebSocketBridge when the JS WebSocket onopen fires.
|
||||
/// </summary>
|
||||
internal void HandleOpen()
|
||||
{
|
||||
_isConnected = true;
|
||||
_isConnecting = false;
|
||||
_connectionTcs?.TrySetResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by JsWebSocketBridge when the JS WebSocket onclose fires.
|
||||
/// </summary>
|
||||
internal void HandleClose()
|
||||
{
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_connectionTcs?.TrySetCanceled();
|
||||
Closed?.Invoke();
|
||||
}
|
||||
|
||||
/// Called by JsWebSocketBridge when the JS WebSocket onerror fires.
|
||||
/// </summary>
|
||||
internal void HandleError(string errorMessage)
|
||||
{
|
||||
_isConnecting = false;
|
||||
_connectionTcs?.TrySetException(new Exception(errorMessage));
|
||||
ReceivedError?.Invoke(new Exception(errorMessage));
|
||||
}
|
||||
|
||||
/// Called by JsWebSocketBridge when binary message data is received.
|
||||
/// </summary>
|
||||
internal void HandleMessage(ArraySegment<byte> data)
|
||||
{
|
||||
Received?.Invoke(data);
|
||||
}
|
||||
|
||||
internal void HandleMessageError(Exception ex)
|
||||
{
|
||||
ReceivedError?.Invoke(ex);
|
||||
}
|
||||
|
||||
internal void EnqueueEvent(Action action)
|
||||
{
|
||||
_eventQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
internal void ProcessEvents()
|
||||
{
|
||||
while (_eventQueue.TryDequeue(out var action))
|
||||
{
|
||||
try
|
||||
{
|
||||
action?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour that receives SendMessage callbacks from the JavaScript WebSocket bridge
|
||||
/// and dispatches events on the Unity main thread.
|
||||
/// </summary>
|
||||
internal class JsWebSocketBridge : MonoBehaviour
|
||||
{
|
||||
private JsWebSocketAdapter _adapter;
|
||||
|
||||
public void Init(JsWebSocketAdapter adapter)
|
||||
{
|
||||
_adapter = adapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from JavaScript via SendMessage when the WebSocket connection opens.
|
||||
/// </summary>
|
||||
public void OnOpen(string _)
|
||||
{
|
||||
_adapter?.EnqueueEvent(() => _adapter?.HandleOpen());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from JavaScript via SendMessage when binary data is received.
|
||||
/// Data is base64-encoded by the JS bridge and decoded here.
|
||||
/// </summary>
|
||||
public void OnMessage(string base64Data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(base64Data) || _adapter == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var data = Convert.FromBase64String(base64Data);
|
||||
var segment = new ArraySegment<byte>(data);
|
||||
_adapter.EnqueueEvent(() => _adapter.HandleMessage(segment));
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
Debug.LogError($"JsWebSocketBridge: Failed to decode base64 message: {ex.Message}");
|
||||
_adapter.EnqueueEvent(() => _adapter.HandleMessageError(ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from JavaScript via SendMessage when the WebSocket connection closes.
|
||||
/// </summary>
|
||||
public void OnClose(string _)
|
||||
{
|
||||
_adapter?.EnqueueEvent(() => _adapter?.HandleClose());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from JavaScript via SendMessage when a WebSocket error occurs.
|
||||
/// </summary>
|
||||
public void OnError(string error)
|
||||
{
|
||||
_adapter?.EnqueueEvent(() => _adapter?.HandleError(error ?? "Unknown WebSocket error"));
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_adapter?.ProcessEvents();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_adapter?.Dispose();
|
||||
_adapter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user