supabase-community / realtime-csharp

A C# client library for supabase/realtime.
https://supabase-community.github.io/realtime-csharp/api/Supabase.Realtime.Client.html
MIT License
70 stars 12 forks source link

Make Realtime Network Status Aware #34

Closed kaushalkumar86 closed 7 months ago

kaushalkumar86 commented 1 year ago
public async void EnableListener(string UserId)
    {
        try
        {
            var channel = supabase.Realtime.Channel("realtime", "public", "Users", "userID", UserId);
            channel.AddPostgresChangeHandler(ListenType.All, PostgresUpdatedHandler);
            await channel.Subscribe();
        }
        catch(Exception e)
        {
            MSG.Log(MSG.TAG, e.Message);
        }
    }
    private void PostgresUpdatedHandler(IRealtimeChannel _, PostgresChangesResponse change)
    {
        _Player.player = change.Model<SupaUsers.SupaPlayer>();
        MSG.Log(MSG.TAG, "Listener getting called");    **This line is working only once...**
        ispl?.OnPlayerStatusUpdate(_Player);
    }

When I update table, the listener is only getting called once. Do I need to enable listener everyting before making an update?

wiverson commented 1 year ago

What client stack are you using? Unity? MAUI?

kaushalkumar86 commented 1 year ago

Unity, developing android app, tested on Samsung S9+

On Fri, 11 Aug, 2023, 4:57 am Will Iverson, @.***> wrote:

What client stack are you using? Unity? MAUI?

— Reply to this email directly, view it on GitHub https://github.com/supabase-community/realtime-csharp/issues/34#issuecomment-1674048635, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABG2A547RUJTIFASMGGJSJ3XUVU4ZANCNFSM6AAAAAA3LONKXA . You are receiving this because you authored the thread.Message ID: @.***>

wiverson commented 1 year ago

Try moving the debug line up so it's the very first thing being called, and don't put anything else but that debug line in for the moment. Want to double-check there's nothing else wonky that's affecting the object.

kaushalkumar86 commented 1 year ago

Got it working, seems like some network issue, change my WiFi network and it works fine now, even on that same network also.

On Fri, 11 Aug, 2023, 10:03 pm Will Iverson, @.***> wrote:

Try moving the debug line up so it's the very first thing being called, and don't put anything else but that debug line in for the moment. Want to double-check there's nothing else wonky that's affecting the object.

— Reply to this email directly, view it on GitHub https://github.com/supabase-community/realtime-csharp/issues/34#issuecomment-1675065611, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABG2A54K2ZCRCAIPKBK6ULTXUZNGPANCNFSM6AAAAAA3LONKXA . You are receiving this because you authored the thread.Message ID: @.***>

wiverson commented 1 year ago

Excellent. FYI I added a Network Status class for GoTrue auth, but I haven't looked at applying any of that to Realtime. That would be something Very Cool to add but I haven't had reason to do it yet. Right now I'm focused on shipping my Unity project and for that I use Auth/GoTrue and RPC heavily but not Realtime.

In the meantime you could try using that class and the networks state listeners to update the realtime subscriptions. e.g. drop and resubscribe if the network goes away.

kaushalkumar86 commented 1 year ago

Thanks, I'll check it out. Please mark it as closed.

On Sat, 12 Aug, 2023, 11:18 pm Will Iverson, @.***> wrote:

Excellent. FYI I added a Network Status class for GoTrue auth, but I haven't looked at applying any of that to Realtime. That would be something Very Cool to add but I haven't had reason to do it yet. Right now I'm focused on shipping my Unity project and for that I use Auth/GoTrue and RPC heavily but not Realtime.

In the meantime you could try using that class and the networks state listeners to update the realtime subscriptions. e.g. drop and resubscribe if the network goes away.

— Reply to this email directly, view it on GitHub https://github.com/supabase-community/realtime-csharp/issues/34#issuecomment-1676028187, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABG2A525EE7DJBXJ363ZEVTXU66XTANCNFSM6AAAAAA3LONKXA . You are receiving this because you authored the thread.Message ID: @.***>

