diff --git a/SpacetimeDB.ClientSDK.csproj b/SpacetimeDB.ClientSDK.csproj index 72a34580..a4dbe858 100644 --- a/SpacetimeDB.ClientSDK.csproj +++ b/SpacetimeDB.ClientSDK.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index 056d940e..8cc6e135 100644 --- a/examples~/quickstart/client/Program.cs +++ b/examples~/quickstart/client/Program.cs @@ -2,11 +2,17 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Net.WebSockets; using System.Threading; using SpacetimeDB; using SpacetimeDB.ClientApi; using SpacetimeDB.Types; +const string HOST = "http://localhost:3000"; +const string DBNAME = "chatqs"; + +DbConnection? conn = null; + // our local client SpacetimeDB identity Identity? local_identity = null; // declare a thread safe queue to store commands @@ -18,7 +24,25 @@ void Main() { AuthToken.Init(".spacetime_csharp_quickstart"); - RegisterCallbacks(); + conn = DbConnection.Builder() + .WithUri(HOST) + .WithModuleName(DBNAME) + //.WithCredentials((null, AuthToken.Token)) + .OnConnect(OnConnect) + .OnConnectError(OnConnectError) + .OnDisconnect(OnDisconnect) + .Build(); + + conn.RemoteTables.User.OnInsert += User_OnInsert; + conn.RemoteTables.User.OnUpdate += User_OnUpdate; + + conn.RemoteTables.Message.OnInsert += Message_OnInsert; + + conn.RemoteReducers.OnSetName += Reducer_OnSetNameEvent; + conn.RemoteReducers.OnSendMessage += Reducer_OnSendMessageEvent; + + conn.onSubscriptionApplied += OnSubscriptionApplied; + conn.onUnhandledReducerError += onUnhandledReducerError; // spawn a thread to call process updates and process commands var thread = new Thread(ProcessThread); @@ -31,25 +55,9 @@ void Main() thread.Join(); } -void RegisterCallbacks() -{ - SpacetimeDBClient.instance.onConnect += OnConnect; - SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived; - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - SpacetimeDBClient.instance.onUnhandledReducerError += onUnhandledReducerError; - - User.OnInsert += User_OnInsert; - User.OnUpdate += User_OnUpdate; - - Message.OnInsert += Message_OnInsert; - - Reducer.OnSetNameEvent += Reducer_OnSetNameEvent; - Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent; -} - string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()[..8]; -void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) +void User_OnInsert(EventContext ctx, User insertedValue) { if (insertedValue.Online) { @@ -57,7 +65,7 @@ void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) } } -void User_OnUpdate(User oldValue, User newValue, ReducerEvent? dbEvent) +void User_OnUpdate(EventContext ctx, User oldValue, User newValue) { if (oldValue.Name != newValue.Name) { @@ -88,15 +96,15 @@ void PrintMessage(Message message) Console.WriteLine($"{senderName}: {message.Text}"); } -void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) +void Message_OnInsert(EventContext ctx, Message insertedValue) { - if (dbEvent != null) + if (ctx != null) { PrintMessage(insertedValue); } } -void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) +void Reducer_OnSetNameEvent(EventContext reducerEvent, string name) { if (reducerEvent.Identity == local_identity && reducerEvent.Status is UpdateStatus.Failed) { @@ -104,7 +112,7 @@ void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) } } -void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) +void Reducer_OnSendMessageEvent(EventContext reducerEvent, string text) { if (reducerEvent.Identity == local_identity && reducerEvent.Status is UpdateStatus.Failed) { @@ -112,15 +120,20 @@ void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) } } -void OnConnect() -{ - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User", "SELECT * FROM Message" }); -} - -void OnIdentityReceived(string authToken, Identity identity, Address _address) +void OnConnect(Identity identity, string authToken) { local_identity = identity; AuthToken.SaveToken(authToken); + + conn!.Subscribe(new List { "SELECT * FROM User", "SELECT * FROM Message" }); +} + +void OnConnectError(WebSocketError? error, string message) { + +} + +void OnDisconnect(DbConnection conn, WebSocketCloseStatus? status, WebSocketError? error) { + } void PrintMessagesInOrder() @@ -137,29 +150,26 @@ void OnSubscriptionApplied() PrintMessagesInOrder(); } -void onUnhandledReducerError(ReducerEvent reducerEvent) +void onUnhandledReducerError(EventContext reducerEvent) { Console.WriteLine($"Unhandled reducer error in {reducerEvent.ReducerName}: {reducerEvent.ErrMessage}"); } -const string HOST = "http://localhost:3000"; -const string DBNAME = "chatqs"; - void ProcessThread() { - SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME); - - // loop until cancellation token - while (!cancel_token.IsCancellationRequested) - { - SpacetimeDBClient.instance.Update(); + try { + // loop until cancellation token + while (!cancel_token.IsCancellationRequested) { + conn.Update(); - ProcessCommands(); + ProcessCommands(); - Thread.Sleep(100); + Thread.Sleep(100); + } + } + finally { + conn.Close(); } - - SpacetimeDBClient.instance.Close(); } void InputLoop() @@ -192,10 +202,10 @@ void ProcessCommands() switch (command.Command) { case "message": - Reducer.SendMessage(command.Args); + conn.RemoteReducers.SendMessage(command.Args); break; case "name": - Reducer.SetName(command.Args); + conn.RemoteReducers.SetName(command.Args); break; } } diff --git a/examples~/quickstart/client/module_bindings/Message.cs b/examples~/quickstart/client/module_bindings/Message.cs index 4676194e..ee41f6c8 100644 --- a/examples~/quickstart/client/module_bindings/Message.cs +++ b/examples~/quickstart/client/module_bindings/Message.cs @@ -14,14 +14,31 @@ namespace SpacetimeDB.Types { [SpacetimeDB.Type] [DataContract] - public partial class Message : SpacetimeDB.DatabaseTable + public partial class Message : SpacetimeDB.DatabaseTable { [DataMember(Name = "sender")] - public SpacetimeDB.Identity Sender = new(); + public SpacetimeDB.Identity Sender; [DataMember(Name = "sent")] public ulong Sent; [DataMember(Name = "text")] - public string Text = ""; + public string Text; + + public Message( + SpacetimeDB.Identity Sender, + ulong Sent, + string Text + ) + { + this.Sender = Sender; + this.Sent = Sent; + this.Text = Text; + } + + public Message() + { + this.Sender = new(); + this.Text = ""; + } public static IEnumerable FilterBySender(SpacetimeDB.Identity value) { diff --git a/examples~/quickstart/client/module_bindings/SendMessageReducer.cs b/examples~/quickstart/client/module_bindings/SendMessageReducer.cs index ffb537b6..f7565b9f 100644 --- a/examples~/quickstart/client/module_bindings/SendMessageReducer.cs +++ b/examples~/quickstart/client/module_bindings/SendMessageReducer.cs @@ -12,32 +12,9 @@ namespace SpacetimeDB.Types [SpacetimeDB.Type] public partial class SendMessageArgsStruct : IReducerArgs { - ReducerType IReducerArgs.ReducerType => ReducerType.SendMessage; string IReducerArgsBase.ReducerName => "send_message"; - bool IReducerArgs.InvokeHandler(ReducerEvent reducerEvent) => Reducer.OnSendMessage(reducerEvent, this); + bool IReducerArgs.InvokeHandler(EventContext ctx) => ctx.Reducers.InvokeSendMessage(ctx, this); public string Text = ""; } - - public static partial class Reducer - { - public delegate void SendMessageHandler(ReducerEvent reducerEvent, string text); - public static event SendMessageHandler? OnSendMessageEvent; - - public static void SendMessage(string text) - { - SpacetimeDBClient.instance.InternalCallReducer(new SendMessageArgsStruct { Text = text }); - } - - public static bool OnSendMessage(ReducerEvent reducerEvent, SendMessageArgsStruct args) - { - if (OnSendMessageEvent == null) return false; - OnSendMessageEvent( - reducerEvent, - args.Text - ); - return true; - } - } - } diff --git a/examples~/quickstart/client/module_bindings/SetNameReducer.cs b/examples~/quickstart/client/module_bindings/SetNameReducer.cs index bdc3efd2..a435dc7f 100644 --- a/examples~/quickstart/client/module_bindings/SetNameReducer.cs +++ b/examples~/quickstart/client/module_bindings/SetNameReducer.cs @@ -12,32 +12,9 @@ namespace SpacetimeDB.Types [SpacetimeDB.Type] public partial class SetNameArgsStruct : IReducerArgs { - ReducerType IReducerArgs.ReducerType => ReducerType.SetName; string IReducerArgsBase.ReducerName => "set_name"; - bool IReducerArgs.InvokeHandler(ReducerEvent reducerEvent) => Reducer.OnSetName(reducerEvent, this); + bool IReducerArgs.InvokeHandler(EventContext ctx) => ctx.Reducers.InvokeSetName(ctx, this); public string Name = ""; } - - public static partial class Reducer - { - public delegate void SetNameHandler(ReducerEvent reducerEvent, string name); - public static event SetNameHandler? OnSetNameEvent; - - public static void SetName(string name) - { - SpacetimeDBClient.instance.InternalCallReducer(new SetNameArgsStruct { Name = name }); - } - - public static bool OnSetName(ReducerEvent reducerEvent, SetNameArgsStruct args) - { - if (OnSetNameEvent == null) return false; - OnSetNameEvent( - reducerEvent, - args.Name - ); - return true; - } - } - } diff --git a/examples~/quickstart/client/module_bindings/User.cs b/examples~/quickstart/client/module_bindings/User.cs index 94442532..22276055 100644 --- a/examples~/quickstart/client/module_bindings/User.cs +++ b/examples~/quickstart/client/module_bindings/User.cs @@ -14,15 +14,31 @@ namespace SpacetimeDB.Types { [SpacetimeDB.Type] [DataContract] - public partial class User : SpacetimeDB.DatabaseTableWithPrimaryKey + public partial class User : SpacetimeDB.DatabaseTableWithPrimaryKey { [DataMember(Name = "identity")] - public SpacetimeDB.Identity Identity = new(); + public SpacetimeDB.Identity Identity; [DataMember(Name = "name")] public string? Name; [DataMember(Name = "online")] public bool Online; + public User( + SpacetimeDB.Identity Identity, + string? Name, + bool Online + ) + { + this.Identity = Identity; + this.Name = Name; + this.Online = Online; + } + + public User() + { + this.Identity = new(); + } + private static Dictionary Identity_Index = new(16); public override void InternalOnValueInserted() diff --git a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs index 49dae53e..3b81afe5 100644 --- a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs +++ b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs @@ -10,59 +10,92 @@ namespace SpacetimeDB.Types { - public enum ReducerType + public interface IReducerArgs : IReducerArgsBase { - None, - SendMessage, - SetName, + bool InvokeHandler(EventContext ctx); } - public interface IReducerArgs : IReducerArgsBase + public sealed class RemoteTables { - ReducerType ReducerType { get; } - bool InvokeHandler(ReducerEvent reducerEvent); + public readonly RemoteTableHandle Message = new(); + public readonly RemoteTableHandle User = new(); } - public partial class ReducerEvent : ReducerEventBase + public sealed class RemoteReducers : RemoteBase { - public IReducerArgs? Args { get; } + public delegate void SendMessageHandler(EventContext ctx, string text); + public event SendMessageHandler? OnSendMessage; - public string ReducerName => Args?.ReducerName ?? ""; + public void SendMessage(string text) + { + conn.InternalCallReducer(new SendMessageArgsStruct { Text = text }); + } + + public bool InvokeSendMessage(EventContext ctx, SendMessageArgsStruct args) + { + if (OnSendMessage == null) return false; + OnSendMessage( + ctx, + args.Text + ); + return true; + } + public delegate void SetNameHandler(EventContext ctx, string name); + public event SetNameHandler? OnSetName; + + public void SetName(string name) + { + conn.InternalCallReducer(new SetNameArgsStruct { Name = name }); + } - [Obsolete("ReducerType is deprecated, please match directly on type of .Args instead.")] - public ReducerType Reducer => Args?.ReducerType ?? ReducerType.None; + public bool InvokeSetName(EventContext ctx, SetNameArgsStruct args) + { + if (OnSetName == null) return false; + OnSetName( + ctx, + args.Name + ); + return true; + } + } - public ReducerEvent(IReducerArgs? args) : base() => Args = args; - public ReducerEvent(TransactionUpdate dbEvent, IReducerArgs? args) : base(dbEvent) => Args = args; + public partial class EventContext : EventContextBase + { + public IReducerArgs? Args { get; } - [Obsolete("Accessors that implicitly cast `Args` are deprecated, please match `Args` against the desired type explicitly instead.")] - public SendMessageArgsStruct SendMessageArgs => (SendMessageArgsStruct)Args!; - [Obsolete("Accessors that implicitly cast `Args` are deprecated, please match `Args` against the desired type explicitly instead.")] - public SetNameArgsStruct SetNameArgs => (SetNameArgsStruct)Args!; + public string ReducerName => Args?.ReducerName ?? ""; + + public EventContext(DbConnection conn, TransactionUpdate update, IReducerArgs? args) : base(conn.RemoteTables, conn.RemoteReducers, update) => Args = args; public override bool InvokeHandler() => Args?.InvokeHandler(this) ?? false; } - public class SpacetimeDBClient : SpacetimeDBClientBase + public class DbConnection : DbConnectionBase { - protected SpacetimeDBClient() + public readonly RemoteTables RemoteTables = new(); + public readonly RemoteReducers RemoteReducers = new(); + + public DbConnection() { + RemoteReducers.Init(this); + clientDB.AddTable(); clientDB.AddTable(); } - public static readonly SpacetimeDBClient instance = new(); - - protected override ReducerEvent ReducerEventFromDbEvent(TransactionUpdate update) + protected override EventContext ReducerEventFromDbEvent(TransactionUpdate update) { - var argBytes = update.ReducerCall.Args; + var encodedArgs = update.ReducerCall.Args; IReducerArgs? args = update.ReducerCall.ReducerName switch { - "send_message" => BSATNHelpers.Decode(argBytes), - "set_name" => BSATNHelpers.Decode(argBytes), + "send_message" => BSATNHelpers.Decode(encodedArgs), + "set_name" => BSATNHelpers.Decode(encodedArgs), "" => null, + "__identity_connected__" => null, + "__identity_disconnected__" => null, + "" => null, var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; - return new ReducerEvent(update, args); + return new EventContext(this, update, args); } } } diff --git a/examples~/quickstart/client/client.csproj b/examples~/quickstart/client/spacetimedb-quickstart-client-cs.csproj similarity index 58% rename from examples~/quickstart/client/client.csproj rename to examples~/quickstart/client/spacetimedb-quickstart-client-cs.csproj index 93444533..31665400 100644 --- a/examples~/quickstart/client/client.csproj +++ b/examples~/quickstart/client/spacetimedb-quickstart-client-cs.csproj @@ -11,6 +11,8 @@ + + diff --git a/src/IDatabaseTable.cs b/src/IDatabaseTable.cs index a028771a..e46db7ec 100644 --- a/src/IDatabaseTable.cs +++ b/src/IDatabaseTable.cs @@ -9,14 +9,14 @@ public interface IDatabaseTable { void InternalOnValueInserted(); void InternalOnValueDeleted(); - void OnInsertEvent(ReducerEventBase? update); - void OnBeforeDeleteEvent(ReducerEventBase? update); - void OnDeleteEvent(ReducerEventBase? update); + void OnInsertEvent(IEventContext? update); + void OnBeforeDeleteEvent(IEventContext? update); + void OnDeleteEvent(IEventContext? update); } - public abstract class DatabaseTable : IDatabaseTable - where T : DatabaseTable, IStructuralReadWrite, new() - where ReducerEvent : ReducerEventBase + public abstract class DatabaseTable : IDatabaseTable + where T : DatabaseTable, IStructuralReadWrite, new() + where EventContext : IEventContext { public virtual void InternalOnValueInserted() { } @@ -37,46 +37,46 @@ public static int Count() return ClientCache.TableCache.Entries.Count; } - public delegate void InsertEventHandler(T insertedValue, ReducerEvent? dbEvent); - public delegate void DeleteEventHandler(T deletedValue, ReducerEvent? dbEvent); + public delegate void InsertEventHandler(T insertedValue, EventContext? dbEvent); + public delegate void DeleteEventHandler(T deletedValue, EventContext? dbEvent); public static event InsertEventHandler? OnInsert; public static event DeleteEventHandler? OnBeforeDelete; public static event DeleteEventHandler? OnDelete; - public void OnInsertEvent(ReducerEventBase? dbEvent) + public void OnInsertEvent(IEventContext? dbEvent) { - OnInsert?.Invoke((T)this, (ReducerEvent?)dbEvent); + OnInsert?.Invoke((T)this, (EventContext?)dbEvent); } - public void OnBeforeDeleteEvent(ReducerEventBase? dbEvent) + public void OnBeforeDeleteEvent(IEventContext? dbEvent) { - OnBeforeDelete?.Invoke((T)this, (ReducerEvent?)dbEvent); + OnBeforeDelete?.Invoke((T)this, (EventContext?)dbEvent); } - public void OnDeleteEvent(ReducerEventBase? dbEvent) + public void OnDeleteEvent(IEventContext? dbEvent) { - OnDelete?.Invoke((T)this, (ReducerEvent?)dbEvent); + OnDelete?.Invoke((T)this, (EventContext?)dbEvent); } } public interface IDatabaseTableWithPrimaryKey : IDatabaseTable { - void OnUpdateEvent(IDatabaseTableWithPrimaryKey newValue, ReducerEventBase? update); + void OnUpdateEvent(IDatabaseTableWithPrimaryKey newValue, IEventContext? update); object GetPrimaryKeyValue(); } - public abstract class DatabaseTableWithPrimaryKey : DatabaseTable, IDatabaseTableWithPrimaryKey - where T : DatabaseTableWithPrimaryKey, IStructuralReadWrite, new() - where ReducerEvent : ReducerEventBase + public abstract class DatabaseTableWithPrimaryKey : DatabaseTable, IDatabaseTableWithPrimaryKey + where T : DatabaseTableWithPrimaryKey, IStructuralReadWrite, new() + where EventContext : IEventContext { public abstract object GetPrimaryKeyValue(); - public delegate void UpdateEventHandler(T oldValue, T newValue, ReducerEvent? update); + public delegate void UpdateEventHandler(T oldValue, T newValue, EventContext? update); public static event UpdateEventHandler? OnUpdate; - public void OnUpdateEvent(IDatabaseTableWithPrimaryKey newValue, ReducerEventBase? dbEvent) + public void OnUpdateEvent(IDatabaseTableWithPrimaryKey newValue, IEventContext? dbEvent) { - OnUpdate?.Invoke((T)this, (T)newValue, (ReducerEvent?)dbEvent); + OnUpdate?.Invoke((T)this, (T)newValue, (EventContext?)dbEvent); } } } diff --git a/src/Primitives.cs b/src/Primitives.cs deleted file mode 100644 index 9466cca5..00000000 --- a/src/Primitives.cs +++ /dev/null @@ -1,83 +0,0 @@ -using SpacetimeDB.BSATN; - -using System; -using System.IO; - -namespace SpacetimeDB -{ - - public struct I128 : IEquatable - { - public long hi; - public ulong lo; - - public I128(long hi, ulong lo) - { - this.hi = hi; - this.lo = lo; - } - - public readonly bool Equals(I128 x) => hi == x.hi && lo == x.lo; - - public override readonly bool Equals(object? o) => o is I128 x && Equals(x); - - public static bool operator ==(I128 a, I128 b) => a.Equals(b); - public static bool operator !=(I128 a, I128 b) => !a.Equals(b); - - public override readonly int GetHashCode() => hi.GetHashCode() ^ lo.GetHashCode(); - - public override readonly string ToString() => $"I128({hi},{lo})"; - - public readonly struct BSATN : IReadWrite - { - public I128 Read(BinaryReader reader) => new(reader.ReadInt64(), reader.ReadUInt64()); - - public void Write(BinaryWriter writer, I128 value) - { - writer.Write(value.hi); - writer.Write(value.lo); - } - - public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - new AlgebraicType.Builtin(new BuiltinType.I128(new Unit())); - } - } - - public struct U128 : IEquatable - { - public ulong hi; - public ulong lo; - - public U128(ulong hi, ulong lo) - { - this.lo = lo; - this.hi = hi; - } - - public readonly bool Equals(U128 x) => hi == x.hi && lo == x.lo; - - public override readonly bool Equals(object? o) => o is U128 x && Equals(x); - - public static bool operator ==(U128 a, U128 b) => a.Equals(b); - public static bool operator !=(U128 a, U128 b) => !a.Equals(b); - - public override readonly int GetHashCode() => hi.GetHashCode() ^ lo.GetHashCode(); - - public override readonly string ToString() => $"U128({hi},{lo})"; - - public readonly struct BSATN : IReadWrite - { - public U128 Read(BinaryReader reader) => new(reader.ReadUInt64(), reader.ReadUInt64()); - - public void Write(BinaryWriter writer, U128 value) - { - writer.Write(value.hi); - writer.Write(value.lo); - } - - public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - new AlgebraicType.Builtin(new BuiltinType.U128(new Unit())); - } - } - -} diff --git a/src/Primitives.cs.meta b/src/Primitives.cs.meta deleted file mode 100644 index 3ed18da1..00000000 --- a/src/Primitives.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 565602d5968368295b6cf26721a8890a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index e0a9daa4..cc8705fb 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -14,9 +14,64 @@ namespace SpacetimeDB { - public abstract class SpacetimeDBClientBase - where ReducerEvent : ReducerEventBase + public sealed class DbConnectionBuilder + where DbConnection : DbConnectionBase, new() + where EventContext : class, IEventContext { + readonly DbConnection conn = new(); + + string? uri; + string? nameOrAddress; + string? token; + + public DbConnection Build() { + if (uri == null) { + throw new InvalidOperationException("Building DbConnection with a null uri. Call WithUri() first."); + } + if (nameOrAddress == null) { + throw new InvalidOperationException("Building DbConnection with a null nameOrAddress. Call WithModuleName() first."); + } + conn.Connect(token, uri, nameOrAddress); + return conn; + } + + public DbConnectionBuilder WithUri(string uri) { + this.uri = uri; + return this; + } + + public DbConnectionBuilder WithModuleName(string nameOrAddress) { + this.nameOrAddress = nameOrAddress; + return this; + } + + public DbConnectionBuilder WithCredentials(in (Identity identity, string token)? creds) { + token = creds?.token; + return this; + } + + public DbConnectionBuilder OnConnect(Action cb) { + conn.onConnect += cb; + return this; + } + + public DbConnectionBuilder OnConnectError(Action cb) { + conn.webSocket.OnConnectError += (a, b) => cb.Invoke(a, b); + return this; + } + + public DbConnectionBuilder OnDisconnect(Action cb) { + conn.webSocket.OnClose += (code, error) => cb.Invoke(conn, code, error); + return this; + } + } + + public abstract class DbConnectionBase + where DbConnection : DbConnectionBase, new() + where EventContext : class, IEventContext + { + public static DbConnectionBuilder Builder() => new(); + struct DbValue { public IDatabaseTable value; @@ -36,26 +91,13 @@ struct DbOp public DbValue? insert; } - /// - /// Called when a connection is established to a spacetimedb instance. - /// - public event Action? onConnect; - - /// - /// Called when a connection attempt fails. - /// - public event Action? onConnectError; + internal event Action? onConnect; /// /// Called when an exception occurs when sending a message. /// public event Action? onSendError; - /// - /// Called when a connection that was established has disconnected. - /// - public event Action? onDisconnect; - /// /// Invoked when a subscription is about to start being processed. This is called even before OnBeforeDelete. /// @@ -69,12 +111,7 @@ struct DbOp /// /// Invoked when a reducer is returned with an error and has no client-side handler. /// - public event Action? onUnhandledReducerError; - - /// - /// Called when we receive an identity from the server - /// - public event Action? onIdentityReceived; + public event Action? onUnhandledReducerError; /// /// Invoked when an event message is received or at the end of a transaction update. @@ -84,11 +121,11 @@ struct DbOp public readonly Address clientAddress = Address.Random(); public Identity? clientIdentity { get; private set; } - private SpacetimeDB.WebSocket webSocket; + internal WebSocket webSocket; private bool connectionClosed; protected readonly ClientCache clientDB = new(); - protected abstract ReducerEvent ReducerEventFromDbEvent(TransactionUpdate dbEvent); + protected abstract EventContext ReducerEventFromDbEvent(TransactionUpdate dbEvent); private readonly Dictionary> waitingOneOffQueries = new(); @@ -96,7 +133,7 @@ struct DbOp private readonly Thread networkMessageProcessThread; public readonly Stats stats = new(); - protected SpacetimeDBClientBase() + protected DbConnectionBase() { var options = new ConnectOptions { @@ -106,9 +143,6 @@ protected SpacetimeDBClientBase() }; webSocket = new WebSocket(options); webSocket.OnMessage += OnMessageReceived; - webSocket.OnClose += (code, error) => onDisconnect?.Invoke(code, error); - webSocket.OnConnect += () => onConnect?.Invoke(); - webSocket.OnConnectError += (a, b) => onConnectError?.Invoke(a, b); webSocket.OnSendError += a => onSendError?.Invoke(a); networkMessageProcessThread = new Thread(PreProcessMessages); @@ -126,7 +160,7 @@ struct ProcessedMessage public ServerMessage message; public List dbOps; public DateTime timestamp; - public ReducerEvent? reducerEvent; + public EventContext reducerEvent; } struct PreProcessedMessage @@ -177,7 +211,7 @@ PreProcessedMessage PreProcessMessage(UnprocessedMessage unprocessed) using var binaryReader = new BinaryReader(decompressedStream); var message = new ServerMessage.BSATN().Read(binaryReader); - ReducerEvent? reducerEvent = null; + EventContext? reducerEvent = null; // This is all of the inserts Dictionary>? subscriptionInserts = null; @@ -463,7 +497,7 @@ public void Connect(string? token, string uri, string addressOrName) } - private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List dbOps) + private void OnMessageProcessCompleteUpdate(EventContext? dbEvent, List dbOps) { // First trigger OnBeforeDelete foreach (var update in dbOps) @@ -627,7 +661,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) { clientIdentity = identityToken.Identity; var address = identityToken.Address; - onIdentityReceived?.Invoke(identityToken.Token, clientIdentity, address); + onConnect?.Invoke(identityToken.Identity, identityToken.Token); } catch (Exception e) { diff --git a/src/Stubs.cs b/src/Stubs.cs index f9ba6202..79a2120e 100644 --- a/src/Stubs.cs +++ b/src/Stubs.cs @@ -1,5 +1,7 @@ using SpacetimeDB.ClientApi; +using System; + namespace SpacetimeDB { public interface IReducerArgsBase : BSATN.IStructuralReadWrite @@ -7,7 +9,22 @@ public interface IReducerArgsBase : BSATN.IStructuralReadWrite string ReducerName { get; } } - public abstract class ReducerEventBase + public abstract class DbContext : DbContext + where DbView : class, new() + where ReducerView : class, new() + { + public ReducerView Reducers; + + public DbContext(DbView db, ReducerView reducers) : base(db) => Reducers = reducers; + } + + public interface IEventContext { + bool InvokeHandler(); + } + + public abstract class EventContextBase : DbContext, IEventContext + where RemoteTables : class, new() + where RemoteReducers : class, new() { public ulong Timestamp { get; } public Identity? Identity { get; } @@ -15,9 +32,7 @@ public abstract class ReducerEventBase public string? ErrMessage { get; } public UpdateStatus? Status { get; } - public ReducerEventBase() { } - - public ReducerEventBase(TransactionUpdate update) + public EventContextBase(RemoteTables db, RemoteReducers reducers, TransactionUpdate update) : base(db, reducers) { Timestamp = update.Timestamp.Microseconds; Identity = update.CallerIdentity; @@ -31,4 +46,20 @@ public ReducerEventBase(TransactionUpdate update) public abstract bool InvokeHandler(); } + + public abstract class RemoteBase { +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. + protected DbConnection conn; +#pragma warning restore CS8618 + + public void Init(DbConnection conn) { + this.conn = conn; + } + } + + public class RemoteTableHandle where EventContext : class, IEventContext { + public event Action? OnInsert; + public event Action? OnDelete; + public event Action? OnUpdate; + } }