wiverson commented 1 year ago

Glad it's working!

I'm going to leave adding network status checking to realtime as an enhancement for now, if that's ok.

dungeon2567 commented 1 year ago

@wiverson is supabase c# now compatible with unity? last time i tried it was not and i`m using the apis directly

wiverson commented 1 year ago

I can speak to Auth and RPC working great. I literally just posted my apps this week, note that they include native Sign in with Apple talking back to Supabase auth...

https://dayboard.io/apps

I'm planning on writing up notes soon. Make sure you use UniTask for async/await, the Unity provided Newtonsoft JSON.

kaushalkumar86 commented 1 year ago

I am using supabase-C sharp. Using Google auth and its working fine, wanted to use Facebook login but it seems it requires webview.

Postgrest is working awesome, just as u wanted.

Had a couple of hiccups due to network issues in Realtime, but no fault of package.

I am using it for a new multi-player android game I am working on.

On Sun, 13 Aug, 2023, 1:37 pm dungeon2567, @.***> wrote:

@wiverson https://github.com/wiverson is supabase c# now compatible with unity? last time i tried it was not and i`m using the apis directly

— Reply to this email directly, view it on GitHub https://github.com/supabase-community/realtime-csharp/issues/34#issuecomment-1676274247, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABG2A53H7HX6JIHEE53FFPDXVCDNBANCNFSM6AAAAAA3LONKXA . You are receiving this because you authored the thread.Message ID: @.***>

wiverson commented 1 year ago

@kaushalkumar86 Which lib are you using for the Google sign in? I've used this Simple Google Sign-In in the past.

Also, did you get it working with nonces?

kaushalkumar86 commented 1 year ago

I am using https://github.com/googlesamples/google-signin-unity package for Google signing, this gives me id token, which I then pass to supabase signin to activate session. Same steps as used in firebase auth. I can share my code file if u need, its very simple.

On Mon, 14 Aug, 2023, 3:11 am Will Iverson, @.***> wrote:

@kaushalkumar86 https://github.com/kaushalkumar86 Which lib are you using for the Google sign in? I've used this Simple Google Sign-In https://assetstore.unity.com/packages/tools/integration/simple-google-sign-in-250663 in the past.

Also, did you get it working with nonces?

— Reply to this email directly, view it on GitHub https://github.com/supabase-community/realtime-csharp/issues/34#issuecomment-1676469842, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABG2A52PD5UBH7YMBYRZWV3XVFC2HANCNFSM6AAAAAA3LONKXA . You are receiving this because you were mentioned.Message ID: @.***>

kaushalkumar86 commented 1 year ago

@wiverson Alternatively, u can also check https://docs.unity.com/ugs/en-us/manual/authentication/manual/platform-signin-google , u just need the IDToken after Google Signin to pass it to Supabase auth...

kaushalkumar86 commented 1 year ago

I think I figured out the issue. This behavior of postgrest changes listening only once was caused because I was changing some texts (UI changes) as soon as I got and update. Moved those UI changes to update function (UI thread) and it seems to work fine now. Anyhow, the listener is not on UI thread and I was updating UI

kaushalkumar86 commented 1 year ago

Hello again, I found a couple of issues with NetworkStatus class. I checked on android.

  1. If "https://PROJECTID.supabase.co/auth/v1/settings" is used, the reply.StatusCode is always unauthorized.
  2. NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; doesnt work (at least on android).

I had to go through a conventional way. created a class which calls a link at an interval of say 4 secs in loop. If the response is ok, we have internet and it resubscribe the realtime listener, else it shows network not available.

wiverson commented 1 year ago

Hmm. FWIW here is the Unity script I'm using to initialize things. I'm building & testing on iOS mainly right now, Apple Watch right now, Android is next. It's possible that there is something about the init sequence and/or the interaction with the UI thread is borking stuff up.

Might want to try breaking into pieces eg just see if you can get the NetworkChange stuff to work alone.

```csharp #nullable enable using System; using io.notification; using Supabase; using Supabase.Gotrue; using Supabase.Gotrue.Interfaces; using TMPro; using UnityEngine; using static io.notification.NotificationManager; using static io.notification.NotificationManager.NotificationType; using Client = Supabase.Client; using Debug = UnityEngine.Debug; namespace io.supabase { public class SupabaseManager : MonoBehaviour { private static SupabaseManager _loaded = null!; public SupabaseSettings SupabaseSettings = null!; public NotificationManager NotificationManager = null!; public TMP_Text email = null!; private Client? _supabase; public readonly NetworkStatus NetworkStatus = new(); public Settings? Settings { get; private set; } public bool Ready { get; private set; } private async void Start() { if (!NotificationManager) throw new ApplicationException("Missing Notification Manager"); SupabaseOptions options = new(); options.AutoRefreshToken = true; _supabase = new Client(SupabaseSettings.SupabaseURL, SupabaseSettings.SupabaseAnonKey, options); _supabase.Auth.AddDebugListener(DebugListener); NetworkStatus.Client = (Supabase.Gotrue.Client)_supabase.Auth; _supabase.Auth.SetPersistence(new UnitySession()); _supabase.Auth.AddStateChangedListener(UnityAuthListener); _supabase.Auth.LoadSession(); _supabase.Auth.Options.AllowUnconfirmedUserSessions = true; string url = $"{SupabaseSettings.SupabaseURL}/auth/v1/settings?apikey={SupabaseSettings.SupabaseAnonKey}"; try { _supabase!.Auth.Online = await NetworkStatus.StartAsync(url); } catch (Exception) { _supabase!.Auth.Online = false; } if (_supabase.Auth.Online) { await _supabase.InitializeAsync(); Settings = await _supabase.Auth.Settings(); } Ready = true; _loaded = this; } public Client? Client() { return _supabase; } private static void DebugListener(string message, Exception? exception) { PostMessage(NotificationType.Debug, message, exception); Debug.Log(message); if (exception != null) { Debug.Log(exception.Message); #if !ENABLE_IL2CPP // Debug.LogException(exception); #endif } } private void UnityAuthListener(IGotrueClient sender, Constants.AuthState newState) { if (sender.CurrentUser?.Email == null) email.text = ""; else { email.text = sender.CurrentUser.Email; } switch (newState) { case Constants.AuthState.SignedIn: PostMessage(Auth, "Signed In"); break; case Constants.AuthState.SignedOut: PostMessage(Auth, "Signed Out"); break; case Constants.AuthState.UserUpdated: PostMessage(Auth, "Signed In"); break; case Constants.AuthState.PasswordRecovery: PostMessage(Auth, "Password Recovery"); break; case Constants.AuthState.TokenRefreshed: //PostMessage(NotificationType.Debug, "Signed In"); break; case Constants.AuthState.Shutdown: break; default: Debug.Log($"Unknown Auth State {nameof(newState)}"); break; } } public static SupabaseManager Supabase() { return _loaded; } private void OnApplicationQuit() { if (_supabase != null) { _supabase?.Auth.Shutdown(); _supabase = null; } } } } ```
kaushalkumar86 commented 1 year ago

Hi, I checked your code, u might be getting it to work on Apple or other devices, but its not working on Android.

  1. $"{SupabaseSettings.SupabaseURL}/auth/v1/settings?apikey={SupabaseSettings.SupabaseAnonKey}" URL is not responding with status "OK". Neither supabase.Postgrest.BaseUrl nor "supabase.Auth.Options.Url + "/settings?apikey=" + apiKey is giving status as OK. But if I use ""https://www.google.com" or "https://www.supabase.com", status come as OK.
  2. It will inform if its online or not just once, and "NetworkChange.NetworkAvailabilityChanged" never gets called (on Android). Also, as per docs, it will get called it network has changed, say, changed from mobile network to wifi, but its not sure if we have internet.
  3. I am developing an online multiplayer game which relies on continuous internet connection, so this might not work.

What I did is, made a NetworkHandler class as per below:-

public class NetworkHandler : MonoBehaviour
{
public static NetworkHandler manager;

private bool checkerIsActive = false;
private Action<NetworkStatus> networkListener = null;
private NetworkStatus currStatus = NetworkStatus.None;

private void Awake()
{
    manager = this;
}
public void SetNetworkListener(Action<NetworkStatus> listener)
{
    networkListener = listener;
}
public async void StartNetChecker(string url)
{
    checkerIsActive = true;
    await PingCheck(url);
}

private void SetInternetAccess(NetworkStatus isOnline)
{
    if (currStatus != isOnline)
    {
        currStatus = isOnline;
        networkListener?.Invoke(currStatus);
    }

}
public async Task PingCheck(string url)
{
    while (checkerIsActive)
    {
        if (Application.internetReachability != NetworkReachability.NotReachable)
        {
            try
            {
                var reply = await new HttpClient().GetAsync(url);
                if (reply?.StatusCode == System.Net.HttpStatusCode.OK)
                {
                    SetInternetAccess(NetworkStatus.Online);
                }
                else
                {
                    SetInternetAccess(NetworkStatus.Offline);
                }
            }
            catch (Exception)
            {
                SetInternetAccess(NetworkStatus.Offline);
            }
        }
        else
        {
            SetInternetAccess(NetworkStatus.Offline);
            await Task.Delay(Preferences.oneSecinMilli);
        }
        await Task.Delay(Preferences.twoSecinMilli);
    }
}

private void OnDestroy()
{
    networkListener = null;
    checkerIsActive = false;
}

public enum NetworkStatus
{
    Online,
    Offline,
    None
}

/*public static void StartNetworkChecker()    //If wanted to do as Coroutine
{
    checkerIsActive = true;
    if (NetworkChecker != null)
    {
        manager.StopCoroutine(NetworkChecker);
        NetworkChecker = null;
    }
    NetworkChecker = InternetAccessCheck();
    manager.StartCoroutine(NetworkChecker);
}
public static void StopNetworkChecker()
{
    checkerIsActive = false;
    if (NetworkChecker != null)
        manager.StopCoroutine(NetworkChecker);
}

private static IEnumerator InternetAccessCheck()
{
    MSG.Log(MSG.TAG, "Network Started");
    while (checkerIsActive)
    {
        if (Application.internetReachability != NetworkReachability.NotReachable)
        {
            UnityWebRequest request = new UnityWebRequest("https://www.supabase.com/");
            yield return request.SendWebRequest();
            if (string.IsNullOrEmpty(request.error))
            {
                SetInternetAccess(true);
            }
            else
            {
                SetInternetAccess(false);
            }
        }
        else
        {
            SetInternetAccess(false);
            yield return new WaitForSeconds(2f);
        }
        yield return new WaitForSeconds(2f);
    }
}*/}

And then from the SupabaseHandler class called an instance of this cass as:-

public void EnableListener()
{
    NetworkHandler.manager.SetNetworkListener(NetworkListener);
    NetworkHandler.manager.StartNetChecker(Preferences.stringPingUrl);   
}

private async void EnablePostGrestListener(PlayerClass userSent)
{
    try
    {
        databaseChannel = supabase.Realtime.Channel("public-users");
        databaseChannel.Register(new PostgresChangesOptions(schema: "public", table: "Users", ListenType.Updates, $"userID=eq.{userSent.UserId}"));
        databaseChannel.AddPostgresChangeHandler(ListenType.All, PostgresUpdatedHandler);
        IRealtimeChannel cn = await databaseChannel.Subscribe();
    }
    catch (Exception e)
    {
        MSG.Log(MSG.TAG, e.Message);
    }
}

private void PostgresUpdatedHandler(IRealtimeChannel _, PostgresChangesResponse change)
{
    _Player.player = change.Model<SupaUsers.SupaPlayer>();
    ispl?.OnPlayerStatusUpdate(_Player);
}

//END of Realtime-Postgrest

private void NetworkListener(NetworkStatus isNetworkAvailable)
{
    if (isNetworkAvailable == NetworkStatus.Online)
    {
        if (databaseChannel != null)
        {
            databaseChannel.Unsubscribe();
            databaseChannel = null;
        }
        EnablePostGrestListener(PlayerClass user);
    }
    else
    {
        MSG.Log(MSG.TAG, "No internet");
        ispl?.OnErrorOccured(); //No internet
    }
}
//ENd of Network-Scan

It will run a task which continuously checks for internet at an interval of 2 seconds.

wiverson commented 1 year ago

https://github.com/supabase/realtime-js/issues/121 watching as potentially related

kaushalkumar86 commented 1 year ago

Hi again..

 private async void EnablePostGrestListener(string userSent)
    {
        try
        {
            databaseChannel = supabase.Realtime.Channel("public-users");
            databaseChannel.Register(new PostgresChangesOptions(schema: "public", table: "Users", ListenType.Updates, $"userID=eq.{userSent}"));
            databaseChannel.AddPostgresChangeHandler(ListenType.Updates, PostgresUpdatedHandler);
            //databaseChannel.AddMessageReceivedHandler(MessageReceivedHandler);
            databaseChannel.AddStateChangedHandler(StateChangedHandler);
            databaseChannel.AddErrorHandler(ErrorEventHandler);
            IRealtimeChannel cn = await databaseChannel.Subscribe();
        }
        catch (Exception e)
        {
            MSG.Log(MSG.TAG, e.Message);
        }
    }

    private void MessageReceivedHandler(IRealtimeChannel sender, SocketResponse message)
    {
        MSG.Log(MSG.TAG, "Message = " + message.Topic);
        MSG.Log(MSG.TAG, "Message = " + message._event);
        MSG.Log(MSG.TAG, "Message = " + message.Payload.Message);
        MSG.Log(MSG.TAG, "Message = " + message.Payload.Status);
    }

    private void StateChangedHandler(IRealtimeChannel sender, Constants.ChannelState state)
    {
        MSG.Log(MSG.TAG, "State = " + state);
        if(state == Constants.ChannelState.Joined)
        {
            //Internet available and connected
            ispl?.OnGainInternet();
        }
        else if (state == Constants.ChannelState.Errored)
        {
            databaseChannel.Rejoin();
        }
    }
    private void ErrorEventHandler(IRealtimeChannel sender, RealtimeException exception)
    {
        MSG.Log(MSG.TAG, "exception = " + exception.ToString());
    }
    private void PostgresUpdatedHandler(IRealtimeChannel _, PostgresChangesResponse change)
    {
        _Player.player = change.Model<SupaUsers.SupaPlayer>();
        ispl?.OnPlayerStatusUpdate(_Player);
    }

    //END of Realtime-Postgrest
    private void NetworkListener(NetworkStatus isNetworkAvailable)
    {
        if (isNetworkAvailable == NetworkStatus.Online)
        {
            if (databaseChannel != null)
            {
                databaseChannel.Rejoin();
            }
            else
            {
                EnablePostGrestListener(UserID);
            }
        }
        else
        {
            ispl?.OnLostInternet(); //No internet
        }
    }

In my last update, I had no way of finding if Realtime listerner has been successfully implemented. SO, I added a few lines. The issue in this is it works sometimes and sometimes dont. The issue is with stateChangedhandler, initially the state passed is joining and after successful joining, the sate passed is joined, which is what is required and should do. But sometimes, statechangedhandler is not triggered even if the realtime listener has succesfully joined, moreover the bool value of IfisJoined remains false.

I also tried unsubscribing the channel and open it again, but it always returns with state == errored in statechangedhandler.

I am getting Push Timeout when it is errored. please guide me how should I proceed?

wiverson commented 1 year ago

I'm looking in some of the Android stuff as I just started converting my project over (I started with iOS first). One extremely weird thing I found is that so long as my Unity code uses this API - Application.internetReachability somewhere in my project it appears to be generating different Android permissions in the generated Gradle project. I was getting some weird errors related to network connectivity and the .NET network status APIs that went away when I added a call to this in my code.

Very weird, not sure if has something to do with IL2CPP code stripping (which I have flipped on for both iOS and Android).

kaushalkumar86 commented 1 year ago

I checked my manifest file, it uses only internet permissions. and .Net network status APIs never worked for me in the first place, that is why I used Application.internetReachability. The issue with Realtime listeners not working when internet is reconnected was not solved, I tried

  1. reinitiating channel (didnt work)
  2. disconnect and reconnect realtime client (didnt work)
  3. reinitiating supabase.client (worked)

I could not understand as to why this is happening. Even closed and opened realtime sockets, but everytime statechangehandler was getting triggered with state error and error msg as push timeout. Finally when closed everything and reinitialized supabase.client, realtime's statechangehandler was getting triggers with state joined.

kaushalkumar86 commented 1 year ago

This is my updated code for realtime listeners and its working properly as it should.

private async void EnablePostGrestListener(Player userSent)
    {
        try
        {
            databaseChannel = supabase.Realtime.Channel("public-users");
            databaseChannel.Register(new PostgresChangesOptions(schema: "public", table: "Users", ListenType.All, $"userID=eq.{userSent.UserId}"));
            //databaseChannel = supabase.Realtime.Channel(database: "realtime", schema: "public", table: "Users", column: "userID", value: userSent.UserId);
            databaseChannel.AddPostgresChangeHandler(ListenType.All, PostgresUpdatedHandler);
            databaseChannel.AddStateChangedHandler(StateChangedHandler);
            databaseChannel.AddErrorHandler(ErrorEventHandler);
            IRealtimeChannel cn = await databaseChannel.Subscribe();
        }
        catch (Exception e)
        {
            MSG.Log(MSG.TAG, e.Message);
            ReinitialiseSupabase(false);
        }
    }

    private void StateChangedHandler(IRealtimeChannel sender, Constants.ChannelState state)
    {
        MSG.Log(MSG.TAG, "State = " + state);
        if(state == Constants.ChannelState.Joined)
        {
            //Internet available and connected
            ispl?.OnGainInternet();
        }
        else if (state == Constants.ChannelState.Errored)
        {
            ReinitialiseSupabase(false);
        }
    }
    private void ErrorEventHandler(IRealtimeChannel sender, RealtimeException exception)
    {
        MSG.Log(MSG.TAG, "exception = " + exception.ToString());
    }
    private void PostgresUpdatedHandler(IRealtimeChannel _, PostgresChangesResponse change)
    {
        _Player.player = change.Model<SupaUsers.SupaPlayer>();
        ispl?.OnPlayerStatusUpdate(_Player);
    }

    //END of Realtime-Postgrest and start of network supabase
    private void ReinitialiseSupabase(bool supaInitialised)
    {
        if (supaInitialised)
        {
            NetworkListener(NetworkHandler.GetCurrStatus());
        }
        else
        {
            ShutSupabase();
            InitializeSupa(ReinitialiseSupabase);
        }
    }
    private void NetworkListener(NetworkStatus isNetworkAvailable)
    {
        if (isNetworkAvailable == NetworkStatus.Online)
        {
            if (supabase == null)
            {
                InitializeSupa(ReinitialiseSupabase);
            }
            else
            {
                EnablePostGrestListener(currPlayer);
            }
        }
        else
        {
            ShutSupabase();
            ispl?.OnLostInternet(); //No internet
        }
    }